個人權限系統設計實現MVC4 + WebAPI + EasyUI + Knockout(三)圖形化機構樹

1、前言

組織機構是國內管理系統中很重要的一個概念,之前咱們基本都是採用數據列表的形式展示,最多隻是採用樹形列表展示。雖然夠用,可是若是能作成圖形化固然是最好不過了。這裏我不用任何圖形控件,就用最原始的方式,用腳本畫html的方式來展示一個圖形化的機構樹。javascript

2、功能分析

固然咱們除了生成圖形的功能還有其它的維護機構數據的功能:
一、展示機構圖形
二、新增組織機構
三、編輯組織機構
四、刪除組織機構
五、給組織機構設置擁有的角色css

3、具體實現

圖形展現的實現前面已經說了用腳本畫頁面html,
新增修改節點則利用easyui的window或dialog控件彈出窗口編輯
設置角色也彈出窗口選擇,除了展示圖形其它的應該都沒什麼難度,後臺仍是採用webapi來處理數據。html

一、仍是從mvc控制器開始,新建一個名叫Organize的控制器,代碼以下: 前端

    public class OrganizeController : Controller
    {
        public ActionResult Index()
        {
            var model = new sys_organizeService().GetModelList();
            return View(model);
        }
    }

這裏直接把機構數據取出來傳到view中使用,固然也能夠在前臺腳本中ajax請求到webapi中得到。java

二、接下來再建立對應的viewnode

@{
    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模板,彈出窗口及生成圖形時用到。web

三、前端UI與數據交互的viewModelajax

/**
* 模塊名: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) { //新增時 自動計算OrganizeCode
                    if (!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>&nbsp;</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來配合一些背景圖片來實現圖形展現,我註釋寫得很清楚了。api

四、後臺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中: mvc

    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中後臺的處理很簡單,每一個方法只有幾句代碼。至此咱們已經大功告成了。

4、效果圖

 

打開頁面
image

選擇總務後點擊新增時,會自動把總務設置爲父節點,並計算出新的機構編碼
image

保存成功
image

雙擊修改總務節點image

給總務節點設置角色
image

5、後述
因爲一些私事,這段時間一直都沒去辦公室,今晚偷空寫了這篇,感謝你們對個人支持。
這個系列的博客寫了後不少博友問我要這個框架的源碼,咱們打算讓你們團了,300人起團,人數到了就統一發給你們。

若是你以爲不錯就幫我【推薦】一下吧,你的支持纔是我能堅持寫完這個系列文章的動力。
技術交流QQ羣:羣一:328510073(已滿),羣二:167813846,歡迎你們來交流,想團源碼的朋友進羣后把城市-名字-手機-QQ發給我便可。

系列博客連接:

個人權限系統設計實現MVC4 + WebAPI + EasyUI + Knockout(一)
個人權限系統設計實現MVC4 + WebAPI + EasyUI + Knockout(二)菜單導航
相關文章
相關標籤/搜索