精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2012-04-15
(原创)新概念智能树形菜单--利用加权多叉树结合JavaScript树形控件实现
大家都知道树形菜单,不管在基于什么样的技术开发的系统上,树形菜单都是用户界面中非常常用的一种菜单形式,在目前市场上常见的JavaScript框架及组件库中均包含自己的树形控件,例如JQuery、Dojo、Yahoo UI、Ext JS等,还有一些独立的树形控件,例如dhtmlxtree等,这些树形控件完美的解决了层次数据的展示问题。那么树形菜单发展到现在就算完美了吗?还有没有进一步的发展空间呢?还有没有进一步的完善空间呢?答案是肯定的。在这篇文章里,我提出一种新的想法对树形菜单进行扩展,也许会给您的项目带来一些新的用户体验,我管这种扩展型的树形菜单叫做“智能树形菜单”。那么智能树形菜单和普通的树形菜单到底有什么区别呢,它到底智能在哪呢,让我们先来看一组用例场景:
网民高小宝是一名网上银行的忠实用户,他经常利用网银办理各种银行业务,假设高小宝在
他想给自己的朋友李小磊转一笔钱,于是他在界面左上角的“输入交易功能关键字”输入框中输入“转账”二字,点击“搜索”按钮,查询出了如下的结果,如图:
他看到左侧的树形菜单区域中搜索出了三个交易功能,分别是行内转账、跨行转账和跨行快速转账,于是他点击跨行转账后,主界面进入跨行转账信息录入界面,高小宝很快完成了转账操作,如下图:
高小宝一看,这个银行的网上银行系统操作这么方便,于是他又陆续使用了以下几个功能,分别是“投资理财->定活互转”、“投资理财->理财产品->产品购买”、“我的贷款->贷款信息查询”、“客户服务->昵称设置”,本次登录他一共使用了五个功能点,其中“转账汇款->跨行转账”功能使用了两次,一次是给朋友李小磊转钱,另一次是给朋友王小然转钱,其他四个功能各使用了一次。
他看到昨天使用的五个功能点菜单全部自动展开了,而且菜单顺序也发生了变化:原来排在最后的一级菜单“客户服务”居然移到了“我的贷款”下面,而原来排在第一位的“账户管理”却移到了“客户服务”下面,由于部分菜单自动展开,所以菜单栏右侧出现了滚动条。
A、用户首次登录系统时,功能菜单树全部闭合,并且按照菜单编号排序,且每一层菜单节点都按照这个规则排序,保持整棵功能树兄弟节点横向有序(关于“兄弟节点横向有序”的概念请查看附录:我的上一篇文章《多叉树结合JavaScript树形控件实现无限级树形菜单(一种构建多级有序树形结构JSON(或XML)数据源的方法)》); B、系统会自动记录每一个功能路径(功能路径指的是由一级菜单节点到功能叶子节点的一条完整路径,例如“投资理财->理财产品->产品购买”就是一条功能路径)的使用率; C、系统提供菜单节点搜索功能,用户可以输入菜单节点关键字,系统会搜索出包含关键字的所有功能路径,并展示出搜索结果; D、用户再次登录系统时,功能菜单重新排序,并且部分展开,其规则如下: (1)整棵功能菜单树按照使用率加节点编号的方式排序,先按使用率排序,使用率相同的情况下按照节点编号排序,保持兄弟节点横向有序; (2)热点功能自动展开(热点功能指的是功能使用率大于所有功能平均使用率的功能叶子节点,例如在上面的用例场景中,高小宝第一次使用网银时,“跨行转账”被使用了两次,“定活互转”、“产品购买”、“贷款信息查询”、“昵称设置”都被使用了一次,其他功能被使用了零次,假设一共有30个功能,那么功能平均使用率是(2+1+1+1+1)/ 30 = 0.2, 这五个功能的使用率均大于0.2,所以都属于热点功能) E、系统主界面默认进入焦点功能(焦点功能指的是热点功能中使用率最高的功能)的操作界面;
好了,智能树形菜单的概念就介绍完了,下面讲述详细实现方案。
二、 详细实现方案
下面这张图就是扩展之后的多叉树,并且根据这张图定义了一系列名词概念,如图所示:
我们先根据这张图定义一组名词概念:
2、节点权值
3、功能叶子节点
4、功能路径
5、功能路径权值
6、 优先功能路径
7、功能叶子列表
8、功能叶子权值
9、热点功能叶子
10、焦点功能叶子
11、功能路径可见
12、菜单节点搜索
13、功能路径筛选
14、增加功能路径权值
我们可以把这棵加了节点权值的多叉树叫做加权多叉树,英文名称叫weighted multiple tree,简称W-M-Tree。
其中黄色圆圈代表功能叶子节点。
构造出这棵树之后,向客户端的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 } ] } ] }
进行功能路径筛选后,向客户端返回如下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”在功能叶子列表中进行搜索,搜索结果如图所示:
进行功能路径筛选后,向客户端返回如下JSON字符串: { id: '0', text: '根', children: [ { id: '3', text: '3', children: [ { id: '32', text: '32', children: [ { id: '321', text: '321', leaf: true } ] } ] } ] }
图五,当用户退出,再次登录系统后,在服务器端再次构造出加权多叉树和功能叶子列表,此时数据结构变成如下样子:
树中红色线条部分为优先功能路径,功能叶子列表中红色圆圈为焦点功能叶子。
图七,当用户退出,再次登录系统后,在服务器端再次构造出加权多叉树和功能叶子列表,此时数据结构变成如下样子:
树中红色线条部分根据路径权值和节点编号重新进行了兄弟节点横向排序;
现在服务器向客户端返回的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; } }
四、 联系方式
声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
浏览 1761 次