在需要表示级联、层级的关系中,Tree作为最直观的表达方式常出现在组织架构、权限选择等层级关系中。典型的表现形试类似于:
一颗树的生成常常包括三个部分:1)数据库设计;2)后台程序;3)前端代码。那么,具体是怎么样的呢?
一、数据库设计
数据库设计对于树的表达常会包含这么几个类似意思的字段:
parent_id、id、name。
id:用于描述自己;
parent_id:用于描述自己的上一级;
name:用于描述自己的名称;
例如:总办(id=3,parent_id=0,name=总办),客户服务中心(id=10,parent_id=3,name=客户服务中心) ,客户部(id=12,parent_id=10,name=客户部)。由此建立了三级层级关系。
二、后台程序
对于一个层级,可能会用于描述部门关系,还可能用于描述菜单关系等等,不同的用途有不同的数据库设计字段。但为了程序的通用性,不可能为一了一个表或功能做单独的前端插件,因此就要在后台为前端插件需要使用到的字段做一个规范(或者在数据库设计中做规范)。在此为“树结构”在后台作这样的规范:
1 /// <summary> 2 3 /// 层级 4 5 /// </summary> 6 7 public class vmHierarchy 8 9 { 10 11 public int id { get; set; } 12 13 public int pid { get; set; } 14 15 public string name { get; set; } 16 17 public object sub { get; set; } 18 19 public int status { get; set; } 20 21 }
Pid:用于描述上级关系;
Sub:用于描述子级关系;
Status:用于描述自身状态或特殊标识;
以做部门的层级关系为例:分为两个部分:
1) 取数据:
/// <summary> /// 取部门层级 /// </summary> /// <returns></returns> public List<vmHierarchy> GetDepartmentRelation() { List<vmHierarchy> vmdrlist = new List<vmHierarchy>(); using (var ctx = DB.ContextForName(DBConnection.DefaultConnection).UseTransaction(true)) { List<au_Department> adlist = new List<au_Department>(); adlist = base.GetModelAll(); vmdrlist = GetDepartmentRelationSub(1, adlist); } return vmdrlist; }
2) 定层级:
/// <summary> /// 取部门层级-子级 /// </summary> /// <param name="parentid"></param> /// <param name="adlist"></param> /// <returns></returns> public List<vmHierarchy> GetDepartmentRelationSub(int parentid, List<au_Department> adlist) { List<vmHierarchy> vmdrlist = new List<vmHierarchy>(); List<au_Department> modellist = new List<au_Department>(); modellist = adlist.Where(s => s.parent_id == parentid).OrderBy(s => s.sequence).ToList<au_Department>(); foreach (au_Department item in modellist) { vmHierarchy vmmodel = new vmHierarchy(); vmmodel.id = item.id; vmmodel.pid = item.parent_id; vmmodel.name = item.name; vmmodel.sub = GetDepartmentRelationSub(item.id, adlist); vmdrlist.Add(vmmodel); } return vmdrlist; }
由此在前端可以得到类似这样的关系数据:
三、前端代码
在与前端代码时,关于树的逻辑关系理清是最为主要的。
1) 如何生成当前层级关系和期子级关系,每个节点的子节点都不同。
2) 需要复选框吗?
3) 需要折叠吗?
4) 当点击一个节点:
A:其下还有一串节点,要全部选中/全部不选中?
B:当前点击中其它子节点都被选中了,再选中这个节点,如何影响上级的选中与不选中?
总结为:在有复选框的情况下,如何影响它的下级和上级节点关系?
5) 三个事件:
A:单击选中复选框事件;
B:单击取消选中复选框事件;
C:单击行事件;
事件顺序?冒泡?必要事件与用户自定义事件?
有需求的童鞋可以看看下面的jQuery代码:
jQuery:
//Tree层级关系 Begin ; (function ($, window, document, undefined) { var defaults = { ajaxurl: '',//ajax取数据的url[data==null时有效] data: null,//数据 erow: null,//点击行时要执行的事件function(){} checbox: true,//是否有复选框 initunfold: true,//初始展开true 初始折叠false event: { selectedrows: null,//单击行时要执行的事件 checked: null,//选中了复选框时要执行的事件 unchecked: null//取消选中复选框时要执行的事件 },//事件 exchangebar: false,//是否有全部展开 全部折叠 按钮 onlyleafcheck: false//是否只有最终子节点才显示checkbox }; $.fn.etree = function (options) { var $that = $(this); var _ops = $.extend(true, {}, defaults, options); var $con = null, _activehtml; var _lv = 0; //初始化数据 function initdata() { if (_ops.data !== null) { generateTree(_ops.data); } else { $.ajax({ url: _ops.ajaxurl, dataType: "JSON", success: function (result) { _ops.data = result; generateTree(_ops.data); } }); } }; function generateTree(_data) { console.log(_data); $con = $('<div></div>').appendTo($that); var $ul = $('<ul class="e-tree-ul"></ul>').appendTo($con); generateSub($con, _data, _lv); initEvent(); if (_ops.initunfold == false) { $con.find('.tge-inv').each(function () { $(this).click(); }) } }; function generateSub($e, _data, _lv) { for (var i = 0; i < _data.length; i++) { var _tdata = _data[i]; var $li = $('<li class="e-tree-li"></li>').appendTo($e); var $p = $('<p class="e-tree-p" lv=' + _lv + '></p>').appendTo($li); var $ti = $('<i class="tge-inv"></i>').appendTo($p); var $tif = $('<i class="tge-invf"></i>').appendTo($p); var $tc = null; if (_ops.checbox == true) { if (_ops.onlyleafcheck == true && _tdata.sub.length == 0) { $tc = $('<i class="sck" tid="' + _tdata.id + '"></i>').appendTo($p); } else if (_ops.onlyleafcheck == false) { $tc = $('<i class="sck" tid="' + _tdata.id + '"></i>').appendTo($p); } } var $ts = $('<span class="e-tree-s"></span>').html(_tdata.name).appendTo($p); if (_tdata.sub.length > 0) { $tif.addClass('tge-invfr'); var $ul = $('<ul class="e-tree-ul"></ul>').appendTo($li); generateSub($ul, _tdata.sub, (_lv + 1)); $ti.addClass('tge-invd'); } else { $tif.addClass('tge-invfd'); } if ($tc != null) { if (_tdata.status == 1) { $tc.addClass('ck');//选中 } else { $tc.addClass('nock');//未选中 } } } }; function checksubordinate($e) { var $slv = $e.parent('p').next('ul'); if ($e.hasClass('ck')) { $slv.find('.sck').removeClass('nock').addClass('ck'); } else if ($e.hasClass('nock')) { $slv.find('.sck').removeClass('ck').addClass('nock'); } }; function checksuperior($e) { var $plv = $e.parent('p').parent('li').parent('ul'); if ($plv.length > 0) { var $sib = $plv.children('li'); var $sumckdcount = $sib.children('p').children('.ck').length; var $scount = $sib.length - $sumckdcount; var $ppsck = $plv.prev('p').children('.sck'); if ($scount == 0) { $plv.prev('p').children('.sck').removeClass('nock').addClass('ck'); } else { $plv.prev('p').children('.sck').removeClass('ck').addClass('nock'); } if ($ppsck.length > 0) { checksuperior($ppsck); } } }; function checkselect($e) { checksuperior($e); checksubordinate($e); }; function setAction($e) { var $ts = $e; var $te = $ts.parent('p'); var $thisid = parseInt($e.attr('tid')); $con.find('.e-tree-active').removeClass('e-tree-active'); if ($ts.hasClass('ck')) { $ts.removeClass('ck').addClass('nock'); checkselect($ts); if (typeof _ops.event.unchecked == "function") { _ops.event.unchecked($te, iselectedhtml());//活动项,唯一选中项|null } } else if ($ts.hasClass('nock')) { $ts.removeClass('nock').addClass('ck'); checkselect($ts); if (_ops.event.checked != null) { _ops.event.checked($te, iselectedhtml());//活动项,唯一选中项|null } } var $shtml = iselectedhtml(); if ($shtml != null) { var $ck = $shtml.children('.ck'); var $sid = parseInt($shtml.attr('tid')); $ck.parent('p').addClass('e-tree-active'); if ($thisid == $sid) { setAction($ck); } } }; function initEvent() { $con.find('.tge-inv').bind('click', function (e) { var $ts = $(this); var $next = $ts.parent('p').next(); var $tsnext = $ts.next(); if ($ts.hasClass('tge-invd')) { $ts.removeClass('tge-invd').addClass('tge-invr'); $next.slideUp(); if ($tsnext.hasClass('tge-invfr')) { $tsnext.removeClass('tge-invfr').addClass('tge-invfd'); } } else if ($ts.hasClass('tge-invr')) { $ts.removeClass('tge-invr').addClass('tge-invd'); $next.slideDown(); if ($tsnext.hasClass('tge-invfd')) { $tsnext.removeClass('tge-invfd').addClass('tge-invfr'); } } e.stopPropagation(); }); $con.find('.sck').bind('click', function (e) { var $ts = $(this); setAction($ts); e.stopPropagation(); }); $con.find('.e-tree-p').bind('click', function () { $(this).children('.sck').click(); }); if (typeof _ops.event.selectedrows == "function") { $con.find('.e-tree-p').bind('click', function () { var $te = $(this).context; _ops.event.selectedrows($($te), iselectedhtml());//活动项,唯一选中项|null }); } $con.find('.e-tree-s').bind('click', function () { return false; }); }; function iactivehtml() { _activehtml = $con.find('.e-tree-active').html(); return _activehtml; }; function iactiveid() { var $thtml = $con.find('.e-tree-active'); _activeid = parseInt($thtml.find('.sck').attr('tid')); return _activeid; }; function iselectedids() { var _ids = new Array(); $con.find('.ck').each(function () { _ids.push(parseInt($(this).attr('tid'))); }); return _ids; }; function iselectedhtml() { if (iselectedids().length == 1) { return $con.find('.ck').parent('p'); } else { return null; } }; function iselectedid() { if (iselectedids().length == 1) { return parseInt($con.find('.ck').attr('tid')); } else { return null; } }; initdata(); //活动项html [活动项:当前点击的项] this.activehtml = function () { return iactivehtml(); }; //活动项id [活动项:当前点击的项] this.activeid = function () { return iactiveid(); }; //获取所有选中的项id this.selectedids = function () { return iselectedids(); }; //当前唯一选中项的html 不满足唯一选中时,返回null this.selectedhtml = function () { return iselectedhtml(); }; //当前唯一选中项的id 不满足唯一选中时,返回null this.selectedid = function () { return iselectedid(); }; return this; }; })(jQuery, window, document); //Tree层级关系 End
使用:
@{ Layout = null; } @using UCMS_Commons; @using UCMS_Model; @using UCMS_Model.ViewModel; <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>DepatmentManage</title> <link href="~/Content/themes/black/Css/eui.css" rel="stylesheet" /> <script src="~/Scripts/jquery-1.8.2.min.js"></script> <script src="~/Scripts/extendjs/jquery.cookie.js"></script> <script src="~/Content/themes/black/Script/jquery.eui.js"></script> <style type="text/css"> #dp-content { 920px; margin: 0 auto; } #de-cont { 400px; border: 1px solid #E4E4E4; padding: 20px; display: inline-block; vertical-align: top; } #oper-cont { 400px; border: 1px solid #E4E4E4; padding: 20px; display: inline-block; vertical-align: top; margin-left: 28px; } fieldset { border: 1px solid #ddd; } legend { color: #9b9b9b; } </style> <script type="text/javascript"> $(function () { var actionUrl = { 'GetDpInfo': '/SystemCenter/GetDpInfo', 'adddp': '/SystemCenter/AddDp', 'deletedp': '/SystemCenter/DeleteDp', 'updatedp': '/SystemCenter/UpdateDp' }; var t = JSON.parse('@Html.Raw(JSONNet.Serialize(Model))'); var $opername = $('#oper-name'); var $opersub = $('#oper-cont-sub'); var $operinfoo = $('#oper-info-o'); var $operinfoname = $('#oper-info-name'); var $errormsg = $('#error-msg'); var $deletedp = $('#deletedp'); var $dpname = $('#dpname'); var _status = -1, _ttname = ''; $('input[name="opertype"]').bind('click', function () { var _index = parseInt($(this).attr('tp')); $operinfoo.show(); switch (_index) { case 0: { $opername.attr('readonly', 'readonly'); $opersub.show(); $opername.val(_ttname); $('#oper-info-o').show(); }; break; case 1: { $opername.removeAttr('readonly'); $opersub.hide(); $opername.focus().val(''); _status = 1; }; break; case 2: { $opername.removeAttr('readonly'); $opersub.hide(); $opername.focus().val(''); _status = 2; }; break; case 3: { $opername.removeAttr('readonly'); $opersub.hide(); $opername.focus(); _status = 3; }; break; } }); $('#savedp').bind('click', function () { switch (_status) { case 1: { adddp(1); }; break; case 2: { adddp(2); }; break; case 3: { updatedp(); }; break; } }); $('#deletedp').bind('click', function () { deletedp(); }); function adddp(_type) { var _id = _dptree.activeid(); var _dpname = $opername.val(); if (_dpname.length == 0) { $errormsg.html('请填写 名称'); } else { $.ajax({ url: actionUrl.adddp, data: { "thisid": _id, "addtype": _type == 1 ? 0 : 1, "addname": _dpname }, success: function (result) { var _result = $.eui.checkresult(result); if (_result) { window.location.reload(); } } }); } }; function deletedp() { var _ids = _dptree.selectedids(); var _idarray = JSON.stringify(_ids); $.ajax({ url: actionUrl.deletedp, data: { "ids": _idarray }, success: function (result) { var _result = $.eui.checkresult(result); if (_result) { window.location.reload(); } } }); }; function updatedp() { var _id = _dptree.activeid(); var _name = $opername.val(); if (_name.length == 0) { $errormsg.html('请填写 名称'); } else { $.ajax({ url: actionUrl.updatedp, data: { "thisid": _id, "newname": _name }, success: function (result) { var _result = $.eui.checkresult(result); if (_result) { window.location.reload(); } } }); } }; function getdpinfo(_id) { $.ajax({ url: actionUrl.GetDpInfo, data: { "id": _id }, success: function (result) { var $os = $('#oper-sub').html(''); for (var i = 0; i < result.length; i++) { $('<span style="padding: 0 10px;"></span>').html(result[i].name + '(' + result[i].percount + '人)').appendTo($os); } } }); }; function schecked(el, sl) { $('input[name="opertype"]').eq(0).click(); if (sl != null) { var _name = sl.find('.e-tree-s').html(); $dpname.html(_name); $opername.val(_name); _ttname = _name; getdpinfo(_dptree.selectedid()); $operinfoo.show(); } else { $dpname.html('已选中个数:' + _dptree.selectedids().length); $operinfoo.hide(); } }; var _dptree = $('#de-cont-d').etree({ data: t, checbox: true, onlyleafcheck: true, event: { selectedrows: function (el, sl) { }, checked: function (el, sl) { var _tname = el.find('.e-tree-s').html(); $deletedp.show(); $errormsg.html(''); schecked(el, sl); }, unchecked: function (el, sl) { $operinfoo.hide(); schecked(el, sl); } } }); }); </script> </head> <body> <div id="dp-content"> <fieldset id="de-cont"> <legend>部门</legend> <div id="de-cont-d"> </div> </fieldset> <fieldset id="oper-cont"> <legend>信息</legend> <div id="oper-info-name" style="line-height: 32px;"> 名称:<span id="dpname"></span> <a href="javascript:void(0);" class="eui-btns" style="float:right;display:none;" id="deletedp">删除?</a> </div> <div id="oper-info-o" style="display:none;"> <hr class="hrgrey" /> <p id="oper-cont-d" class="eui-p50"> 操作类别: <label><input name="opertype" type="radio" value="" tp="0" checked="checked" />查看信息</label> <label><input name="opertype" type="radio" value="" tp="1" />添加同级</label> <label><input name="opertype" type="radio" value="" tp="2" />添加子级</label> <label><input name="opertype" type="radio" value="" tp="3" />修改自身</label> </p> <p class="eui-p50"> 名称: <input type="text" name="name" value="" id="oper-name" class="eui-input" readonly="readonly" /> </p> <p class="eui-p50" id="oper-cont-sub"> 下辖职位:<span id="oper-sub"></span> </p> <label id="error-msg" class="error-msg"></label> <a href="javascript:void(0);" class="eui-btns" style="float:right;bottom:0;" id="savedp">保存</a> </div> </fieldset> </div> </body> </html>