论坛首页 Web前端技术论坛

(原创)新概念智能树形菜单--利用加权多叉树结合JavaScript树形控件实现

浏览 6416 次
精华帖 (1) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2012-02-08   最后修改:2012-04-15

(原创)新概念智能树形菜单--利用加权多叉树结合JavaScript树形控件实现


一、 智能树形菜单的概念
在描述智能树形菜单的概念之前,请先阅读我的上一篇文章《多叉树结合JavaScript树形控件实现无限级树形菜单(一种构建多级有序树形结构JSON(或XML)数据源的方法)》,请点击链接查看:http://www.iteye.com/topic/1122125,阅读完上一篇文章之后,再继续向下阅读,因为这两篇文章是紧密联系、前后关联的,谢谢。

 

大家都知道树形菜单,不管在基于什么样的技术开发的系统上,树形菜单都是用户界面中非常常用的一种菜单形式,在目前市场上常见的JavaScript框架及组件库中均包含自己的树形控件,例如JQuery、Dojo、Yahoo UI、Ext JS等,还有一些独立的树形控件,例如dhtmlxtree等,这些树形控件完美的解决了层次数据的展示问题。那么树形菜单发展到现在就算完美了吗?还有没有进一步的发展空间呢?还有没有进一步的完善空间呢?答案是肯定的。在这篇文章里,我提出一种新的想法对树形菜单进行扩展,也许会给您的项目带来一些新的用户体验,我管这种扩展型的树形菜单叫做“智能树形菜单”。那么智能树形菜单和普通的树形菜单到底有什么区别呢,它到底智能在哪呢,让我们先来看一组用例场景: 

 

网民高小宝是一名网上银行的忠实用户,他经常利用网银办理各种银行业务,假设高小宝在
某某银行开通了网上银行功能,他回到家之后,想试验一下这个新的网银系统,于是他首次登录了某某银行的网上银行,登录成功后,系统展示界面如下所示(简略概要图):
图1.1

 

他想给自己的朋友李小磊转一笔钱,于是他在界面左上角的“输入交易功能关键字”输入框中输入“转账”二字,点击“搜索”按钮,查询出了如下的结果,如图:
图1.2


 

他看到左侧的树形菜单区域中搜索出了三个交易功能,分别是行内转账、跨行转账和跨行快速转账,于是他点击跨行转账后,主界面进入跨行转账信息录入界面,高小宝很快完成了转账操作,如下图:
图1.3
 

 

高小宝一看,这个银行的网上银行系统操作这么方便,于是他又陆续使用了以下几个功能,分别是“投资理财->定活互转”、“投资理财->理财产品->产品购买”、“我的贷款->贷款信息查询”、“客户服务->昵称设置”,本次登录他一共使用了五个功能点,其中“转账汇款->跨行转账”功能使用了两次,一次是给朋友李小磊转钱,另一次是给朋友王小然转钱,其他四个功能各使用了一次。


第二天,高小宝又登录了网上银行系统,登录成功后,他看到系统的操作界面和第一次登录时不太一样了,变成了如下样子:
图1.4


 

他看到昨天使用的五个功能点菜单全部自动展开了,而且菜单顺序也发生了变化:原来排在最后的一级菜单“客户服务”居然移到了“我的贷款”下面,而原来排在第一位的“账户管理”却移到了“客户服务”下面,由于部分菜单自动展开,所以菜单栏右侧出现了滚动条。
不光一级菜单的顺序发生了变化,二级菜单的顺序也发生了变化,原来在“转账汇款”下面的子菜单中,“跨行转账”排在“行内转账”下面,现在却排在了第一位。不光菜单发生了变化,中间主界面也发生了变化,原来是欢迎界面,现在变成了“跨行转账”信息录入界面。
高小宝一看,正合我意,他正想给朋友吴艳清转一笔钱,吴艳清的开户行是工商银行,不是本行,正好需要使用跨行转账功能,于是他很快完成了转账操作。他今天还想通过“定活互转”功能,存一笔定期,由于昨天使用了一次该功能,所以这个功能也默认自动展开,他很容易就看到了这个菜单项,顺利完成了存定期的操作,也就是说功能使用率大于平均功能使用率的功能节点自动展开了(假设有10个功能,功能A和功能B分别被使用了3次和2次,其它功能都被使用了1次,那么平均使用率是(3+2+8)/ 10 = 1.3次,A和B的使用率大于平均使用率,自动展开,其他8个功能的使用率小于1.3,不展开),真智能啊!高小宝心想,看来以后得多多使用网上银行了,太方便了。


好了,读者朋友们,通过上面这组用例场景,相信您已经看出了本文要论述的主题内容,对了,这就是智能树形菜单,现在我们给智能树形菜单下一个完整科学的定义,


智能树形菜单满足如下几个条件:

A、用户首次登录系统时,功能菜单树全部闭合,并且按照菜单编号排序,且每一层菜单节点都按照这个规则排序,保持整棵功能树兄弟节点横向有序(关于“兄弟节点横向有序”的概念请查看附录:我的上一篇文章《多叉树结合JavaScript树形控件实现无限级树形菜单(一种构建多级有序树形结构JSON(或XML)数据源的方法)》);

B、系统会自动记录每一个功能路径(功能路径指的是由一级菜单节点到功能叶子节点的一条完整路径,例如“投资理财->理财产品->产品购买”就是一条功能路径)的使用率;

C、系统提供菜单节点搜索功能,用户可以输入菜单节点关键字,系统会搜索出包含关键字的所有功能路径,并展示出搜索结果;

D、用户再次登录系统时,功能菜单重新排序,并且部分展开,其规则如下:
(1)整棵功能菜单树按照使用率加节点编号的方式排序,先按使用率排序,使用率相同的情况下按照节点编号排序,保持兄弟节点横向有序;
(2)热点功能自动展开(热点功能指的是功能使用率大于所有功能平均使用率的功能叶子节点,例如在上面的用例场景中,高小宝第一次使用网银时,“跨行转账”被使用了两次,“定活互转”、“产品购买”、“贷款信息查询”、“昵称设置”都被使用了一次,其他功能被使用了零次,假设一共有30个功能,那么功能平均使用率是(2+1+1+1+1)/ 30 = 0.2, 这五个功能的使用率均大于0.2,所以都属于热点功能)

E、系统主界面默认进入焦点功能(焦点功能指的是热点功能中使用率最高的功能)的操作界面;

 

好了,智能树形菜单的概念就介绍完了,下面讲述详细实现方案。

 

 

二、 详细实现方案
在《多叉树结合JavaScript树形控件实现无限级树形菜单(一种构建多级有序树形结构JSON(或XML)数据源的方法)》一文中,谈到了多叉树,智能树形菜单就可以在多叉树的基础上进行扩展,实现概念定义中所描述的所有功能,下面介绍详细设计方案。

 

下面这张图就是扩展之后的多叉树,并且根据这张图定义了一系列名词概念,如图所示:
图1.5

 

我们先根据这张图定义一组名词概念:
1、 菜单树
所有菜单可以抽象成一棵菜单树,为了保持树的完整性,在一级菜单上面加了一个虚拟的根节点,每个节点由两个值组成,用逗号分割,前一个值是菜单节点编号,后一个值是节点权值,默认权值都为1。

 

2、节点权值
为了描述某一个节点在其兄弟节点之中的优先级(也就是功能使用率),用一个数值来表示,就叫节点权值。

 

3、功能叶子节点
菜单树中的叶子节点,下面不再下挂其他子功能节点。

 

4、功能路径
由根节点到功能叶子节点的一条完整路径,例如图中的蓝色线条部分,即 “根->3->32->323”就是一条功能路径,上图的菜单树中共有13条功能路径,您可以数一数,呵呵。

 

5、功能路径权值
功能路径权值由功能路径中所有节点的权值由父到子拼接而成,例如路径“根->3->32->323”的路径权值是“111”。

 

6、 优先功能路径
指的是功能路径权值最高的那一条路径,如上图菜单树中的红色线条部分,该路径为“根->1->11”,它的路径权值是“32”。路径权值比较的规则是逐级比较,“32”的第一级是3,“111”的第一级是1,所以“32”大于“111”,路径权值相同时比较节点编号,也就是用权值加节点编号进行比较,例如路径“根->1->11”和路径“根->1->12->121”,它们的路径权值分别是“32”和“322”,当比较到第二级时,节点权值都是2,但是由于节点11比节点12的编号小,所以节点11排列在节点12前面,也就是说功能路径“根->1->11”的优先级排在“根->1->12->121”之前。

 

