使用gojs制作一个具备文件读写功能的家谱网页

效果

一些功能

操作过程中,随时可以用 Ctrl + Z, Ctrl + Y 撤销或回退操作

双击空白创建一个节点
这里写图片描述
也可以导入已有文件
这里写图片描述
这里写图片描述
查看节点信息:可以查看到性别,生卒年,孩子数,孙子数,后代数等的信息
这里写图片描述
搜索功能:可按名字,性别,生年卒年等进行搜索,结果高亮
这里写图片描述
编写节点信息:相应节点颜色信息被更新
这里写图片描述
拖动节点:可使其成为某人的后代
这里写图片描述
拖动子树可以任意改变后代关系

右击节点:有以下操作:
这里写图片描述
“斩断”功能:将子树变为另一棵树
“除名”功能:去掉节点名字
“驱逐”功能:消灭当前节点,其子树生成新的树
“灭门”功能:遍历删除当前节点与其后代

可随时下载当前图表的数据文件。

数据结构

数据储存使用双亲表示法,由于树中的每个结点都有唯一的一个双亲结点,所以可用一组连续的存储空间(一维数组)存储树中的各个结点,数组中的一个元素表示树中的一个结点,每个结点含两个域,数据域存放结点本身信息,双亲域指示本结点的双亲结点在数组中位置。
树的查找算法:遍历算法从数据库中遍历属性 parents 是该节点的 key 即可 得到该节点的孩子,并将孩子存入到栈中,如果栈不为空,递归调用。
树的插入算法:首先找到插入的位置,要么向左,要么向右,直到找到空结点, 即为插入位置,如果找到了相同值的结点,插入失败。
树的删除算法:相对查找和插入复杂一点,根据待删除结点的孩子情况,分三种情况:没有孩子,只有一个孩子,有两个孩子。没有孩子的情况,其父结点指向空,删除该结点。有一个孩子的情况,其父结点指向其孩子,删除该结点。有两个孩子的情况,当前结点与左子树中最大的元素交换,然后删除当前结点。左子树最大的元素一定是叶子结点,交换后,当前结点即为叶子结点,删除参考没有孩子的情况。另一种方法是,当前结点与右子树中最小的元素交换,然后删除当前结点。

实现

gojs是一个基于原生js的图表图形框架,兼容各浏览器,而且上手容易,gojs官网里有丰富的样例。
为了符合家谱增删改查的基本功能和一些扩展功能,我选择了orgChartEditor模型,还会用到编辑框DataInspector组件,并在DataInspector介绍页面获取依赖的 DataInspector.css和DataInspector.js。
这里写图片描述
还要获取最重要的go.js文件。

文件结构

│  edit.html
│
├─css
│      DataInspector.css
│
└─js
        DataInspector.js
        edit.js
        go.js

主要工作是完成edit.html和edit.js。

edit.html

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>家谱</title>
        <meta charset="UTF-8">
        <script src="js/go.js"></script>
        <link rel="stylesheet" href="css/DataInspector.css"/>
        <script src="js/DataInspector.js"></script>
        <script src="js/edit.js"></script>
    </head>
    <body style="background-color: #696969; overflow: hidden;">
        <div style="position: fixed; z-index: 999">
            <select id="query">
                <option value="key">key</option>
                <option value="名字">名字</option>
                <option value="parent">parent</option>
                <option value="性别">性别</option>
                <option value="双亲">双亲</option>
                <option value="生年">生年</option>
                <option value="卒年">卒年</option>
                <option value="在世">在世</option>
            </select>
            <input type="text" id="mySearch">
            <span id="answerCount" style="color: white;"></span>
            <div>
                <div id="myInspector"></div>
            </div>
            <div>
                <button id="refresh">刷新</button>
                <button onclick="Save()">下载</button>
                <input type="file" name="fleUpload" id="fleUpload" onChange="load(this.files);" style="color: #696969"/>
            </div>
        </div>
        <div id="sample">
            <div id="myDiagramDiv" style="background-color: #696969; height: 500px"></div>
        </div>
    </body>
</html>

这里用到了html5的<input type="file" />,来实现文件的读取。

edit.js

