一、前言
组织机构是国内管理系统中很重要的一个概念,以前我们基本都是采用数据列表的形式展现,最多只是采用树形列表展现。虽然够用,但是如果能做成图形化当然是最好不过了。这里我不用任何图形控件,就用最原始的方式,用脚本画html的方式来展现一个图形化的机构树。
二、功能分析
当然我们除了生成图形的功能还有其它的维护机构数据的功能:
1、展现机构图形
2、新增组织机构
3、编辑组织机构
4、删除组织机构
5、给组织机构设置拥有的角色
三、具体实现
图形展示的实现前面已经说了用脚本画页面html,
新增修改节点则利用easyui的window或dialog控件弹出窗口编辑
设置角色也弹出窗口选择,除了展现图形其它的应该都没什么难度,后台还是采用webapi来处理数据。
1、还是从mvc控制器开始,新建一个名叫Organize的控制器,代码如下:
public class OrganizeController : Controller{public ActionResult Index(){var model = new sys_organizeService().GetModelList();return View(model);}}
这里直接把机构数据取出来传到view中使用,当然也可以在前台脚本中ajax请求到webapi中获得。
2、接下来再创建对应的view
@{ViewBag.Title = "title";Layout = "~/Views/Shared/_Layout.cshtml"; }@section scripts{@Scripts.Render("~/Resource/Sys/Organize.js")<script type="text/javascript">var data = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model));ko.bindingViewModel(new viewModelOrganize(data));</script> }<div class="z-toolbar"><a id="a_refresh" href="#" plain="true" class="easyui-linkbutton" icon="icon-rfs" title="刷新" data-bind="click:refreshClick">刷新</a><a id="a_add" href="#" plain="true" class="easyui-linkbutton" icon="icon-add" title="新增" data-bind="click:addClick">新增</a><a id="a_edit" href="#" plain="true" class="easyui-linkbutton" icon="icon-edit" data-bind="click:editClick" title="编辑">编辑</a><a id="a_del" href="#" plain="true" class="easyui-linkbutton" icon="icon-cross" title="删除" data-bind="click:deleteClick">删除</a><a id="a_role" href="#" plain="true" class="easyui-linkbutton" icon="icon-group" title="设置角色" data-bind="click:roleClick">设置角色</a></div><div class="wrapper" style="width: 100%; height: 100%; margin-top:15px;"></div><script type="text/html" id="edit-template"><div class="container_16" style="width:90%;margin:5%;"> <div class="grid_3 lbl" >上级机构</div> <div class="grid_13 val" ><input class="z-text easyui-combotree" data-bind="datasource:combotreeData,combotreeValue:form.ParentCode" /><span data-bind="text:form.ParentCode" style="margin:5px;"></span></div><div class="grid_3 lbl">机构编码</div> <div class="grid_13 val"><input class="z-txt easyui-validatebox" style="width:145px;" data-bind="value:form.OrganizeCode" data-options="required:true" /></div><div class="grid_3 lbl">机构名称</div> <div class="grid_13 val"><input class="z-txt easyui-validatebox" style="width:145px;" data-bind="value:form.OrganizeName" data-options="required:true" /></div><div class="grid_3 lbl">备注</div> <div class="grid_13 val"><textarea class="z-txt" style="width:220px;height:50px;" data-bind="value:form.Description" ></textarea></div><div class="clear"></div></div> <div style="text-align:center;"><a class="easyui-linkbutton" data-options="iconCls:'icon-ok'" data-bind="click:confirmClick" href="javascript:void(0)" >确定</a> <a class="easyui-linkbutton" data-options="iconCls:'icon-cancel'" data-bind="click:cancelClick" href="javascript:void(0)">取消</a> </div> </script><script type="text/html" id="setrole-template"><style type="text/css">.listview{ margin:0 !important;}.listview li{width:100px !important;background-color:skyblue !important;float:left;margin:3px;}</style><div style="margin:5px;height:370px;overflow:auto;" ><div style="border-bottom:1px solid #CCC; margin-bottom:5px;"><span class="icon32 icon-org32" style="padding-left:48px;font-weight:bold; font-size:14px;color:#666;" data-bind="text:OrganizeName">机构名称</span> </div><div> 拥有角色(请点击勾选):</div><div class="metrouicss"><ul class="listview"></ul></div></div><div style="text-align:center;"><a class="easyui-linkbutton" data-options="iconCls:'icon-ok'" data-bind="click:confirmClick" href="javascript:void(0)" >确定</a> <a class="easyui-linkbutton" data-options="iconCls:'icon-cancel'" data-bind="click:cancelClick" href="javascript:void(0)">取消</a> </div> </script><script type="text/html" id="tr-node-template"><tr class="tr-node"><td colspan="{0}"><table align="center"border="1" cellpadding="2" cellspacing="0"><tr><td class="td-node" id='td{3}' data-node='{2}' align="center" valign="top">{ 1}</td></tr></table></td></tr> </script><script type="text/html" id="tr-hline-template"><tr class="tr-hline"><td><table><tr><td class="treeempty"></td><td class="treedot"></td><td class="treedot"></td></tr></table></td><td class="treedot" colspan="{0}"></td><td><table><tr><td class="treedot"></td><td class="treedot"></td><td class="treempty"></td></tr></table></td></tr> </script>
这个view还是很简单的,图形区只需要一个class="wrapper"的div即可,其它都是html模板,弹出窗口及生成图形时用到。
3、前端UI与数据交互的viewModel
/** * 模块名:mms viewModel * 程序名: organize.js * Copyright(c) 2013-2015 liuhuisheng [ liuhuisheng.xm@gmail.com ] **/function viewModelOrganize(data) {var self = this;this.refreshClick = function () {window.location.reload();};this.save = function (vm,win) {var post = { form: ko.toJS(vm.form) };com.ajax({type: 'POST',url: '/api/sys/organize/edit',data: JSON.stringify(post),success: function (d) {com.message('success', '保存成功!');win.dialog('close');self.initGraph(d);}});}this.addClick = function () {var defaults = { ParentCode: (self.selectNode || {}).OrganizeCode || 0 };self.openDiloag("添加新机构", defaults, function (vm, win) {if (com.formValidate(win)) {vm.form._OrganizeCode = vm.form.OrganizeCode();self.save(vm,win);}});};this.editClick = function () {if (!self.selectNode) return com.message('warning', '请先选择一个机构!');self.openDiloag("编辑机构-"+self.selectNode.OrganizeName,self.selectNode, function (vm, win) {if (com.formValidate(win)) {self.save(vm,win);}});};this.deleteClick = function () {if (!self.selectNode) return com.message('warning', '请先选择一个机构!');com.message('confirm', '确认要删除选中的机构吗?', function (b) {if (b) {com.ajax({type: 'DELETE',url: '/api/sys/organize/' + self.selectNode.OrganizeCode,success: function (d) {com.message('success', '删除成功!');self.initGraph(d);}});}});};this.roleClick = function () {if (!self.selectNode)return com.message('warning', '请先选择一个机构!');com.dialog({title: "设置角色",width: 600,height: 450,html: "#setrole-template",viewModel: function (w) {var thisRole = this;this.OrganizeName = ko.observable(self.selectNode.OrganizeName);com.loadCss('/Resource/css/metro/css/modern.css', parent.document);com.ajax({type: 'GET',url: '/api/sys/organize/getrolewithorganizecheck/' + self.selectNode.OrganizeCode,success: function (d) {var ul = w.find(".listview");for (var i in d)ul.append(utils.formatString('<li role="{0}" class="{2}">{1}</li>', d[i].RoleCode, d[i].RoleName, d[i].Checked == 'true' ? 'selected' : ''));ul.find("li").click(function () {if ($(this).hasClass('selected'))$(this).removeClass('selected');else$(this).addClass('selected');});}});this.confirmClick = function () {var roles = [];w.find("li.selected").each(function () {roles.push({ RoleCode: $(this).attr('role') });});com.ajax({url: '/api/sys/organize/editorganizeroles/' + self.selectNode.OrganizeCode,data: ko.toJSON(roles),success: function (d) {thisRole.cancelClick();com.message('success', '保存成功!');}});};this.cancelClick = function () {w.dialog('close');};}});};this.openDiloag = function (title,node,fnConfirm) {com.dialog({title: title,height: 250,width: 400,html: "#edit-template",viewModel: function (w) {var that = this;this.combotreeData = function () {var list = utils.filterProperties(data, ['OrganizeCode as id', 'ParentCode as pid', 'OrganizeName as text']);var treeData = utils.toTreeData(list, "id", "pid", "children");treeData.unshift({ "id": 0, "text": "==请选择==" });return treeData;};this.form = {_OrganizeCode:node.OrganizeCode,ParentCode: ko.observable(node.ParentCode),OrganizeCode: ko.observable(node.OrganizeCode),OrganizeName: ko.observable(node.OrganizeName),Description: ko.observable(node.Description)};this.calcCode = function (v) { //新增时 自动计算OrganizeCodeif (!that.form._OrganizeCode) {v = v == 0 ? "" : v;var list = [], suffix;for (var i in self.data) {list.push(self.data[i].OrganizeCode);}for (var j = 1; j < 100; j++) {suffix = j < 10 ? ("0" + j.toString()) : j.toString();if ($.inArray(v + suffix,list) == -1) break;}that.form.OrganizeCode(v + suffix);}};this.form.ParentCode.subscribe(this.calcCode);this.calcCode(node.ParentCode);this.confirmClick = function () {fnConfirm(this,w);};this.cancelClick = function () {w.dialog('close');};}});};this.initGraph = function (data) {self.data = data;var wrapper = $("div.wrapper").empty();var treeData = utils.toTreeData(data, "OrganizeCode", "ParentCode", "children");var tb = renderTreeGraph(treeData);tb.appendTo(wrapper);//绑定事件$(wrapper).find(".td-node").click(function () {$(".td-node").css({ "background-color": "#f6f6ff", "color": "" });$(this).css({ "background-color": "#faffbe", "color": "#FF0000" });self.selectNode = $(this).data("node");}).dblclick(self.editClick);if (self.selectNode) {$("#td" + self.selectNode.OrganizeCode).css({ "background-color": "#faffbe", "color": "#FF0000" });}};this.initGraph(data); }function renderTreeGraph(treeData) {//生成图形var tb = $('<table class="tb-node" cellspacing="0" cellpadding="0" align="center" border="0" style="border-width:0px;border-collapse:collapse;margin:0 auto;vertical-align:top"></table>');var tr = $('<tr></tr>');for (var i in treeData) {if (i > 0) $('<td> </td>').appendTo(tr);$('<td style="vertical-align:top;text-align:center;"></td>').append(createChild(treeData[i])).appendTo(tr);}tr.appendTo(tb);return tb; }//递归生成机构树图形 function createChild(node, ischild) {var length = (node.children || []).length;var colspan = length * 2 - 1;if (length == 0)colspan = 1;var fnTrVert = function () {var tr1 = $('<tr class="tr-vline"><td colspan="'+colspan+'"><img class="img-v" src="/Resource/images/tree/Tree_Vert.gif" ></td></tr>');return tr1;};
//1.创建容器var tb = $('<table class="tb-node" cellspacing="0" cellpadding="0" align="center" border="0"></table>'); //2.如果本节点是子节点,添加竖线在节点上面if (ischild) {fnTrVert().appendTo(tb);}// 3.添加本节点到图表var tr3 = $("#tr-node-template").html();tr3 = utils.formatString(tr3, colspan, node.OrganizeName, JSON.stringify(node),node.OrganizeCode);$(tr3).appendTo(tb);// 4.增加上下级的连接线if (length > 1) {//增加本级连接下级的首节点竖线,在节点下方 fnTrVert().appendTo(tb);//增加本级连接下级的中间横线var tr4 = $("#tr-hline-template").html();tr4 = utils.formatString(tr4, colspan - 2);$(tr4).appendTo(tb);}//5.递归增加下级所有子节点到图表if (length > 0) {var tr5 = $('<tr></tr>');for (var i in node.children) {if (i > 0) {$('<td</td>').appendTo(tr5);}$('<td></td>').append(createChild(node.children[i], true)).appendTo(tr5);}tr5.appendTo(tb);}return tb; }
这段交互的逻辑和之前的viewModel一样,基本上定义了工具栏上的按钮对应的事件,通过data-bind绑定到UI上。熟悉knockoutjs的朋友就很容易理解了。
在新增机构时我有做一个处理根据新增节点的父节点自动计算本节点的编码。
生成图形的处理基本上是通过生成table来配合一些背景图片来实现图形展示,我注释写得很清楚了。
4、后台webapi中的数据处理
主要是viewModel中ajax调用的方法,有以下:
1 添加或编辑机构 POST /api/sys/organize/edit
2 删除机构 DELETE /api/sys/organize/id
3 获取机构拥有的角色 GET /api/sys/organize/getrolewithorganizecheck/id
4 保存机构拥有的角色 POST /api/sys/organize/editorganizeroles/id
那么我们的web api controller中:
public class OrganizeApiController : ApiController{ public dynamic GetRoleWithOrganizeCheck(string id){var service = new sys_organizeService();return service.GetOrganizeRole(id);}[System.Web.Http.HttpPost]public dynamic Edit(dynamic data){var formWrapper = RequestWrapper.Instance().LoadSettingXmlString(@" <settings><table>sys_organize</table><where><field name='OrganizeCode' cp='equal' variable='_OrganizeCode'></field></where> </settings>");var service = new sys_organizeService();service.Edit(formWrapper, null, data);
var result = service.GetModelList();return result;}public dynamic Delete(string id){var service = new sys_organizeService();service.RecursionDelete(id);var result = service.GetModelList();return result;}[System.Web.Http.HttpPost]public void EditOrganizeRoles(string id, dynamic data){var service = new sys_organizeService();service.SaveOrganizeRoles(id, data as JToken);}}
在webapi中后台的处理很简单,每个方法只有几句代码。至此我们已经大功告成了。
四、效果图
打开页面
选择总务后点击新增时,会自动把总务设置为父节点,并计算出新的机构编码
保存成功
双击修改总务节点
给总务节点设置角色
五、后述