7、功能叶子列表
当构造好一棵菜单树后,通过深度优先先序遍历,找到所有叶子节点,并将其存放在一个列表里面,这个列表叫做功能叶子列表。

 

8、功能叶子权值
功能叶子节点的权值叫做功能叶子权值。

 

9、热点功能叶子
功能叶子权值大于所有功能叶子节点的平均权值的叶子节点,例如上图中功能叶子列表中的前两个节点:11和121,其权值分别是2和2,而叶子节点的平均权值是1.15,用数学语言描述就是权值大于加权平均数的功能叶子节点

 

10、焦点功能叶子
热点功能叶子中权值最高的叶子叫做焦点功能叶子,如果权值相等,则比较节点编号,节点编号优先的叶子为焦点功能叶子。

 

11、功能路径可见
在菜单节点中增加一个“是否可见”属性,如果一条功能路径中所有节点的“是否可见”属性为true,那么称该条路径可见,否则该条路径不可见,所谓不可见,指的是不向用户展示这条功能路径。

 

12、菜单节点搜索
指的是在整棵功能树的所有节点中按照节点关键字查找包含该关键字的菜单节点。

 

13、功能路径筛选
指的是对整棵树的功能路径的可见性进行重新设置,首先将所有节点设置为不可见,然后根据菜单节点的搜索结果,将包含该菜单节点的所有功能路径设置为可见,也就是向用户展示菜单节点搜索的结果。

 

14、增加功能路径权值
指的是将包含某一功能叶子节点的一条完整功能路径上的所有节点的权值增加相同的幅度(例如将某一功能路径上的所有节点权值加1)。

 

 

我们可以把这棵加了节点权值的多叉树叫做加权多叉树,英文名称叫weighted multiple tree,简称W-M-Tree

 


好了,定义完名词概念之后,用下面一组图来展示加权多叉树的变化过程。

 


图一,用户首次登录系统时,在服务器端构造出加权多叉树和功能叶子列表:
图1.6
 

 

其中黄色圆圈代表功能叶子节点。

 

构造出这棵树之后,向客户端的JavaScript树形控件返回树形结构的JSON字符串,返回如下内容:

{
  id: '0',
  text: '根',
  children: [
    {
      id: '1',
      text: '1',
      children: [
        {
          id: '11',
          text: '11',
          leaf: true
        },
        {
          id: '12',
          text: '12',
          children: [
            {
              id: '121',
              text: '121',
              leaf: true
            },
            {
              id: '122',
              text: '122',
              leaf: true
            },
            {
              id: '123',
              text: '123',
              leaf: true
            }
          ]
        }
      ]
    },
    {
      id: '2',
      text: '2',
      children: [
        {
          id: '21',
          text: '21',
          leaf: true
        },
        {
          id: '22',
          text: '22',
          leaf: true
        }
      ]
    },
    {
      id: '3',
      text: '3',
      children: [
        {
          id: '31',
          text: '31',
          leaf: true
        },
        {
          id: '32',
          text: '32',
          children: [
            {
              id: '321',
              text: '321',
              leaf: true
            },
            {
              id: '322',
              text: '322',
              leaf: true
            },
            {
              id: '323',
              text: '323',
              leaf: true
            }
          ]
        }
      ]
    },
    {
      id: '4',
      text: '4',
      children: [
        {
          id: '41',
          text: '41',
          leaf: true
        },
        {
          id: '42',
          text: '42',
          leaf: true
        },
        {
          id: '43',
          text: '43',
          leaf: true
        }
      ]
    }
  ]
}

 


图二,用户使用关键字“12”在功能叶子列表中进行搜索,并将包含搜索结果的功能路径设置为可见,其他路径设置为不可见,图中红色线条部分为可见路径,其他的为不可见路径,如图所示:
图1.7
 

 

进行功能路径筛选后,向客户端返回如下JSON字符串:

{
  id: '0',
  text: '根',
  children: [
    {
      id: '1',
      text: '1',
      children: [
        {
          id: '12',
          text: '12',
          children: [
            {
              id: '121',
              text: '121',
              leaf: true
            },
            {
              id: '122',
              text: '122',
              leaf: true
            },
            {
              id: '123',
              text: '123',
              leaf: true
            }
          ]
        }
      ]
    }
  ]
}

 

 

图三,用户再使用关键字“321”在功能叶子列表中进行搜索,搜索结果如图所示:
图1.8
 

 

进行功能路径筛选后,向客户端返回如下JSON字符串:

{
  id: '0',
  text: '根',
  children: [
    {
      id: '3',
      text: '3',
      children: [
        {
          id: '32',
          text: '32',
          children: [
            {
              id: '321',
              text: '321',
              leaf: true
            }
          ]
        }
      ]
    }
  ]
}

 


图四,用户使用某个功能后,系统将该功能路径权值加1,并将该功能路径上的所有节点的权值更新到数据库中的菜单节点定义表中,如图所示,路径“根->3->32->323”的权值加1,由“111”变成了“222”:
图1.9
 

 

图五,当用户退出,再次登录系统后,在服务器端再次构造出加权多叉树和功能叶子列表,此时数据结构变成如下样子:
图2.0
 

 

树中红色线条部分为优先功能路径,功能叶子列表中红色圆圈为焦点功能叶子。


图六,用户再次使用某个功能后,系统将该功能路径权值加1,并将该功能路径上的所有节点的权值更新到数据库中的菜单节点定义表中,如图所示,路径“根->1->12->122”的权值加1,由“111”变成了“222”:
图2.1
 

 

图七,当用户退出,再次登录系统后,在服务器端再次构造出加权多叉树和功能叶子列表,此时数据结构变成如下样子:
图2.2
 

 

树中红色线条部分根据路径权值和节点编号重新进行了兄弟节点横向排序;
功能叶子列表中红色圆圈为热点叶子,现在有两个热点叶子了,其中122为焦点叶子。

 

现在服务器向客户端返回的JSON字符串变成了如下:

{
  id: '0',
  text: '根',
  children: [
    {
      id: '1',
      text: '1',
      children: [
        {
          id: '12',
          text: '12',
          children: [
            {
              id: '122',
              text: '122',
              leaf: true
            },
            {
              id: '121',
              text: '121',
              leaf: true
            },
            {
              id: '123',
              text: '123',
              leaf: true
            }
          ]
        },
        {
          id: '11',
          text: '11',
          leaf: true
        }
      ]
    },
    {
      id: '3',
      text: '3',
      children: [
        {
          id: '32',
          text: '32',
          children: [
            {
              id: '323',
              text: '323',
              leaf: true
            },
            {
              id: '321',
              text: '321',
              leaf: true
            },
            {
              id: '322',
              text: '322',
              leaf: true
            }
          ]
        },
        {
          id: '31',
          text: '31',
          leaf: true
        }
      ]
    },
    {
      id: '2',
      text: '2',
      children: [
        {
          id: '21',
          text: '21',
          leaf: true
        },
        {
          id: '22',
          text: '22',
          leaf: true
        }
      ]
    },
    {
      id: '4',
      text: '4',
      children: [
        {
          id: '41',
          text: '43211',
          leaf: true
        },
        {
          id: '42',
          text: '42',
          leaf: true
        },
        {
          id: '43',
          text: '43',
          leaf: true
        }
      ]
    }
  ]
}

 

 

对于热点功能叶子自动展开,和系统默认进入焦点功能界面这两个功能,只需要客户端通过AJAX的方式从服务器端的功能叶子列表中取出热点功能叶子和焦点功能叶子的节点编号后,在客户端通过编程处理,取热点功能叶子的方法很简单,只需要取出叶子权值大于叶子平均权值的节点即可,然后再找到一个权值最大的叶子就是焦点功能叶子;至于如何展开树形控件的叶子节点本文不做叙述,那是树形控件要实现的功能。

 

 

三、 源代码实现(服务器端JAVA代码演示)