function init() {
    var $ = go.GraphObject.make;
    myDiagram = $(go.Diagram, "myDiagramDiv", {
        initialContentAlignment: go.Spot.Center,
        validCycle: go.Diagram.CycleDestinationTree,
        "clickCreatingTool.archetypeNodeData": {},
        "clickCreatingTool.insertPart": function(loc) {
            this.archetypeNodeData = {
                key: getNextKey(),
                parent: "",
                名字: "(佚名)",
                性别: "",
                双亲: "",
                生年: "",
                卒年: "",
                备注: ""
            };
            return go.ClickCreatingTool.prototype.insertPart.call(this, loc)
        },
        layout: $(go.TreeLayout, {
            sorting: go.TreeLayout.SortingAscending,
            comparer: function(a, b) {
                var av = a.node.data.生年;
                var bv = b.node.data.生年;
                if (av < bv) {
                    return -1
                }
                if (av > bv) {
                    return 1
                }
                return 0
            },
            treeStyle: go.TreeLayout.StyleLastParents,
            arrangement: go.TreeLayout.ArrangementHorizontal,
            angle: 90,
            layerSpacing: 35,
            alternateAngle: 90,
            alternateLayerSpacing: 35,
            alternateAlignment: go.TreeLayout.AlignmentBus,
            alternateNodeSpacing: 20
        }),
        "undoManager.isEnabled": true
    });
    fitBrowserWindow();
    myDiagram.add($(go.Part, "Table", {
        position: new go.Point(-100,0),
        selectable: false
    }, $(go.Panel, "Horizontal", {
        row: 0,
        alignment: go.Spot.Left
    }, $(go.Shape, "Rectangle", {
        desiredSize: new go.Size(30,30),
        fill: "#0099CC",
        margin: 5,
        stroke: "#0099CC"
    }), $(go.TextBlock, "男", {
        font: "500 15px Droid Serif, sans-serif",
        stroke: "white"
    })), $(go.Panel, "Horizontal", {
        row: 1,
        alignment: go.Spot.Left
    }, $(go.Shape, "Rectangle", {
        desiredSize: new go.Size(30,30),
        fill: "#FF6666",
        margin: 5,
        stroke: "#FF6666"
    }), $(go.TextBlock, "女", {
        font: "500 15px Droid Serif, sans-serif",
        stroke: "white"
    })), $(go.Panel, "Horizontal", {
        row: 2,
        alignment: go.Spot.Left
    }, $(go.Shape, "Rectangle", {
        desiredSize: new go.Size(30,30),
        fill: "#FFA100",
        margin: 5,
        stroke: "#FFA100"
    }), $(go.TextBlock, "未知", {
        font: "500 15px Droid Serif, sans-serif",
        stroke: "white"
    }))));
    myDiagram.addDiagramListener("Modified", function(e) {
        var button = document.getElementById("SaveButton");
        if (button) {
            button.disabled = !myDiagram.isModified
        }
        var idx = document.title.indexOf("*");
        if (myDiagram.isModified) {
            if (idx < 0) {
                document.title += "*"
            }
        } else {
            if (idx >= 0) {
                document.title = document.title.substr(0, idx)
            }
        }
    });
    function getNextKey() {
        var key = nodeIdCounter;
        while (myDiagram.model.findNodeDataForKey(key) !== null) {
            key = nodeIdCounter++
        }
        return key
    }
    var nodeIdCounter = 1;
    function nodeDoubleClick(e, obj) {
        var clicked = obj.part;
        if (clicked !== null) {
            var thisemp = clicked.data;
            myDiagram.startTransaction("add employee");
            var Yparent = myDiagram.model.findNodeDataForKey(thisemp.key);
            var newemp;
            newemp = {
                key: getNextKey(),
                名字: "(佚名)",
                parent: thisemp.key,
                性别: "",
                双亲: "",
                生年: "",
                卒年: "",
                备注: ""
            };
            myDiagram.model.addNodeData(newemp);
            myDiagram.commitTransaction("add employee")
        }
    }
    function mayWorkFor(node1, node2) {
        if (!(node1 instanceof go.Node)) {
            return false
        }
        if (node1 === node2) {
            return false
        }
        if (node2.isInTreeOf(node1)) {
            return false
        }
        return true
    }
    function textStyle() {
        return {
            font: "9pt  Segoe UI,sans-serif",
            stroke: "white"
        }
    }
    function tooltipTextConverter(person) {
        var str = "";
        var YnewJson = myDiagram.model.toJSON();
        var result = JSON.parse(YnewJson);
        var Yparent = myDiagram.model.findNodeDataForKey(person.parent);
        var Yself = myDiagram.model.findNodeDataForKey(person.key);
        var YchildNumber = 0;
        var YgrandChildNumber = 0;
        var YallChild = 0;
        for (var i = 1; i <= result.nodeDataArray.length; i++) {
            var Ytemp = myDiagram.model.findNodeDataForKey(i);
            if (Ytemp.parent === undefined) {
                continue
            } else {
                if (Ytemp.parent == Yself.key) {
                    YchildNumber++
                }
            }
        }
        var Ytemps = [];
        for (var i = 1; i <= result.nodeDataArray.length; i++) {
            var Ytemp = myDiagram.model.findNodeDataForKey(i);
            if (Ytemp.parent === undefined) {
                continue
            } else {
                if (Ytemp.parent == Yself.key) {
                    Ytemps.push(Ytemp.key)
                }
            }
        }
        while (Ytemps.length != 0) {
            var hello = Ytemps.pop();
            var Ytemp1 = myDiagram.model.findNodeDataForKey(hello);
            for (var i = 1; i <= result.nodeDataArray.length; i++) {
                var Ytemp = myDiagram.model.findNodeDataForKey(i);
                if (Ytemp.parent === undefined) {
                    continue
                } else {
                    if (Ytemp.parent == Ytemp1.key) {
                        YgrandChildNumber++
                    }
                }
            }
        }
        var Ytemp2s = [];
        Ytemp2s.push(person.key);
        do {
            var hello = Ytemp2s.pop();
            var Ytemp1 = myDiagram.model.findNodeDataForKey(hello);
            for (var i = 1; i <= result.nodeDataArray.length; i++) {
                var Ytemp = myDiagram.model.findNodeDataForKey(i);
                if (Ytemp.parent === undefined) {
                    continue
                } else {
                    if (Ytemp.parent == Ytemp1.key) {
                        YallChild++;
                        Ytemp2s.push(Ytemp.key)
                    }
                }
            }
        } while (Ytemp2s.length != 0);str += "名字: " + person.名字;
        if (person.性别 !== undefined) {
            str += "\n性别: " + person.性别
        }
        if (person.双亲 !== undefined && person.parent !== undefined && Yparent.性别 === "男") {
            str += "\n父亲: " + Yparent.名字 + "\n母亲: " + person.双亲
        }
        if (person.双亲 !== undefined && person.parent !== undefined && Yparent.性别 === "女") {
            str += "\n母亲: " + Yparent.名字 + "\n父亲: " + person.双亲
        }
        if (person.生年 !== undefined) {
            str += "\n生年: " + person.生年
        }
        if (person.卒年 !== undefined) {
            str += "\n卒年: " + person.卒年
        }
        str += "\n孩子数: " + YchildNumber;
        str += "\n孙子数: " + YgrandChildNumber;
        str += "\n后代数: " + YallChild;
        return str
    }
    var tooltiptemplate = $(go.Adornment, "Auto", $(go.Shape, "Rectangle", {
        fill: "whitesmoke",
        stroke: "black"
    }), $(go.TextBlock, {
        font: "bold 8pt Helvetica, bold Arial, sans-serif",
        wrap: go.TextBlock.WrapFit,
        margin: 5
    }, new go.Binding("text","",tooltipTextConverter)));
    function genderBrushConverter(gender) {
        if (gender === "男") {
            return "#0099CC"
        }
        if (gender === "女") {
            return "#FF6666"
        }
        return "#FFA100"
    }
    myDiagram.nodeTemplate = $(go.Node, "Auto", new go.Binding("text","生年"), {
        toolTip: tooltiptemplate
    }, {
        doubleClick: nodeDoubleClick
    }, {
        mouseDragEnter: function(e, node, prev) {
            var diagram = node.diagram;
            var selnode = diagram.selection.first();
            if (!mayWorkFor(selnode, node)) {
                return
            }
            var shape = node.findObject("SHAPE");
            if (shape) {
                shape._prevFill = shape.fill;
                shape.fill = "darkred"
            }
        },
        mouseDragLeave: function(e, node, next) {
            var shape = node.findObject("SHAPE");
            if (shape && shape._prevFill) {
                shape.fill = shape._prevFill
            }
        },
        mouseDrop: function(e, node) {
            var diagram = node.diagram;
            var selnode = diagram.selection.first();
            if (mayWorkFor(selnode, node)) {
                var link = selnode.findTreeParentLink();
                if (link !== null) {
                    link.fromNode = node
                } else {
                    diagram.toolManager.linkingTool.insertLink(node, node.port, selnode, selnode.port)
                }
            }
        }
    }, new go.Binding("layerName","isSelected",function(sel) {
        return sel ? "Foreground" : ""
    }
    ).ofObject(), $(go.Shape, "Rectangle", {
        name: "SHAPE",
        fill: null,
        strokeWidth: 2,
        stroke: null,
        portId: "",
        fromLinkable: true,
        toLinkable: true,
        cursor: "pointer"
    }, new go.Binding("stroke","isHighlighted",function(h) {
        return h ? "white" : "#696969"
    }
    ).ofObject(), new go.Binding("fill","性别",genderBrushConverter)), $(go.Panel, "Horizontal", $(go.Panel, "Table", {
        maxSize: new go.Size(150,999),
        margin: new go.Margin(6,10,0,10),
        defaultAlignment: go.Spot.Left
    }, $(go.RowColumnDefinition, {
        column: 2,
        width: 4
    }), $(go.TextBlock, textStyle(), {
        row: 0,
        column: 0,
        columnSpan: 5,
        font: "12pt Segoe UI,sans-serif",
        editable: true,
        isMultiline: false,
        minSize: new go.Size(10,16)
    }, new go.Binding("text","名字").makeTwoWay()), $(go.TextBlock, textStyle(), {
        row: 1,
        column: 0
    }, new go.Binding("text","key",function(v) {
        return "key: " + v
    }
    )), $(go.TextBlock, textStyle(), {
        row: 1,
        column: 3,
    }, new go.Binding("text","parent",function(v) {
        return "parent: " + v
    }
    )), $(go.TextBlock, textStyle(), {
        row: 2,
        column: 0,
    }, new go.Binding("text","生年",function(v) {
        return "(" + v
    }
    )), $(go.TextBlock, textStyle(), {
        row: 2,
        column: 1,
    }, new go.Binding("text","卒年",function(v) {
        return " - " + (v ? v : "        ") + ")"
    }
    )), $(go.TextBlock, textStyle(), {
        row: 3,
        column: 0,
        columnSpan: 5,
        font: "italic 9pt sans-serif",
        wrap: go.TextBlock.WrapFit,
        editable: true,
        minSize: new go.Size(10,14)
    }, new go.Binding("text","备注").makeTwoWay()))));
    myDiagram.nodeTemplate.contextMenu = $(go.Adornment, "Vertical", $("ContextMenuButton", $(go.TextBlock, "-  斩断  -"), {
        click: function(e, obj) {
            var node = obj.part.adornedPart;
            if (node !== null) {
                var thisemp = node.data;
                myDiagram.startTransaction("斩断");
                myDiagram.model.setDataProperty(thisemp, "parent", "0");
                myDiagram.commitTransaction("斩断")
            }
        }
    }), $("ContextMenuButton", $(go.TextBlock, "-  除名  -"), {
        click: function(e, obj) {
            var node = obj.part.adornedPart;
            if (node !== null) {
                var thisemp = node.data;
                myDiagram.startTransaction("vacate");
                myDiagram.model.setDataProperty(thisemp, "名字", "(已除名)");
                myDiagram.commitTransaction("vacate")
            }
        }
    }), $("ContextMenuButton", $(go.TextBlock, "-  驱逐   -"), {
        click: function(e, obj) {
            var node = obj.part.adornedPart;
            if (node !== null) {
                myDiagram.startTransaction("reparent remove");
                var chl = node.findTreeChildrenNodes();
                while (chl.next()) {
                    var emp = chl.value;
                    myDiagram.model.setParentKeyForNodeData(emp.data, 0)
                }
                myDiagram.model.removeNodeData(node.data);
                myDiagram.commitTransaction("reparent remove")
            }
        }
    }), $("ContextMenuButton", $(go.TextBlock, "-  灭门  -"), {
        click: function(e, obj) {
            var node = obj.part.adornedPart;
            if (node !== null) {
                myDiagram.startTransaction("remove dept");
                myDiagram.removeParts(node.findTreeParts());
                myDiagram.commitTransaction("remove dept")
            }
        }
    }));
    myDiagram.linkTemplate = $(go.Link, {
        routing: go.Link.Orthogonal,
        corner: 5,
        selectable: false
    }, $(go.Shape, {
        strokeWidth: 3,
        stroke: "#00FF00"
    }));
    myDiagram.model = go.Model.fromJson({
        "class": "go.TreeModel",
        "nodeDataArray": []
    });
    if (window.Inspector) {
        myInspector = new Inspector("myInspector",myDiagram,{
            properties: {
                "key": {
                    readOnly: true
                },
                "备注": {},
                "parent": {}
            }
        })
    }
}
function doSave(value, type, name) {
    var blob;
    if (typeof window.Blob == "function") {
        blob = new Blob([value],{
            type: type
        })
    } else {
        var BlobBuilder = window.BlobBuilder || window.MozBlobBuilder || window.WebKitBlobBuilder || window.MSBlobBuilder;
        var bb = new BlobBuilder();
        bb.append(value);
        blob = bb.getBlob(type)
    }
    var URL = window.URL || window.webkitURL;
    var bloburl = URL.createObjectURL(blob);
    var anchor = document.createElement("a");
    if ("download"in anchor) {
        anchor.style.visibility = "hidden";
        anchor.href = bloburl;
        anchor.download = name;
        document.body.appendChild(anchor);
        var evt = document.createEvent("MouseEvents");
        evt.initEvent("click", true, true);
        anchor.dispatchEvent(evt);
        document.body.removeChild(anchor)
    } else {
        if (navigator.msSaveBlob) {
            navigator.msSaveBlob(blob, name)
        } else {
            location.href = bloburl
        }
    }
}
function Save() {
    doSave(myDiagram.model.toJson(), "text/latex", "家谱.json");
    myDiagram.isModified = false
}
function load(f) {
    if (typeof FileReader == "undefined") {
        alert("检测到您的浏览器不支持FileReader对象!")
    }
    var tmpFile = f[0];
    var reader = new FileReader();
    reader.readAsText(tmpFile);
    reader.onload = function(e) {
        myDiagram.model = go.Model.fromJson(e.target.result)
    }
}
function refresh() {
    myDiagram.model = go.Model.fromJson(myDiagram.model.toJson())
}
function fitBrowserWindow() {
    myDiagram.div.style.height = window.innerHeight + "px";
    myDiagram.requestUpdate()
}
function searchDiagram() {
    document.getElementById("answerCount").innerText = "";
    var input = document.getElementById("mySearch");
    if (!input) {
        return
    }
    input.focus();
    var regex = new RegExp(input.value,"i");
    myDiagram.startTransaction("highlight search");
    myDiagram.clearHighlighteds();
    if (input.value) {
        var queryObj = {};
        if (document.getElementById("query").value != "在世") {
            queryObj[document.getElementById("query").value] = regex
        } else {
            queryObj.生年 = function(n) {
                return n <= input.value && n != ""
            }
            ;
            queryObj.卒年 = function(n) {
                return n >= input.value | n == ""
            }
        }
        var results = myDiagram.findNodesByExample(queryObj);
        myDiagram.highlightCollection(results);
        if (results.count > 0) {
            myDiagram.centerRect(results.first().actualBounds)
        }
        document.getElementById("answerCount").innerText = "共找到" + results.count + "个结果"
    }
    myDiagram.commitTransaction("highlight search")
}
window.onload = function() {
    init();
    document.getElementById("mySearch").addEventListener("input", searchDiagram);
    document.getElementById("query").addEventListener("change", searchDiagram);
    document.getElementById("refresh").addEventListener("click", refresh)
}
;
window.onresize = fitBrowserWindow;

猜你喜欢

转载自blog.csdn.net/z_j_q_/article/details/79588432
今日推荐