公司項目有個功能要實現樹形結構的展示,類似於組織架構的那種。
想了下幾種方案,
- 表格內實現
- 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 }
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 }
js主要用到jquery庫,css js 都寫的比較毛糙,基礎沒打好呀,歡迎大神指正~
PS:寫個博客好費時間,真是佩服那些大神寫一系列的文章,給你們點贊,寫了兩小時我快吐了~