以下代码拷贝出来后可以直接运行测试(注:关于菜单节点搜索功能,这里用java来演示,在服务器端进行,系统响应速度很慢,应该放在客户端实现):

 

package test;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Collections;

/**
 * 功能菜单树类
*/
public class FunctionTree {
	
	public static void main(String[] args) {
		// 读取层次数据结果集列表
		List dataList = VirtualDataGenerator.getVirtualResult();

		// 构造加权多叉树
		Node root = buildWeightedMultiTree(dataList);
		
		// 构造功能叶子列表
		List functionLeafList = buildFunctionLeafList(root);
		
		// 对多叉树重新进行横向排序
		root.sortChildren();
		// 输出首次登录后的树形菜单
		System.out.println("首次登录时的树形菜单:\n" + root.toString());	
		
		// 进行菜单节点搜索(即功能路径筛选)
		searchTreeNode(root, "321");
		
		// 输出搜索结果
		System.out.println("搜索后的树形菜单:\n" + root.toString());
		
		// 增加功能路径权值
		increaseRouteWeight(root, functionLeafList, "122");
		increaseRouteWeight(root, functionLeafList, "323");
		
		// 对多叉树重新进行横向排序
		root.sortChildren();
		// 输出权值变化后的树形菜单		
		System.out.println("路径权值变化后再次登录时的树形菜单:\n" + root.toString());
		
		// 获取热点功能叶子
		List hotFunctionLeaf = getHotFunctionLeaf(functionLeafList);
		
		// 输出热点功能叶子
		printHotFunctionLeaf(hotFunctionLeaf);

		// 程序输出结果如下:
		// 首次登录时的树形菜单:
		// {id : '0', text : '根', children : [{id : '1', text : '1', children : [{id : '11', text : '11', leaf : true},{id : '12', text : '12', children : [{id : '121', text : '121', leaf : true},{id : '122', text : '122', leaf : true},{id : '123', text : '123', leaf : true}]}]},{id : '2', text : '2', children : [{id : '21', text : '21', leaf : true},{id : '22', text : '22', leaf : true}]},{id : '3', text : '3', children : [{id : '31', text : '31', leaf : true},{id : '32', text : '32', children : [{id : '321', text : '321', leaf : true},{id : '322', text : '322', leaf : true},{id : '323', text : '323', leaf : true}]}]},{id : '4', text : '4', children : [{id : '41', text : '41', leaf : true},{id : '42', text : '42', leaf : true},{id : '43', text : '43', leaf : true}]}]}
		// 搜索后的树形菜单:
		// {id : '0', text : '根', children : [{id : '3', text : '3', children : [{id : '32', text : '32', children : [{id : '321', text : '321', leaf : true}]}]}]}
		// 路径权值变化后再次登录时的树形菜单:
		// {id : '0', text : '根', children : [{id : '1', text : '1', children : [{id : '12', text : '12', children : [{id : '122', text : '122', leaf : true},{id : '121', text : '121', leaf : true},{id : '123', text : '123', leaf : true}]},{id : '11', text : '11', leaf : true}]},{id : '3', text : '3', children : [{id : '32', text : '32', children : [{id : '323', text : '323', leaf : true},{id : '321', text : '321', leaf : true},{id : '322', text : '322', leaf : true}]},{id : '31', text : '31', leaf : true}]},{id : '2', text : '2', children : [{id : '21', text : '21', leaf : true},{id : '22', text : '22', leaf : true}]},{id : '4', text : '4', children : [{id : '41', text : '41', leaf : true},{id : '42', text : '42', leaf : true},{id : '43', text : '43', leaf : true}]}]}
		// 热点功能叶子:
		// [{id : '323', text : '323', leaf : true}, {id : '122', text : '122', leaf : true}]
	}			
	
	/**
	 * 构造加权多叉树
	 * @return
	 */
	public static Node buildWeightedMultiTree(List dataList) {
		// 节点列表(散列表,用于临时存储节点对象)
		HashMap nodeList = new HashMap();
		// 根节点
		Node root = null;
		// 根据结果集构造节点列表(存入散列表)
		for (Iterator it = dataList.iterator(); it.hasNext();) {
			Map dataRecord = (Map) it.next();
			Node node = new Node();
			node.id = (String) dataRecord.get("id");
			node.text = (String) dataRecord.get("text");
			node.parentId = (String) dataRecord.get("parentId");
			node.weight = Integer.parseInt((String) dataRecord.get("weight"));
			nodeList.put(node.id, node);
		}
		// 构造无序的多叉树
		Set entrySet = nodeList.entrySet();
		for (Iterator it = entrySet.iterator(); it.hasNext();) {
			Node node = (Node) ((Map.Entry) it.next()).getValue();
			if (node.parentId == null || node.parentId.equals("")) {
				root = node;
			} else {
				((Node) nodeList.get(node.parentId)).addChild(node);
				// 在节点中增加一个父节点的引用
				node.parentNode = (Node) nodeList.get(node.parentId);
			}
		}

		return root;
	} 
	
	/**
	 * 构造功能叶子列表
	 * @param root
	 * @return
	 */
	public static List buildFunctionLeafList(Node root) {
		List functionLeafList = new ArrayList();
		root.initializeLeafList(functionLeafList);
		return functionLeafList;
	}
	
	/**
	 * 进行菜单节点搜索(即功能路径筛选)
	 * @param root
	 * @param keyWord
	 */
	public static void searchTreeNode(Node root, String keyWord) {
		// 首先设置整棵树的功能路径为不可见
		root.setTreeNotVisible();
		// 在整棵功能树中搜索包含关键字的节点,并进行路径筛选
		root.searchTreeNode(keyWord);					
	}
	
	/**
	 * 增加功能路径权值
	 * @param root
	 */
	public static void increaseRouteWeight(Node root, List functionLeafList, String nodeId) {
		// 首先设置整棵树的功能路径为可见
		root.setTreeVisible();		
		// 对包含功能叶子节点的路径权值加1
		for (Iterator it = functionLeafList.iterator(); it.hasNext();) {
			Node leafNode = (Node) it.next();
			if (leafNode.id.equals(nodeId)) {
				leafNode.increaseRouteWeight();
			}
		}		
	}
	
	/**
	 * 获取热点功能叶子
	 * @param functionLeafList
	 * @return
	 */
	public static List getHotFunctionLeaf(List functionLeafList) {
		int count = 0;
		int totalWeight = 0;
		BigDecimal avgWeight;
		for (Iterator it = functionLeafList.iterator(); it.hasNext();) {
			Node node = (Node) it.next();
			totalWeight += node.weight;
			count++;
		}
		avgWeight = (new BigDecimal(totalWeight)).divide(new BigDecimal(count), 2, BigDecimal.ROUND_HALF_UP);
		List retList = new ArrayList();
		for (Iterator it = functionLeafList.iterator(); it.hasNext();) {
			Node node = (Node) it.next();
			if (node.weight > avgWeight.doubleValue()) {
				retList.add(node);
			}
		}
		return retList;
	}
	
	/**
	 * 输出热点功能叶子
	 * @param hotFunctionLeaf
	 */
	public static void printHotFunctionLeaf(List hotFunctionLeaf) {
		System.out.println("热点功能叶子:\n" + hotFunctionLeaf);
	}
	
	
}


/**
* 节点类
*/
class Node {
	/**
	 * 节点编号
	 */
	public String id;
	/**
	 * 节点内容
	 */
	public String text;
	/**
	 * 父节点编号
	 */
	public String parentId;
	/**
	 * 节点权值
	 */
	public int weight;
	/**
	 * 是否可见,默认为true
	 */
	public boolean visible = true;
	/**
	 * 父节点引用
	 */
	public Node parentNode;
	/**
	 * 孩子节点列表
	 */
	private Children children = new Children();
	
	// 添加孩子节点
	public void addChild(Node node) {
		this.children.addChild(node);
	}
	
	// 先序遍历,拼接JSON字符串
	public String toString() {
		if (visible) {
			String result = "{"
				+ "id : '" + id + "'"
				+ ", text : '" + text + "'";
			
			if (children != null && children.getSize() != 0) {
				result += ", children : " + children.toString();
			} else {
				result += ", leaf : true";
			}
					
			return result + "}";
		} else {
			return "";
		}
	}
	
