公司项目有个功能要实现树形结构的展示,类似于组织架构的那种。
想了下几种方案,
- 表格内实现
- div内通过绝对定位实现
- canva实现
想了一下,感觉第一种简单一些, 用表格的话可能不需要计算繁琐的定位,top,left什么的,不过用表格的坏处就是可扩展性差了些
粗略一想,大概设计方案如下:
- 后台组织好json格式数据返回前端,属性结构肯定有id和pid字段
- 结构图前端js+css展示,内容用带边框的方块展示,关系用线条。
想玩就像立马码字实现,代码还没敲问题来了,树形结构后面的分叉是动态的,该怎么实现呢?
于是我在纸上尝试画出图形,从顶层开始画,第一层一个节点 T,第二层两个节点T1,T2。上下距离岔开点,我一想如果T1后面有3个节点,T2后面又有3个节点。在不知道第三次的情况下我之前T1,T2上下就留了一点位置,然后画第三层就会得到示例的图形:
如果上图不算丑,那遇到第三层4个,第二个节点也有,则位置会被占用,那必然会出现更加不好看的效果:
第一张图如果我们事先知道是 1 2 6的结构,那我们肯定能画的好看一些,如下
这时候我意识到我这边除了之前考虑的那两点外,我可能还需要计算方块位置的算法,基于我们用table来实现位置的分布,方块位置需要如下展示:
那么如何得到图中的这个位置结构呢,首先知道这么画我们是知道后面节点的位置的,所以我决定从后往前推,T4,T5,T6是同一个父节点下的叶子节点,那T3的位置是与T5平齐的,于是得到下图:
如此可以得到T3位置,然后根据T2,T3得到T1的位置
当然这种子节点都在最后一层是理想结果,显示并不是所有情况都是上面那样,比如:
当然这只是简单的情况,还有各种更多层更加复杂的结构。
图中做了些改进,不同层之前间隔两列,一个用来父节点延申,一列用来分叉到子节点。另外便于画图,初步设计是两个子节点中间间隔一行,这样父节点位置刚好在一个表格的td中间。
现在来说核心的算法,不是太复杂,就是首先算好叶子节点的位置,比如图中的T3,T4,T5,T6,T8,T9。这些都可以通过遍历json数据来得到,通过递归,我们能得到前面所说几个节点的行和列的位置。
然后计算得到T2,T7位置,最后得出T1的位置。
得到方块内容的位置,我们就需要连线,连线的大体思路就是,td里放DIV,通过设置div的边框颜色和div的位置来绘制,小箭头么可以设置border得到三角形。
当然要实现并不是这么容易,需要各种折腾写样式,最后我是通过div然后写个before的伪类来实现线条,after伪类来实现箭头。
另外一般页面大小也就那么点大,为了减少点空间我去除了两个叶子节点中间的行,不过这也给父节点的定位造成小小的麻烦,比如T6,T7 这两个位置是在td中,而父节点则需要向上偏半个方块的高度。当然方块的高度是比td矮一些的,不然T6,T7就会碰一块了。
最后效果如下:
js代码如下:
1 $.fn.arch = function (rid, list) {
2 var $tb = $(this);
3 var maxRow = 0; //读取叶子节点位置开始行
4 var maxLevel = 0; //最大层级
5 readTree(list);
6 initTable();
7 drawTree();
8 drawPath();
9
10 function setLeaf(list) {
11 for (var i = 0; i < list.length; i++) {
12 var isleaf = 1;
13 for (var j = 0; j < list.length; j++) {
14 if (list[i].id == list[j].pid) {
15 isleaf = 0;
16 break;
17 }
18 }
19 list[i].isleaf = isleaf;
20 }
21 }
22
23 function readLeaf(list, pid, level) {
24 maxLevel = Math.max(level, maxLevel);
25 for (var i = 0; i < list.length; i++) {
26 if (list[i].pid == pid) {
27 list[i].level = level;
28 if (!list[i].isleaf) {
29 readLeaf(list, list[i].id, level + 1);
30 } else {
31 list[i].r = maxRow;
32 list[i].offset = 0;
33 maxRow++;
34 }
35 }
36 }
37 }
38
39 function calcPosition() {
40 var tmpList = [];
41 for (var i = maxLevel; i > 0; i--) {
42 for (var j = 0; j < list.length; j++) {
43 if (list[j].level == i && tmpList.indexOf(list[j].pid) < 0) {
44 var total = 0;
45 var count = 0;
46 var pindex = -1;
47 var isOffset = 0;
48 for (var k = 0; k < list.length; k++) {
49 if (list[k].pid == list[j].pid) {
50 isOffset = list[k].offset;
51 total += list[k].r;
52 count++;
53 } else if (list[k].id == list[j].pid) {
54 pindex = k;
55 }
56 }
57 var last = total % count;
58
59 if (pindex >= 0) {
60 list[pindex].r = count == 0 ? 0 : ((total - last) / count + ((last > 0) ? 1 : 0));
61 list[pindex].offset = last > 0 ? 1 : 0;
62 if (count == 1) {
63 list[pindex].offset = isOffset;
64 } else {
65 list[pindex].offset = last > 0 ? 1 : 0;
66 }
67 }
68 tmpList.push(list[j].pid);
69 }
70 }
71 }
72 }
73
74 function readTree(list) {
75 setLeaf(list);
76 readLeaf(list, rid, 1);
77 calcPosition();
78 }
79
80 function drawTree() {
81 for (var i = 0; i < list.length; i++) {
82 var item = list[i];
83 $tb.find("tr:eq(" + (item.r) + ")")
84 .find("td:eq(" + (item.level - 1) * 3 + ")>div")
85 .addClass("item-box" + (item.offset == 1 ? " offset" : ""))
86 .attr("data-pid", item.pid)
87 .attr("data-id", item.id)
88 .attr("data-offset", item.offset)
89 .append("<div class='item'>" + item.name + "</div>");
90 }
91 }
92
93
94 function drawPath() {
95 for (var i = 0; i < maxLevel; i++) {
96 var targetColumn = (i - 1) * 3;
97 var pNodes = $tb.find("tr").find("td:eq(" + targetColumn + ")>div[data-id]");
98 for (var j = 0; j < pNodes.length; j++) {
99 var $pNode = $(pNodes[j]);
100 var $pTd = $pNode.closest("td");
101 var isPOffset = $pNode.is("[data-offset='1']");
102
103 var subNodes = $tb.find("tr").find("[data-pid='" + $pNode.attr("data-id") + "']");
104 var length = subNodes.length;
105 if (length == 0) {
106 continue;
107 }
108
109 var ptopCls = isPOffset ? "line-top" : "line-center";
110 $pTd.next().children("div").addClass(ptopCls);
111
112 if (length == 1) {
113 $(subNodes[0]).closest("td").prev().children("div").addClass(ptopCls + " arrow");
114 } else if (length == 2) {
115 var startIndex = $(subNodes[0]).closest("tr").index();
116 var endIndex = $(subNodes[1]).closest("tr").index();
117
118 var firstOffset = $(subNodes[0]).is("[data-offset='1']");
119 var lastOffset = $(subNodes[1]).is("[data-offset='1']");
120
121 if (endIndex - startIndex == 1) {
122 if (firstOffset == lastOffset) {
123 $(subNodes[0]).closest("td").prev().children("div").addClass("corner-top-bottom " + (firstOffset ? "" : "top-35"));
124 } else if (firstOffset) {
125 $(subNodes[0]).closest("td").prev().children("div").addClass("corner-top arrow");
126 $(subNodes[1]).closest("td").prev().children("div").addClass("corner-bottom arrow top-35");
127 }
128 } else {
129 $(subNodes[0]).closest("td").prev().children("div").addClass("corner-top arrow " + (firstOffset ? "" : "top-35"));
130 !lastOffset && $(subNodes[1]).closest("td").prev().children("div").addClass("corner-bottom arrow top-35");
131
132 for (var m = startIndex + 1; m < endIndex; m++) {
133 if (m == endIndex - 1 && lastOffset) {
134 $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 2) + ")").children("div").addClass("corner-bottom arrow");
135 } else {
136 $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 2) + ")>div").addClass("line-left");
137 var currNodes = $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 3) + ")").children(".item-box");
138 if (currNodes.length > 0) {
139 var $currNode = $(currNodes[0]);
140 var currOffset = $currNode.is("[data-offset='1']");
141 var cls = currOffset ? "line-top arrow" : "line-center arrow";
142 $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 2) + ")").children("div").addClass(cls);
143 }
144 }
145 }
146 }
147 } else {
148 var $firstNode = $(subNodes[0]);
149 var $lastNode = $(subNodes[length - 1]);
150
151 var firstOffset = $firstNode.is("[data-offset='1']");
152 $firstNode.closest("td").prev().children("div").addClass("corner-top arrow " + (firstOffset ? "" : "top-35"));
153
154 var lastOffset = $lastNode.is("[data-offset='1']");
155 !lastOffset && $(subNodes[length - 1]).closest("td").prev().children("div").addClass("corner-bottom arrow top-35");
156
157 var startIndex = $firstNode.closest("tr").index();
158 var endIndex = $lastNode.closest("tr").index();
159 for (var m = startIndex + 1; m < endIndex; m++) {
160 if (m == endIndex - 1 && lastOffset) {
161 $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 2) + ")").children("div").addClass("corner-bottom arrow");
162 } else {
163 $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 2) + ")>div").addClass("line-left");
164 var currNodes = $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 3) + ")").children(".item-box");
165 if (currNodes.length > 0) {
166 var $currNode = $(currNodes[0]);
167 var currOffset = $currNode.is("[data-offset='1']");
168 var cls = currOffset ? "line-top arrow" : "line-center arrow";
169 $tb.find("tr:eq(" + m + ")").find("td:eq(" + (targetColumn + 2) + ")").children("div").addClass(cls);
170 }
171 }
172 }
173 }
174 }
175 }
176 }
177
178 function initTable() {
179 var rowCount = maxRow;
180 var columnCount = maxLevel * 3 - 2;
181 createTable(rowCount, columnCount);
182 }
183
184 function createTable(r, c) {
185 for (var i = 0; i < r; i++) {
186 var $tr = $("<tr class='item-row'></tr>");
187 for (var j = 0; j < c; j++) {
188 var last = j % 3;
189 var cls = "";
190 if (last == 0) {
191 cls = "td-item";
192 } else if (last == 1) {
193 cls = "td-line";
194 } else if (last == 2) {
195 cls = "td-arrow";
196 }
197 $tr.append("<td class='" + cls + "'><div></div></td>");
198 //$tr.append("<td " + (isitem ? "data-item='1'" : "width='50px'") + "><div class='" + (isitem ? "" : "line-item") + "'><div>" + (isitem ? "</div></div>" : "") + "</td>");
199 }
200 $tb.append($tr);
201 }
202 }
203
204 }
View Code
css如下
1 .item-box {
2 position: absolute;
3 top: 3px;
4 height: 100%;
5 }
6
7 .item-box.offset {
8 top: -35px;
9 }
10
11 .item-box .item {
12 background-color: #c1dcfc;
13 border: 2px solid #4499D6;
14 border-radius: 10px;
15 width: auto;
16 height: 100%;
17 }
18
19 .item-box .item {
20 height: 70px;
21 }
22
23 .line-item {
24 }
25
26 .line-left {
27 border-left: 1px solid #4499D6;
28 }
29
30 .line-center:before {
31 content: ' ';
32 display: inline-block;
33 width: 100%;
34 border-bottom: 1px solid #4499D6;
35 position: absolute;
36 top: 50%;
37 }
38
39 .line-center.arrow:after {
40 top: 36px;
41 }
42
43 .line-top:before {
44 content: ' ';
45 display: inline-block;
46 width: 100%;
47 border-bottom: 1px solid #4499D6;
48 position: absolute;
49 }
50
51 .line-top.arrow:after {
52 top: -5px;
53 }
54
55 .corner-top:before {
56 content: ' ';
57 width: 100%;
58 height: 100%;
59 border-top: 1px solid #4499D6;
60 border-left: 1px solid #4499D6;
61 border-top-left-radius: 10px;
62 display: inline-block;
63 position: absolute;
64 }
65
66 .corner-top.top-35:before {
67 top: 40px;
68 }
69
70 .corner-top.arrow:after {
71 top: -5px;
72 }
73
74 .corner-top.arrow.top-35:after {
75 top: 35px;
76 }
77
78
79
80 .corner-bottom:before {
81 content: ' ';
82 width: 100%;
83 height: 100%;
84 border-bottom: 1px solid #4499D6;
85 border-left: 1px solid #4499D6;
86 border-bottom-left-radius: 10px;
87 display: inline-block;
88 position: absolute;
89 }
90
91 .corner-bottom.top-35:before {
92 top: -40px;
93 }
94
95 .corner-bottom.arrow:after {
96 bottom: -5px;
97 }
98
99 .corner-bottom.arrow.top-35:after {
100 bottom: 35px;
101 }
102
103 .corner-top-bottom {
104 border-top: 1px solid #4499D6;
105 border-left: 1px solid #4499D6;
106 border-bottom: 1px solid #4499D6;
107 border-top-left-radius: 10px;
108 border-bottom-left-radius: 10px;
109 position: relative;
110 }
111
112 .corner-top-bottom.top-35 {
113 top: 40px;
114 }
115
116 .corner-top-bottom::before {
117 content: " ";
118 width: 0px;
119 height: 0px;
120 border-left: 10px;
121 border-right: 0px;
122 border-top: 5px;
123 border-bottom: 5px;
124 border-style: solid;
125 border-color: transparent transparent transparent #4499D6;
126 position: absolute;
127 right: -1px;
128 bottom: -5px;
129 }
130
131 .arrow::after, .corner-top-bottom::after {
132 content: " ";
133 width: 0px;
134 height: 0px;
135 border-left: 10px;
136 border-right: 0px;
137 border-top: 5px;
138 border-bottom: 5px;
139 border-style: solid;
140 border-color: transparent transparent transparent #4499D6;
141 position: absolute;
142 right: -1px;
143 }
144
145 .corner-top-bottom::after {
146 top: -5px;
147 }
148
149
150 .archtable {
151 border-spacing: 0px;
152 }
153
154
155 .archtable td {
156 position: relative;
157 }
158
159 .archtable td > div {
160 width:100%;
161 height: 100%;
162 }
163
164 .archtable tr.item-row td {
165 height: 80px;
166 }
167
168 .archtable tr:first-child,.archtable tr:last-child {
169 height: 40px;
170 }
171
172 .archtable td.td-item {
173 min-width: 140px;
174 }
175
176 .archtable td.td-line {
177 min-width: 30px;
178 }
179
180 .archtable td.td-arrow {
181 min-width: 50px;
182 }
View Code
js主要用到jquery库,css js 都写的比较毛糙,基础没打好呀,欢迎大神指正~
PS:写个博客好费时间,真是佩服那些大神写一系列的文章,给你们点赞,写了两小时我快吐了~