樹狀目錄是檔案管理、組織架構、分類導航的基礎 UI。本文示範從資料庫讀取扁平資料,構建樹狀陣列並輸出 HTML 巢狀清單,支援無限層級與前端互動。
CREATE TABLE categories (
id INT PRIMARY KEY AUTO_INCREMENT,
parent_id INT DEFAULT 0 COMMENT '父節點ID,0為根節點',
name VARCHAR(100) NOT NULL,
sort_order INT DEFAULT 0,
is_active TINYINT DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
<?php
function buildTree(array $items, int $parentId = 0): array {
$tree = [];
foreach ($items as $item) {
if ($item['parent_id'] == $parentId) {
$children = buildTree($items, $item['id']);
if ($children) {
$item['children'] = $children;
}
$tree[] = $item;
}
}
return $tree;
}
// 使用範例
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$stmt = $pdo->query('SELECT * FROM categories WHERE is_active = 1 ORDER BY sort_order, id');
$flatData = $stmt->fetchAll(PDO::FETCH_ASSOC);
$tree = buildTree($flatData);
// $tree 現為巢狀陣列,可直接 json_encode 輸出給前端
?>
<?php
function renderTree(array $tree, int $level = 0): string {
if (empty($tree)) return '';
$html = '';
foreach ($tree as $node) {
$hasChildren = !empty($node['children']);
$toggleClass = $hasChildren ? 'has-children' : '';
$expanded = $level < 2 ? 'expanded' : ''; // 前兩層預設展開
$html .= '';
}
$html .= '
';
return $html;
}
// 輸出
echo renderTree($tree);
?>
<style>
.tree-level-0 { margin: 0; padding-left: 0; list-style: none; }
.tree-level-0 > li { margin: 2px 0; }
.tree-node { display: flex; align-items: center; gap: 4px; padding: 4px 8px; cursor: pointer; }
.tree-node:hover { background: #f0f0f0; border-radius: 3px; }
.tree-toggle { width: 16px; height: 16px; border: none; background: none; cursor: pointer; font-size: 10px; }
.tree-spacer { width: 16px; }
.tree-label { flex: 1; }
.has-children > ul { display: block; }
.has-children.collapsed > ul { display: none; }
.has-children.collapsed .tree-toggle::before { content: '▶'; }
.has-children.expanded .tree-toggle::before { content: '▼'; }
<script>
document.addEventListener('click', e => {
const toggle = e.target.closest('.tree-toggle');
if (toggle) {
const li = toggle.closest('li.has-children');
li.classList.toggle('collapsed');
li.classList.toggle('expanded');
toggle.setAttribute('aria-expanded', li.classList.contains('expanded'));
}
});
<?php
// 僅載入前 2 層,其餘透過 AJAX 載入
function buildTreePartial(array $items, int $parentId = 0, int $maxDepth = 2, int $currentDepth = 0): array {
if ($currentDepth >= $maxDepth) return [];
$tree = [];
foreach ($items as $item) {
if ($item['parent_id'] == $parentId) {
$children = buildTreePartial($items, $item['id'], $maxDepth, $currentDepth + 1);
$item['has_children'] = (bool)($children || hasChildrenInDb($item['id']));
if ($children) $item['children'] = $children;
$tree[] = $item;
}
}
return $tree;
}
function hasChildrenInDb(int $parentId): bool {
global $pdo;
$stmt = $pdo->prepare('SELECT 1 FROM categories WHERE parent_id = ? AND is_active = 1 LIMIT 1');
$stmt->execute([$parentId]);
return (bool)$stmt->fetchColumn();
}
?>
<script>
async function loadChildren(nodeId, liElement) {
if (liElement.dataset.loaded === 'true') return;
const response = await fetch(`/api/categories/children?parent_id=${nodeId}`);
const children = await response.json();
const ul = document.createElement('ul');
ul.className = 'tree-level-dynamic';
ul.innerHTML = renderTreeAjax(children); // 類似 renderTree 但輸出字串
liElement.appendChild(ul);
liElement.dataset.loaded = 'true';
}
function renderTreeAjax(children) { /* 同 renderTree 邏輯 */ }
-- 核心查詢索引
CREATE INDEX idx_parent_active ON categories(parent_id, is_active);
CREATE INDEX idx_sort ON categories(parent_id, sort_order, id);
-- 遞迴 CTE 查詢 (MySQL 8.0+) 亦受益於上述索引
樹狀目錄核心在於「扁平資料 ⇄ 樹狀結構」雙向轉換,PHP 端負責構建、前端負責渲染與互動,大量資料採用延遲載入平衡效能與體驗。