	// 兄弟节点横向排序
	public void sortChildren() {
		if (children != null && children.getSize() != 0) {
			children.sortChildren();
		}
	}
	
	// 先序遍历,构造功能叶子列表
	public void initializeLeafList(List leafList) {
		if (children == null || children.getSize() == 0) {
			leafList.add(this);
		} else {
			children.initializeLeafList(leafList);
		}
	}
	
	// 先序遍历,设置该节点下的所有功能路径为不可见
	public void setTreeNotVisible() {
		visible = false;
		if (children != null && children.getSize() != 0) {
			children.setTreeNotVisible();
		}
	}
	
	// 先序遍历,设置该节点下的所有功能路径为可见
	public void setTreeVisible() {
		visible = true;
		if (children != null && children.getSize() != 0) {
			children.setTreeVisible();
		}
	}
	
	// 设置包含该叶子节点的功能路径可见
	public void setRouteVisible() {
		visible = true;
		for (Node parentNode = this.parentNode; parentNode != null; parentNode = parentNode.parentNode) {
			parentNode.visible = true;
		}
	}
	
	// 对包含该叶子节点的功能路径权值加1
	public void increaseRouteWeight() {
		weight++;
		updateNodeWeightToDB(this);
		for (Node parentNode = this.parentNode; parentNode != null; parentNode = parentNode.parentNode) {
			parentNode.weight++;
			updateNodeWeightToDB(parentNode);
		}
	}
	
	// 更新节点权值到数据库
	public void updateNodeWeightToDB(Node node) {
		// 暂时不实现,实际应用中需要实现该方法
		// 或者用户退出系统时,遍历整棵树,统一更新所有节点的权值到数据库中,应该这样做比较好,一次性统一处理
	}
	
	// 先序遍历,搜索菜单节点,同时进行功能路径过滤
	public void searchTreeNode(String keyWord) {
		if (this.text.indexOf(keyWord) > -1) {
			this.setTreeVisible();
			this.setRouteVisible();
		} else {
			if (children != null && children.getSize() != 0) {
				children.searchTreeNode(keyWord);
			}
		}
	}
}

/**
* 孩子列表类
*/
class Children {
	private List list = new ArrayList();
	
	public int getSize() {
		return list.size();
	}
	
	public void addChild(Node node) {
		list.add(node);
	}
	
	// 拼接孩子节点的JSON字符串
	public String toString() {
		String result = "[";		
		for (Iterator it = list.iterator(); it.hasNext();) {
			Node node = (Node) it.next();
			if (node.visible) {
				result += node.toString();
				result += ",";
			}
		}
		result = result.substring(0, result.length() - 1);
		result += "]";
		return result;
	}
	
	// 在孩子节点中寻找功能叶子节点
	public void initializeLeafList(List leafList) {
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).initializeLeafList(leafList);
		}
	}
	
	// 孩子节点排序
	public void sortChildren() {
		// 对本层节点进行排序
		// 可根据不同的排序属性,传入不同的比较器,这里传入优先级比较器
		Collections.sort(list, new NodePriorityComparator());
		// 对每个节点的下一层节点进行排序
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).sortChildren();
		}
	}
	
	// 设置孩子节点为不可见 
	public void setTreeNotVisible() {
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).setTreeNotVisible();
		}
	}
	
	// 设置孩子节点为可见 
	public void setTreeVisible() {
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).setTreeVisible();
		}
	}
	
	// 搜索菜单节点,同时进行功能路径过滤
	public void searchTreeNode(String keyWord) {
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).searchTreeNode(keyWord);
		}
	}
}

/**
 * 节点比较器
 */
class NodePriorityComparator implements Comparator {
	// 按照 (节点权值+节点编号) 比较
	public int compare(Object o1, Object o2) {
		// 按权值由大到小排序
		int w1 = ((Node)o1).weight;
	    int w2 = ((Node)o2).weight;
	    if (w1 < w2) {
	    	return 1;
	    } else if (w1 > w2) {
	    	return -1;
	    } else { // 权值相等时,按照节点编号由小到大排序
	    	int i1 = Integer.parseInt(((Node)o1).id);
	    	int i2 = Integer.parseInt(((Node)o2).id);
	    	return i1 < i2 ? -1 : (i1 == i2 ? 0 : 1);
	    }
	}	
}

/**
 * 构造虚拟的层次数据
 */
class VirtualDataGenerator {
	// 构造无序的结果集列表,实际应用中,该数据应该从数据库中查询获得;
	public static List getVirtualResult() {				
		List dataList = new ArrayList();
		
		HashMap dataRecord1 = new HashMap();
		dataRecord1.put("id", "0");
		dataRecord1.put("text", "根");
		dataRecord1.put("parentId", "");
		dataRecord1.put("weight", "1");
		
		HashMap dataRecord2 = new HashMap();
		dataRecord2.put("id", "1");
		dataRecord2.put("text", "1");
		dataRecord2.put("parentId", "0");
		dataRecord2.put("weight", "1");
		
		HashMap dataRecord3 = new HashMap();
		dataRecord3.put("id", "2");
		dataRecord3.put("text", "2");
		dataRecord3.put("parentId", "0");
		dataRecord3.put("weight", "1");
						
		HashMap dataRecord4 = new HashMap();
		dataRecord4.put("id", "3");
		dataRecord4.put("text", "3");
		dataRecord4.put("parentId", "0");
		dataRecord4.put("weight", "1");
						
		HashMap dataRecord5 = new HashMap();
		dataRecord5.put("id", "4");
		dataRecord5.put("text", "4");
		dataRecord5.put("parentId", "0");
		dataRecord5.put("weight", "1");
		
		HashMap dataRecord6 = new HashMap();
		dataRecord6.put("id", "11");
		dataRecord6.put("text", "11");
		dataRecord6.put("parentId", "1");
		dataRecord6.put("weight", "1");
		
		HashMap dataRecord7 = new HashMap();
		dataRecord7.put("id", "12");
		dataRecord7.put("text", "12");
		dataRecord7.put("parentId", "1");
		dataRecord7.put("weight", "1");
		
		HashMap dataRecord8 = new HashMap();
		dataRecord8.put("id", "21");
		dataRecord8.put("text", "21");
		dataRecord8.put("parentId", "2");
		dataRecord8.put("weight", "1");
		
		HashMap dataRecord9 = new HashMap();
		dataRecord9.put("id", "22");
		dataRecord9.put("text", "22");
		dataRecord9.put("parentId", "2");
		dataRecord9.put("weight", "1");
		
		HashMap dataRecord10 = new HashMap();
		dataRecord10.put("id", "31");
		dataRecord10.put("text", "31");
		dataRecord10.put("parentId", "3");
		dataRecord10.put("weight", "1");
		
		HashMap dataRecord11 = new HashMap();
		dataRecord11.put("id", "32");
		dataRecord11.put("text", "32");
		dataRecord11.put("parentId", "3");
		dataRecord11.put("weight", "1");
		
		HashMap dataRecord12 = new HashMap();
		dataRecord12.put("id", "41");
		dataRecord12.put("text", "41");
		dataRecord12.put("parentId", "4");
		dataRecord12.put("weight", "1");
		
		HashMap dataRecord13 = new HashMap();
		dataRecord13.put("id", "42");
		dataRecord13.put("text", "42");
		dataRecord13.put("parentId", "4");
		dataRecord13.put("weight", "1");
		
		HashMap dataRecord14 = new HashMap();
		dataRecord14.put("id", "43");
		dataRecord14.put("text", "43");
		dataRecord14.put("parentId", "4");
		dataRecord14.put("weight", "1");
				
		HashMap dataRecord15 = new HashMap();
		dataRecord15.put("id", "121");
		dataRecord15.put("text", "121");
		dataRecord15.put("parentId", "12");
		dataRecord15.put("weight", "1");
		
		HashMap dataRecord16 = new HashMap();
		dataRecord16.put("id", "122");
		dataRecord16.put("text", "122");
		dataRecord16.put("parentId", "12");
		dataRecord16.put("weight", "1");
		
		HashMap dataRecord17 = new HashMap();
		dataRecord17.put("id", "123");
		dataRecord17.put("text", "123");
		dataRecord17.put("parentId", "12");
		dataRecord17.put("weight", "1");
				
		HashMap dataRecord18 = new HashMap();
		dataRecord18.put("id", "321");
		dataRecord18.put("text", "321");
		dataRecord18.put("parentId", "32");
		dataRecord18.put("weight", "1");
		
		HashMap dataRecord19 = new HashMap();
		dataRecord19.put("id", "322");
		dataRecord19.put("text", "322");
		dataRecord19.put("parentId", "32");
		dataRecord19.put("weight", "1");
		
		HashMap dataRecord20 = new HashMap();
		dataRecord20.put("id", "323");
		dataRecord20.put("text", "323");
		dataRecord20.put("parentId", "32");
		dataRecord20.put("weight", "1");
				
		dataList.add(dataRecord1);
		dataList.add(dataRecord2);
		dataList.add(dataRecord3);
		dataList.add(dataRecord4);
		dataList.add(dataRecord5);
		dataList.add(dataRecord6);
		dataList.add(dataRecord7);		
		dataList.add(dataRecord8);
		dataList.add(dataRecord9);
		dataList.add(dataRecord10);
		dataList.add(dataRecord11);
		dataList.add(dataRecord12);
		dataList.add(dataRecord13);
		dataList.add(dataRecord14);		
		dataList.add(dataRecord15);
		dataList.add(dataRecord16);
		dataList.add(dataRecord17);
		dataList.add(dataRecord18);
		dataList.add(dataRecord19);
		dataList.add(dataRecord20);
		
		return dataList;
	}	
}

  

