傳統遞歸輸出無限級分類多為縮進列表或下拉選單,難以直觀展示層級歸屬關系。TreeTable 通過 HTML 表格的 rowspan 和 colspan 將樹狀結構渲染為類似文件夾樹的表格,清晰展示父子層級。
輸入數組必須包含 id、parentid、name 三字段:
$data = [
['id'=>1, 'parentid'=>0, 'name'=>'一級欄目一'],
['id'=>2, 'parentid'=>0, 'name'=>'一級欄目二'],
['id'=>3, 'parentid'=>1, 'name'=>'二級欄目一'],
['id'=>4, 'parentid'=>1, 'name'=>'二級欄目二'],
['id'=>5, 'parentid'=>2, 'name'=>'二級欄目三'],
['id'=>6, 'parentid'=>3, 'name'=>'三級欄目一'],
['id'=>7, 'parentid'=>3, 'name'=>'三級欄目二'],
];
<?php
class TreeTable {
public $arr = []; // 處理後的節點數組
public $columns = 0; // 最大層級數(列數)
public $rows = 0; // 總行數(葉子節點數)
public function init(array $data) {
// 1. 建立 ID 索引
foreach ($data as $v) $this->arr[$v['id']] = $v;
// 2. 計算每個節點屬性
foreach ($this->arr as $k => $v) {
$this->arr[$k]['column'] = $this->getLevel($v['id']); // 所在列(層級)
$this->arr[$k]['arrchildid'] = $this->getArrChildId($v['id']); // 所有子孫 ID
$this->arr[$k]['arrparentid'] = $this->getArrParentId($v['id']); // 所有祖先 ID
$this->arr[$k]['child_bottom_num'] = $this->getChildCount($v['id']); // 葉子節點數
}
$this->columns = $this->getColumns();
$this->rows = $this->getRows();
$this->sortArr(); // 按祖先路徑排序
$this->calcRowColSpan(); // 計算 rowspan/colspan
return $this->arr;
}
// 遞歸獲取層級
private function getLevel($id, $init = true) {
static $level = 1;
if ($init) $level = 1;
if ($this->arr[$id]['parentid']) {
$level++;
$this->getLevel($this->arr[$id]['parentid'], false);
}
return $level;
}
// 遞歸獲取所有葉子節點數(用於 rowspan)
private function getChildCount($id, $init = true) {
static $count = 0;
if ($init) $count = 0;
$children = $this->getChild($id);
if ($children) {
foreach ($children as $c) $this->getChildCount($c['id'], false);
} else {
$count++;
}
return $count;
}
// 獲取直接子節點
private function getChild($id) {
$res = [];
foreach ($this->arr as $v) if ($v['parentid'] == $id) $res[] = $v;
return $res;
}
// 獲取所有子孫 ID 字符串
private function getArrChildId($id, $init = true) {
static $ids = '';
if ($init) $ids = '';
foreach ($this->arr as $v) {
if ($v['parentid'] == $id) {
$ids .= ($ids ? ',' : '') . $v['id'];
$this->getArrChildId($v['id'], false);
}
}
return $ids;
}
// 獲取所有祖先 ID 字符串
private function getArrParentId($id, $str = '') {
$pid = $this->arr[$id]['parentid'];
if ($pid > 0) {
$str = $str ? $pid . ',' . $str : (string)$pid;
$str = $this->getArrParentId($pid, $str);
}
return $str;
}
// 排序:按祖先路徑字符串、ID 排序,保證樹狀順序
private function sortArr() {
$pidArr = $idArr = [];
foreach ($this->arr as $k => $v) {
$pidArr[$k] = $v['arrparentid'];
$idArr[$k] = $v['id'];
}
array_multisort($pidArr, SORT_ASC, SORT_STRING, $idArr, SORT_ASC, SORT_NUMERIC, $this->arr);
// 重新索引
$new = [];
foreach ($this->arr as $v) $new[$v['id']] = $v;
$this->arr = $new;
}
// 計算行位置、rowspan、colspan
private function calcRowColSpan() {
foreach ($this->arr as $k => $v) {
$this->arr[$k]['row'] = $this->getRowLocation($v['id']);
$this->arr[$k]['rowspan'] = $v['child_bottom_num'];
$this->arr[$k]['colspan'] = $v['child_bottom_num'] == 0 ? $this->columns - $v['column'] + 1 : 0;
}
}
// 計算行位置(核心算法)
private function getRowLocation($id) {
$node = $this->arr[$id];
$count = 0;
// 1. 所有祖先同層中,層級序號小於當前祖先且為葉子的節點
$ancestors = explode(',', $node['arrparentid']);
foreach ($ancestors as $pid) {
foreach ($this->arr as $n) {
if ($n['column'] == $this->arr[$pid]['column'] &&
$this->arr[$pid]['column_level'] > $n['column_level'] &&
$n['child_bottom_num'] == 0) $count++;
}
}
// 2. 當前層級中,序號小於當前節點的葉子節點數
foreach ($this->arr as $n) {
if ($n['column'] == $node['column'] &&
$n['column_level'] < $node['column_level']) {
$count += $n['child_bottom_num'] ?: 1;
}
}
return $count + 1;
}
// 生成表格 HTML(不含 <table> 標簽)
public function getTreeTable() {
$html = '';
for ($r = 1; $r <= $this->rows; $r++) {
$html .= "\n<tr>";
foreach ($this->arr as $v) {
if ($v['row'] == $r) {
$rs = $v['rowspan'] ? " rowspan='{$v['rowspan']}'" : '';
$cs = $v['colspan'] ? " colspan='{$v['colspan']}'" : '';
$html .= "\n<td$rs$cs>{$v['name']}</td>";
}
}
$html .= "\n</tr>";
}
return $html;
}
}
?>
<?php
require 'TreeTable.class.php';
$data = [ /* 上述數據 */ ];
$tree = new TreeTable();
$tree->init($data);
?>
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">
<?php echo $tree->getTreeTable(); ?>
</table>
rowspan 跨越其所有葉子後代行。colspan 跨越剩餘列,填滿行尾。