樹型菜單在桌面應用普遍,Web端受限於無現成組件,常用JavaScript硬編碼實現,擴展性差。本文展示如何結合PHP遞歸函數、MySQL遞歸數據結構、JavaScript交互,構建無級數限制、數據驅動的動態樹型菜單。
CREATE TABLE menu (
id TINYINT(4) NOT NULL AUTO_INCREMENT,
parent_id TINYINT(4) DEFAULT 0 NOT NULL,
name VARCHAR(20) NOT NULL,
url VARCHAR(60) DEFAULT '',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段含義:
id:主鍵parent_id:父菜單ID,頂級為0name:顯示名稱url:葉子節點鏈接,非葉子為空INSERT INTO menu VALUES
(1, 0, '人事管理', ''),
(2, 0, '通訊交流', ''),
(3, 1, '檔案管理', ''),
(4, 1, '考勤管理', 'https://localhost/personal/attendance.php'),
(5, 2, '通訊錄', ''),
(6, 2, '網絡會議', ''),
(7, 3, '新增檔案', 'https://localhost/personal/add_achive.php'),
(8, 3, '查詢檔案', 'https://localhost/personal/search_archive.php'),
(9, 3, '刪除檔案', 'https://localhost/personal/delete_archive.php'),
(10, 5, '新增通訊記錄', 'https://localhost/communication/add_address.php'),
(11, 5, '查詢通訊記錄', 'https://localhost/communication/search_address.php'),
(12, 5, '刪除通訊記錄', 'https://localhost/communication/delete_address.php'),
(13, 6, '召開會議', 'https://localhost/communication/convence_meeting.php'),
(14, 6, '會議查詢', 'https://localhost/communication/search_meeting.php');
<?php
function buildMenu($parent_id = 0, $level = 0) {
global $pdo;
$stmt = $pdo->prepare("SELECT * FROM menu WHERE parent_id = ? ORDER BY id");
$stmt->execute([$parent_id]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($items)) return '';
$html = $level === 0 ? '<ul class="tree-menu">' : '<ul style="display:none;">';
foreach ($items as $item) {
$has_children = hasChildren($item['id']);
$html .= '<li>';
if ($has_children) {
$html .= '<span class="toggle" data-menu="' . $item['id'] . '">' . htmlspecialchars($item['name']) . '</span>';
$html .= buildMenu($item['id'], $level + 1);
} else {
$html .= '<a href="' . htmlspecialchars($item['url']) . '" target="_blank">' . htmlspecialchars($item['name']) . '</a>';
}
$html .= '</li>';
}
$html .= '</ul>';
return $html;
}
function hasChildren($id) {
global $pdo;
$stmt = $pdo->prepare("SELECT 1 FROM menu WHERE parent_id = ? LIMIT 1");
$stmt->execute([$id]);
return (bool)$stmt->fetchColumn();
}
echo buildMenu();
<script>
document.addEventListener('click', function(e) {
if (e.target.matches('.toggle')) {
const menuId = e.target.dataset.menu;
const subMenu = document.querySelector(`ul[data-parent="${menuId}"]`);
if (subMenu) {
subMenu.style.display = subMenu.style.display === 'none' ? 'block' : 'none';
e.target.classList.toggle('expanded');
}
}
});
</script>
.tree-menu { list-style: none; padding-left: 20px; margin: 0; }
.tree-menu li { position: relative; }
.toggle { cursor: pointer; user-select: none; }
.toggle::before { content: '▶ '; display: inline-block; transition: transform .2s; }
.toggle.expanded::before { transform: rotate(90deg); }
.tree-menu a { text-decoration: none; color: #333; }
.tree-menu a:hover { color: #0066cc; }
| 場景 | 優化方案 |
|---|---|
| 菜單項超過百個 | 一次性查詢所有數據,PHP內存中構建樹(遞歸/引用構建),單次查詢 |
| 高併發讀取 | Redis緩存生成的HTML片段,菜單更新時失效 |
| 拖拽排序/編輯 | 前端收集順序,批量更新parent_id與sort_order字段 |
| 權限控制 | JOIN權限表過濾不可見節點,或前端根據權限標識隱藏 |
遞歸模型簡單直觀,適合中小規模菜單。超大規模或高頻變更場景可考慮嵌套集合模型或閉包表,讀寫性能各有權衡。