四、 联系方式
memorymultitree@163.com

 

 

   发表时间:2012-02-10  
性能怎么样呢?
0 请登录后投票
   发表时间:2012-02-11  
楼主的示意图用什么工具画的
0 请登录后投票
   发表时间:2012-02-11  
code_k 写道
楼主的示意图用什么工具画的

 

 

系统界面用axure RP画的,数据结构图用visio画的

0 请登录后投票
   发表时间:2012-02-15  
memorymultitree 写道

(原创)在基于Ext JS的应用系统中实现智能树形菜单--利用加权双历树实现(初稿,版本1.0)


一、 智能树形菜单的概念
在描述智能树形菜单的概念之前,请先阅读我的上一篇文章《利用双历树实现Ext JS中的无限级树形菜单(一种构建多级有序树形结构JSON的方法》,请点击链接查看:http://www.iteye.com/topic/1119961,阅读完上一篇文章之后,再继续向下阅读,因为这两篇文章是紧密联系、前后关联的,谢谢。

 

大家都知道树形菜单,不管在基于什么样的技术开发的系统上,树形菜单都是用户界面中非常常用的一种菜单形式,在基于Ext JS的应用系统中,树形控件更是重要的UI控件之一,借助Ext JS框架的技术优势:界面交互性好(组件功能丰富、事件驱动);系统响应速度快(JSON格式传输,数据量小、速度快),使得实现一种比普通树形菜单更加智能的树形菜单成为可能。那么智能树形菜单和普通的树形菜单到底有什么区别呢,它到底智能在哪呢,让我们先来看一组用例场景:

 

网民王小利是一名网上银行的忠实用户,他经常利用网银办理各种银行业务,假设王小利在
某某银行开通了网上银行功能(假设该银行的网银系统是基于Ext JS开发),他回到家之后,想试验一下这个新的网银系统,于是他首次登录了某某银行的网上银行,登录成功后,系统展示界面如下所示(简略概要图):
图1.1

 

他想给自己的朋友李小磊转一笔钱,于是他在界面左上角的“输入交易功能关键字”输入框中输入“转账”二字,点击“搜索”按钮,查询出了如下的结果,如图:
图1.2


 

他看到左侧的树形菜单区域中搜索出了三个交易功能,分别是行内转账、跨行转账和跨行快速转账,于是他点击跨行转账后,主界面进入跨行转账信息录入界面,王小利很快完成了转账操作,如下图:
图1.3
 

 

王小利一看,这个银行的网上银行系统操作这么方便,于是他又陆续使用了以下几个功能,分别是“投资理财->定活互转”、“投资理财->理财产品->产品购买”、“我的贷款->贷款信息查询”、“客户服务->昵称设置”,本次登录他一共使用了五个功能点,其中“转账汇款->跨行转账”功能使用了两次,一次是给朋友李小磊转钱,另一次是给朋友王小然转钱,其他四个功能各使用了一次。


第二天,王小利又登录了网上银行系统,登录成功后,他看到系统的操作界面和第一次登录时不太一样了,变成了如下样子:
图1.4


 

他看到昨天使用的五个功能点菜单全部自动展开了,而且菜单顺序也发生了变化:原来排在最后的一级菜单“客户服务”居然移到了“我的贷款”下面,而原来排在第一位的“账户管理”却移到了“客户服务”下面,由于部分菜单自动展开,所以菜单栏右侧出现了滚动条。
不光一级菜单的顺序发生了变化,二级菜单的顺序也发生了变化,原来在“转账汇款”下面的子菜单中,“跨行转账”排在“行内转账”下面,现在却排在了第一位。不光菜单发生了变化,中间主界面也发生了变化,原来是欢迎界面,现在变成了“跨行转账”信息录入界面。
王小利一看,正合我意,他正想给朋友吴艳清转一笔钱,吴艳清的开户行是工商银行,不是本行,正好需要使用跨行转账功能,于是他很快完成了转账操作。他今天还想通过“定活互转”功能,存一笔定期,由于昨天使用了一次该功能,所以这个功能也默认自动展开,他很容易就看到了这个菜单项,顺利完成了存定期的操作,也就是说功能使用率大于平均功能使用率的功能节点自动展开了(假设有10个功能,功能A和功能B分别被使用了3次和2次,其它功能都被使用了1次,那么平均使用率是(3+2+8)/ 10 = 1.3次,A和B的使用率大于平均使用率,自动展开,其他8个功能的使用率小于1.3,不展开),真智能啊!王小利心想,看来以后得多多使用网上银行了,太方便了。


好了,读者朋友们,通过上面这组用例场景,相信您已经看出了本文要论述的主题内容,对了,这就是智能树形菜单,现在我们给智能树形菜单下一个完整科学的定义,


智能树形菜单满足如下几个条件:


A、 用户首次登录系统时,功能菜单树全部闭合,并且按照菜单编号排序,且每一层菜单节点都按照这个规则排序,保持整棵功能树兄弟节点横向有序(关于“兄弟节点横向有序”的概念请查看:我的上一篇文章《利用双历树实现Ext JS中的无限级树形菜单(一种构建多级有序树形结构JSON的方法)》);

 

B、 系统会自动记录每一个功能路径(功能路径指的是由一级菜单节点到功能叶子节点的一条完整路径,例如“投资理财->理财产品->产品购买”就是一条功能路径)的使用率;

 

C、系统提供交易功能搜索功能,用户可以输入交易功能关键字,系统会搜索出包含关键字的功能叶子节点,并展示出搜索结果的完整功能路径;

 

D、用户再次登录系统时,功能菜单重新排序,并且部分展开,其规则如下:
(1)整棵功能菜单树按照使用率加节点编号的方式排序,先按使用率排序,使用率相同的情况下按照节点编号排序,保持兄弟节点横向有序;
(2)热点功能自动展开(热点功能指的是功能使用率大于所有功能平均使用率的功能叶子节点,例如在上面的用例场景中,王小利第一次使用网银时,“跨行转账”被使用了两次,“定活互转”、“产品购买”、“贷款信息查询”、“昵称设置”都被使用了一次,其他功能被使用了零次,假设一共有30个功能,那么功能平均使用率是(2+1+1+1+1)/ 30 = 0.2, 这五个功能的使用率均大于0.2,所以都属于热点功能)

 

E、系统主界面默认进入焦点功能(焦点功能指的是热点功能中使用率最高的功能)的操作界面;

 

 

好了,智能树形菜单的概念就介绍完了,下面讲述详细实现方案。

 

 

二、 详细实现方案
在《利用双历树实现Ext JS中的无限级树形菜单(一种构建多级有序树形结构JSON的方法)》一文中,谈到了双历树,智能树形菜单就可以在双历树的基础上进行扩展,实现概念定义中所描述的所有功能,下面介绍详细设计方案。

 

下面这张图就是扩展之后的双历树,并且根据这张图定义了一系列名词概念,如图所示:
图1.5

 

我们先根据这张图定义一组名词概念:
1、 菜单树
所有菜单可以抽象成一棵菜单树,为了保持树的完整性,在一级菜单上面加了一个虚拟的根节点,每个节点由两个值组成,用逗号分割,前一个值是菜单节点编号,后一个值是节点权值,默认权值都为1。

 

2、节点权值
为了描述某一个节点在其兄弟节点之中的优先级(也就是功能使用率),用一个数值来表示,就叫节点权值。

 

3、功能叶子节点
菜单树中的叶子节点,下面不再下挂其他子功能节点。

 

4、功能路径
由根节点到功能叶子节点的一条完整路径,例如图中的蓝色线条部分,即 “根->3->32->323”就是一条功能路径,上图的菜单树中共有13条功能路径,您可以数一数,呵呵。

 

5、功能路径权值
功能路径权值由功能路径中所有节点的权值由父到子拼接而成,例如路径“根->3->32->323”的路径权值是“111”。

 

6、 优先功能路径
指的是功能路径权值最高的那一条路径,如上图菜单树中的红色线条部分,该路径为“根->1->11”,它的路径权值是“32”。路径权值比较的规则是逐级比较,“32”的第一级是3,“111”的第一级是1,所以“32”大于“111”,路径权值相同时比较节点编号,也就是用权值加节点编号进行比较,例如路径“根->1->11”和路径“根->1->12->121”,它们的路径权值分别是“32”和“322”,当比较到第二级时,节点权值都是2,但是由于节点11比节点12的编号小,所以节点11排列在节点12前面,也就是说功能路径“根->1->11”的优先级排在“根->1->12->121”之前。

 

7、功能叶子列表
当构造好一棵菜单树后,通过深度优先先序遍历,找到所有叶子节点,并将其存放在一个列表里面,这个列表叫做功能叶子列表。

 

8、功能叶子权值
功能叶子节点的权值叫做功能叶子权值。

 

9、热点功能叶子
功能叶子权值大于所有功能叶子节点的平均权值的叶子节点,例如上图中功能叶子列表中的前两个节点:11和121,其权值分别是2和2,而叶子节点的平均权值是1.15。

 

10、焦点功能叶子
热点功能叶子中权值最高的叶子叫做焦点功能叶子,如果权值相等,则比较节点编号,节点编号优先的叶子为焦点功能叶子。

 

11、功能路径可见
在菜单节点中增加一个“是否可见”属性,如果一条功能路径中所有节点的“是否可见”属性为true,那么称该条路径可见,否则该条路径不可见,所谓不可见,指的是不向用户展示这条功能路径。

 

12、功能叶子搜索
指的是在功能叶子列表中按照节点关键字查找包含该关键字的功能叶子节点。

 

13、功能路径筛选
指的是对整棵树的功能路径的可见性进行重新设置,首先将所有节点设置为不可见,然后根据功能叶子搜索的结果,将包含该功能叶子的功能路径设置为可见,也就是向用户展示功能叶子搜索结果的完整路径。

 

14、增加功能路径权值
指的是将包含某一功能叶子节点的一条完整功能路径上的所有节点的权值增加相同的幅度(例如将某一功能路径上的所有节点权值加1)。

 

 

我们可以把这棵加了节点权值的双历树叫做加权双历树。

 


好了,定义完名词概念之后,用下面一组图来展示加权双历树的变化过程。

 


图一,用户首次登录系统时,在服务器端构造出加权双历树和功能叶子列表:
图1.6
 

 

其中黄色圆圈代表功能叶子节点。

 

构造出这棵树之后,向客户端的Ext JS树形控件返回树形结构的JSON字符串,返回如下内容:
{
id : '0',
text : '根',
children : [
 {
 id : '1',
 text : '1',
 children : [
  {
  id : '11',
  text : '11',
  leaf : true
  },
  {
  id : '12',
  text : '12',
  children : [
     {
     id : '121',
     text : '121',
     leaf : true
     },
     {
     id : '122',
     text : '122',
     leaf : true
     },
     {
     id : '123',
     text : '123',
     leaf : true
     }
  ]
  }
 ]
 },
  {
  id : '2',
  text : '2',
  children : [
    {
    id : '21',
    text : '21',
    leaf : true
    },
    {
    id : '22',
    text : '22',
    leaf : true
    }
  ]
  },
 {
 id : '3',
 text : '3',
 children : [
  {
  id : '31',
  text : '31',
  leaf : true
  },
  {
  id : '32',
  text : '32',
  children : [
    {
    id : '321',
    text : '321',
    leaf : true
    },
    {
    id : '322',
    text : '322',
    leaf : true
    },
    {
    id : '323',
    text : '323',
    leaf : true
    }
  ]
  }
 ]
 },
 {
 id : '4',
 text : '4',
 children : [
   {
   id : '41',
   text : '41',
   leaf : true
   },
   {
   id : '42',
   text : '42',
   leaf : true
   },
   {
   id : '43',
   text : '43',
   leaf : true
   }
 ]
 }
]
}


图二,用户使用关键字“12”在功能叶子列表中进行搜索,并将包含搜索结果的功能路径设置为可见,其他路径设置为不可见,图中红色线条部分为可见路径,其他的为不可见路径,如图所示:
图1.7
 

 

进行功能路径筛选后,向客户端返回如下JSON字符串:
{
id : '0',
text : '根',
children : [
  {
  id : '1',
  text : '1',
  children : [
    {
    id : '12',
    text : '12',
    children : [
      {
      id : '121',
      text : '121',
      leaf : true
      },
      {
      id : '122',
      text : '122',
      leaf : true
      },
      {
      id : '123',
      text : '123',
      leaf : true
      }
    ]
    }
    ]
    }
]
}

图三,用户再使用关键字“321”在功能叶子列表中进行搜索,搜索结果如图所示:
图1.8
 

 

进行功能路径筛选后,向客户端返回如下JSON字符串:
{
id : '0',
text : '根',
children : [
  {
  id : '3',
  text : '3',
  children : [
   {
    id : '32',
    text : '32',
    children : [
      {
      id : '321',
      text : '321',
      leaf : true
      }
    ]
      }
  ]
  }
]
}


图四,用户使用某个功能后,系统将该功能路径权值加1,并将该功能路径上的所有节点的权值更新到数据库中的菜单节点定义表中,如图所示,路径“根->3->32->323”的权值加1,由“111”变成了“222”:
图1.9
 

 

图五,当用户退出,再次登录系统后,在服务器端再次构造出加权双历树和功能叶子列表,此时数据结构变成如下样子:
图2.0
 

 

树中红色线条部分为优先功能路径,功能叶子列表中红色圆圈为焦点功能叶子。


图六,用户再次使用某个功能后,系统将该功能路径权值加1,并将该功能路径上的所有节点的权值更新到数据库中的菜单节点定义表中,如图所示,路径“根->1->12->122”的权值加1,由“111”变成了“222”:
图2.1
 

 

图七,当用户退出,再次登录系统后,在服务器端再次构造出加权双历树和功能叶子列表,此时数据结构变成如下样子:
图2.2
 

 

树中红色线条部分根据路径权值和节点编号重新进行了兄弟节点横向排序;
功能叶子列表中红色圆圈为热点叶子,现在有两个热点叶子了,其中122为焦点叶子。

 

现在服务器向客户端返回的JSON字符串变成了如下:
{
id : '0',
text : '根',
children : [
  {
  id : '1',
  text : '1',
  children : [
    {
    id : '12',
    text : '12',
    children : [
      {
      id : '122',
      text : '122',
      leaf : true
      },
      {
      id : '121',
      text : '121',
      leaf : true
      },
      {
      id : '123',
      text : '123',
      leaf : true
      }
    ]
    },
    {
    id : '11',
    text : '11',
    leaf : true
    }
  ]
  },
  {
  id : '3',
  text : '3',
  children : [
    {
    id : '32',
    text : '32',
    children : [
      {
      id : '323',
      text : '323',
      leaf : true
      },
      {
      id : '321',
      text : '321',
      leaf : true
      },
      {
      id : '322',
      text : '322',
      leaf : true
      }
    ]
    },
    {
    id : '31',
    text : '31',
    leaf : true
    }
  ]
  },
  {
  id : '2',
  text : '2',
  children : [
    {
    id : '21',
    text : '21',
    leaf : true
    },
    {
    id : '22',
    text : '22',
    leaf : true
    }
  ]
  },
  {
  id : '4',
  text : '4',
  children : [
    {
    id : '41',
    text : '43211',
    leaf : true
    },
    {
    id : '42',
    text : '42',
    leaf : true
    },
    {
    id : '43',
    text : '43',
    leaf : true
    }
  ]
  }
]
}

 


对于热点功能叶子自动展开,和系统默认进入焦点功能界面这两个功能,只需要客户端通过AJAX的方式从服务器端的功能叶子列表中取出热点功能叶子和焦点功能叶子的节点编号后,在客户端通过编程处理,具体方法不在此叙述,关于如何展开叶子节点请参考Ext JS的相关书籍。

 

三、 源代码实现(服务器端JAVA代码演示)

以下代码拷贝出来后可以直接运行测试:

 

package test;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Collections;

/**
 * 树类
*/
public class ExtTree {
	public static void main(String[] args) {
		// 读取层次数据结果集列表 
		List dataList = VirtualDataGenerator.getVirtualResult();		
		
		/*****************************演示构造加权双历树和功能叶子列表*********************************/
		// 节点列表(哈希表,用于临时存储节点对象)
		HashMap nodeList = new HashMap();
		// 根节点
		Node root = null;
		// 根据结果集构造节点列表(存入哈希表)
		for (Iterator it = dataList.iterator(); it.hasNext();) {
			Map dataRecord = (Map) it.next();
			Node node = new Node();
			node.id = (String) dataRecord.get("id");
			node.text = (String) dataRecord.get("text");
			node.parentId = (String) dataRecord.get("parentId");
			node.weight = Integer.parseInt((String) dataRecord.get("weight"));
			nodeList.put(node.id, node);
		}
		// 构造无序的多叉树
		Set entrySet = nodeList.entrySet();
		for (Iterator it = entrySet.iterator(); it.hasNext();) {
			Node node = (Node) ((Map.Entry) it.next()).getValue();
			if (node.parentId == null || node.parentId.equals("")) {
				root = node;
			} else {
				((Node) nodeList.get(node.parentId)).children.addChild(node);
				// 在节点中增加一个父节点的引用
				node.parentNode = (Node) nodeList.get(node.parentId);
			}
		}		
		// 功能叶子列表
		List leafList = new ArrayList();
		// 构造功能叶子列表
		root.initializeLeafList(leafList);
		
		// 对多叉树进行横向排序
		root.sortChildren();		
		// 输出有序的树形菜单的JSON字符串
		System.out.println("首次登录时的树形菜单:\n" + root.toString());	
		
		/*****************************演示功能叶子搜索*********************************/	
		// 首先设置整棵树的功能路径为不可见
		root.setTreeNotVisible();
		// 在功能叶子列表中搜索节点内容中包含关键字“321”的节点,并将包含该节点的功能路径设置为可见		
		for (Iterator it = leafList.iterator(); it.hasNext();) {
			Node leafNode = (Node) it.next();
			if (leafNode.text.indexOf("321") > -1) {
				leafNode.setRouteVisible();
			}
		}						
		// 输出搜索结果的功能路径的JSON字符串
		System.out.println("搜索后的树形菜单:\n" + root.toString());

		/*****************************演示增加功能路径权值*********************************/
		// 首先设置整棵树的功能路径为可见
		root.setTreeVisible();		
		// 对包含功能叶子节点122和323的路径权值加1
		for (Iterator it = leafList.iterator(); it.hasNext();) {
			Node leafNode = (Node) it.next();
			if (leafNode.id.equals("122") || leafNode.id.equals("323")) {
				leafNode.increaseRouteWeight();
			}
		}		
		// 对多叉树重新进行横向排序
		root.sortChildren();		
		// 输出重新排序后的树形菜单的JSON字符串
		System.out.println("路径权值变化后再次登录时的树形菜单:\n" + root.toString());	
		
		// 程序输出结果如下:
		// 首次登录时的树形菜单:
		// {id : '0', text : '根', children : [{id : '1', text : '1', children : [{id : '11', text : '11', leaf : true},{id : '12', text : '12', children : [{id : '121', text : '121', leaf : true},{id : '122', text : '122', leaf : true},{id : '123', text : '123', leaf : true}]}]},{id : '2', text : '2', children : [{id : '21', text : '21', leaf : true},{id : '22', text : '22', leaf : true}]},{id : '3', text : '3', children : [{id : '31', text : '31', leaf : true},{id : '32', text : '32', children : [{id : '321', text : '321', leaf : true},{id : '322', text : '322', leaf : true},{id : '323', text : '323', leaf : true}]}]},{id : '4', text : '4', children : [{id : '41', text : '41', leaf : true},{id : '42', text : '42', leaf : true},{id : '43', text : '43', leaf : true}]}]}
		// 搜索后的树形菜单:
		// {id : '0', text : '根', children : [{id : '3', text : '3', children : [{id : '32', text : '32', children : [{id : '321', text : '321', leaf : true}]}]}]}
		// 路径权值变化后再次登录时的树形菜单:
		// {id : '0', text : '根', children : [{id : '1', text : '1', children : [{id : '12', text : '12', children : [{id : '122', text : '122', leaf : true},{id : '121', text : '121', leaf : true},{id : '123', text : '123', leaf : true}]},{id : '11', text : '11', leaf : true}]},{id : '3', text : '3', children : [{id : '32', text : '32', children : [{id : '323', text : '323', leaf : true},{id : '321', text : '321', leaf : true},{id : '322', text : '322', leaf : true}]},{id : '31', text : '31', leaf : true}]},{id : '2', text : '2', children : [{id : '21', text : '21', leaf : true},{id : '22', text : '22', leaf : true}]},{id : '4', text : '4', children : [{id : '41', text : '41', leaf : true},{id : '42', text : '42', leaf : true},{id : '43', text : '43', leaf : true}]}]}
	}			
}


/**
* 节点类
*/
class Node {
	/**
	 * 节点编号
	 */
	public String id;
	/**
	 * 节点内容
	 */
	public String text;
	/**
	 * 父节点编号
	 */
	public String parentId;
	/**
	 * 节点权值
	 */
	public int weight;
	/**
	 * 是否可见,默认为true
	 */
	public boolean visible = true;
	/**
	 * 父节点引用
	 */
	public Node parentNode;
	/**
	 * 孩子节点列表
	 */
	public Children children = new Children();
	
	// 深度优先先序遍历,拼接JSON字符串
	public String toString() {
		if (visible) {
			String result = "{"
				+ "id : '" + id + "'"
				+ ", text : '" + text + "'";
			
			if (children != null && children.getSize() != 0) {
				result += ", children : " + children.toString();
			} else {
				result += ", leaf : true";
			}
					
			return result + "}";
		} else {
			return "";
		}
	}
	
	// 广度优先遍历,对子节点进行横向排序
	public void sortChildren() {
		if (children != null && children.getSize() != 0) {
			children.sortChildren();
		}
	}
	
	// 深度优先先序遍历,构造功能叶子列表
	public void initializeLeafList(List leafList) {
		if (children == null || children.getSize() == 0) {
			leafList.add(this);
		} else {
			children.initializeLeafList(leafList);
		}
	}
	
	// 深度优先先序遍历,设置该节点下的所有功能路径为不可见
	public void setTreeNotVisible() {
		visible = false;
		if (children != null && children.getSize() != 0) {
			children.setTreeNotVisible();
		}
	}
	
	// 深度优先先序遍历,设置该节点下的所有功能路径为可见
	public void setTreeVisible() {
		visible = true;
		if (children != null && children.getSize() != 0) {
			children.setTreeVisible();
		}
	}
	
	// 设置包含该叶子节点的功能路径可见
	public void setRouteVisible() {
		visible = true;
		for (Node parentNode = this.parentNode; parentNode != null; parentNode = parentNode.parentNode) {
			parentNode.visible = true;
		}
	}
	
	// 对包含该叶子节点的功能路径权值加1
	public void increaseRouteWeight() {
		weight++;
		updateNodeWeightToDB(this);
		for (Node parentNode = this.parentNode; parentNode != null; parentNode = parentNode.parentNode) {
			parentNode.weight++;
			updateNodeWeightToDB(parentNode);
		}
	}
	
	// 更新节点权值到数据库
	public void updateNodeWeightToDB(Node node) {
		// 暂时不实现,实际应用中需要实现该方法
		// 或者用户退出系统时,遍历整棵树,统一更新所有节点的权值到数据库中,应该这样做比较好,一次性统一处理
	}
}

/**
* 孩子列表类
*/
class Children {
	public List list = new ArrayList();
	
	public int getSize() {
		return list.size();
	}
	
	public void addChild(Node node) {
		list.add(node);
	}
	
	// 拼接孩子节点的JSON字符串
	public String toString() {
		String result = "[";		
		for (Iterator it = list.iterator(); it.hasNext();) {
			Node node = (Node) it.next();
			if (node.visible) {
				result += node.toString();
				result += ",";
			}
		}
		result = result.substring(0, result.length() - 1);
		result += "]";
		return result;
	}
	
	// 在孩子节点中寻找功能叶子节点
	public void initializeLeafList(List leafList) {
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).initializeLeafList(leafList);
		}
	}
	
	// 孩子节点排序
	public void sortChildren() {
		// 对本层节点进行排序
		// 可根据不同的排序属性,传入不同的比较器,这里传入优先级比较器
		Collections.sort(list, new NodePriorityComparator());
		// 对每个节点的下一层节点进行排序
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).sortChildren();
		}
	}
	
	// 设置孩子节点为不可见 
	public void setTreeNotVisible() {
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).setTreeNotVisible();
		}
	}
	
	// 设置孩子节点为可见 
	public void setTreeVisible() {
		for (Iterator it = list.iterator(); it.hasNext();) {
			((Node) it.next()).setTreeVisible();
		}
	}
}

/**
 * 节点比较器
 */
class NodePriorityComparator implements Comparator {
	// 按照 (节点权值+节点编号) 比较
	public int compare(Object o1, Object o2) {
		// 按权值由大到小排序
		int w1 = ((Node)o1).weight;
	    int w2 = ((Node)o2).weight;
	    if (w1 < w2) {
	    	return 1;
	    } else if (w1 > w2) {
	    	return -1;
	    } else { // 权值相等时,按照节点编号由小到大排序
	    	int i1 = Integer.parseInt(((Node)o1).id);
	    	int i2 = Integer.parseInt(((Node)o2).id);
	    	return i1 < i2 ? -1 : (i1 == i2 ? 0 : 1);
	    }
	}	
}

/**
 * 构造虚拟的层次数据
 */
class VirtualDataGenerator {
	// 构造无序的结果集列表,实际应用中,该数据应该从数据库中查询获得;
	public static List getVirtualResult() {				
		List dataList = new ArrayList();
		
		HashMap dataRecord1 = new HashMap();
		dataRecord1.put("id", "0");
		dataRecord1.put("text", "根");
		dataRecord1.put("parentId", "");
		dataRecord1.put("weight", "1");
		
		HashMap dataRecord2 = new HashMap();
		dataRecord2.put("id", "1");
		dataRecord2.put("text", "1");
		dataRecord2.put("parentId", "0");
		dataRecord2.put("weight", "1");
		
		HashMap dataRecord3 = new HashMap();
		dataRecord3.put("id", "2");
		dataRecord3.put("text", "2");
		dataRecord3.put("parentId", "0");
		dataRecord3.put("weight", "1");
						
		HashMap dataRecord4 = new HashMap();
		dataRecord4.put("id", "3");
		dataRecord4.put("text", "3");
		dataRecord4.put("parentId", "0");
		dataRecord4.put("weight", "1");
						
		HashMap dataRecord5 = new HashMap();
		dataRecord5.put("id", "4");
		dataRecord5.put("text", "4");
		dataRecord5.put("parentId", "0");
		dataRecord5.put("weight", "1");
		
		HashMap dataRecord6 = new HashMap();
		dataRecord6.put("id", "11");
		dataRecord6.put("text", "11");
		dataRecord6.put("parentId", "1");
		dataRecord6.put("weight", "1");
		
		HashMap dataRecord7 = new HashMap();
		dataRecord7.put("id", "12");
		dataRecord7.put("text", "12");
		dataRecord7.put("parentId", "1");
		dataRecord7.put("weight", "1");
		
		HashMap dataRecord8 = new HashMap();
		dataRecord8.put("id", "21");
		dataRecord8.put("text", "21");
		dataRecord8.put("parentId", "2");
		dataRecord8.put("weight", "1");
		
		HashMap dataRecord9 = new HashMap();
		dataRecord9.put("id", "22");
		dataRecord9.put("text", "22");
		dataRecord9.put("parentId", "2");
		dataRecord9.put("weight", "1");
		
		HashMap dataRecord10 = new HashMap();
		dataRecord10.put("id", "31");
		dataRecord10.put("text", "31");
		dataRecord10.put("parentId", "3");
		dataRecord10.put("weight", "1");
		
		HashMap dataRecord11 = new HashMap();
		dataRecord11.put("id", "32");
		dataRecord11.put("text", "32");
		dataRecord11.put("parentId", "3");
		dataRecord11.put("weight", "1");
		
		HashMap dataRecord12 = new HashMap();
		dataRecord12.put("id", "41");
		dataRecord12.put("text", "41");
		dataRecord12.put("parentId", "4");
		dataRecord12.put("weight", "1");
		
		HashMap dataRecord13 = new HashMap();
		dataRecord13.put("id", "42");
		dataRecord13.put("text", "42");
		dataRecord13.put("parentId", "4");
		dataRecord13.put("weight", "1");
		
		HashMap dataRecord14 = new HashMap();
		dataRecord14.put("id", "43");
		dataRecord14.put("text", "43");
		dataRecord14.put("parentId", "4");
		dataRecord14.put("weight", "1");
				
		HashMap dataRecord15 = new HashMap();
		dataRecord15.put("id", "121");
		dataRecord15.put("text", "121");
		dataRecord15.put("parentId", "12");
		dataRecord15.put("weight", "1");
		
		HashMap dataRecord16 = new HashMap();
		dataRecord16.put("id", "122");
		dataRecord16.put("text", "122");
		dataRecord16.put("parentId", "12");
		dataRecord16.put("weight", "1");
		
		HashMap dataRecord17 = new HashMap();
		dataRecord17.put("id", "123");
		dataRecord17.put("text", "123");
		dataRecord17.put("parentId", "12");
		dataRecord17.put("weight", "1");
				
		HashMap dataRecord18 = new HashMap();
		dataRecord18.put("id", "321");
		dataRecord18.put("text", "321");
		dataRecord18.put("parentId", "32");
		dataRecord18.put("weight", "1");
		
		HashMap dataRecord19 = new HashMap();
		dataRecord19.put("id", "322");
		dataRecord19.put("text", "322");
		dataRecord19.put("parentId", "32");
		dataRecord19.put("weight", "1");
		
		HashMap dataRecord20 = new HashMap();
		dataRecord20.put("id", "323");
		dataRecord20.put("text", "323");
		dataRecord20.put("parentId", "32");
		dataRecord20.put("weight", "1");
				
		dataList.add(dataRecord1);
		dataList.add(dataRecord2);
		dataList.add(dataRecord3);
		dataList.add(dataRecord4);
		dataList.add(dataRecord5);
		dataList.add(dataRecord6);
		dataList.add(dataRecord7);		
		dataList.add(dataRecord8);
		dataList.add(dataRecord9);
		dataList.add(dataRecord10);
		dataList.add(dataRecord11);
		dataList.add(dataRecord12);
		dataList.add(dataRecord13);
		dataList.add(dataRecord14);		
		dataList.add(dataRecord15);
		dataList.add(dataRecord16);
		dataList.add(dataRecord17);
		dataList.add(dataRecord18);
		dataList.add(dataRecord19);
		dataList.add(dataRecord20);
		
		return dataList;
	}	
}

 

 

 

四、 联系方式
memorymultitree@163.com

 

 

0 请登录后投票
   发表时间:2012-02-15  

确实很智能,学习一下,希望以后做项目用得到!

0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics