<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">交互式菜单脚本 · 可视化演示</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}
body {
background: #f4f6f9;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.card {
max-width: 600px;
width: 100%;
background: #ffffff;
border-radius: 24px;
box-shadow: 0 20px 40px rgba(0,0,0,0.08), 0 8px 16px rgba(0,0,0,0.04);
padding: 28px 24px;
transition: all 0.2s ease;
}
h1 {
font-size: 1.7rem;
font-weight: 600;
margin-top: 0;
margin-bottom: 0.3rem;
color: #1e293b;
display: flex;
align-items: center;
gap: 8px;
}
.subhead {
color: #64748b;
font-size: 0.95rem;
margin-bottom: 1.5rem;
padding-bottom: 0.8rem;
border-bottom: 1px solid #e9edf2;
display: flex;
justify-content: space-between;
align-items: center;
}
.menu-container {
background: #fafbfc;
border-radius: 16px;
padding: 12px 8px;
border: 1px solid #edf0f5;
}
.menu-item {
display: flex;
flex-direction: column;
border-radius: 10px;
transition: background 0.1s ease;
}
.menu-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-radius: 8px;
transition: background 0.1s ease;
user-select: none;
}
.menu-row:hover {
background: #eef2f8;
}
.menu-row:active {
background: #e2e8f0;
}
.expand-icon {
font-size: 0.8rem;
width: 20px;
text-align: center;
color: #64748b;
flex-shrink: 0;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.expand-icon.leaf {
opacity: 0.4;
transform: none;
}
.menu-label {
flex: 1;
font-weight: 500;
color: #0f172a;
letter-spacing: 0.01em;
}
.badge {
background: #e2e8f0;
color: #334155;
font-size: 0.7rem;
font-weight: 600;
padding: 2px 10px;
border-radius: 30px;
letter-spacing: 0.3px;
}
.children-wrapper {
display: flex;
flex-direction: column;
margin-left: 28px;
padding-left: 12px;
border-left: 2px solid #dce2ec;
}
.action-bar {
margin-top: 24px;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.btn {
background: white;
border: 1px solid #d1d9e6;
padding: 8px 20px;
border-radius: 40px;
font-weight: 500;
font-size: 0.9rem;
color: #1e293b;
cursor: pointer;
transition: 0.15s ease;
display: inline-flex;
align-items: center;
gap: 6px;
box-shadow: 0 1px 2px rgba(0,0,0,0.02);
flex: 1 0 auto;
justify-content: center;
}
.btn-primary {
background: #1e293b;
border: 1px solid #1e293b;
color: white;
}
.btn-primary:hover {
background: #0f172a;
border-color: #0f172a;
box-shadow: 0 4px 8px rgba(15, 23, 42, 0.15);
}
.btn-outline {
background: white;
}
.btn-outline:hover {
background: #f1f5f9;
border-color: #b9c5d6;
}
.btn:active {
transform: scale(0.96);
}
.status-area {
margin-top: 20px;
background: #f1f5f9;
border-radius: 40px;
padding: 10px 18px;
font-size: 0.85rem;
color: #1e293b;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
border: 1px solid #e2e8f0;
}
.status-icon {
font-size: 1.1rem;
}
#statusText {
font-weight: 500;
font-family: 'SF Mono', 'Fira Code', monospace;
background: #e2e8f0;
padding: 2px 12px;
border-radius: 40px;
color: #0f172a;
word-break: break-word;
}
.empty-hint {
color: #94a3b8;
font-style: italic;
padding: 6px 0 6px 16px;
font-size: 0.85rem;
}
/* 折叠状态 */
.children-wrapper.collapsed {
display: none;
}
.rotate-icon {
transform: rotate(90deg);
}
.footer-note {
color: #94a3b8;
font-size: 0.75rem;
text-align: center;
margin-top: 20px;
border-top: 1px solid #edf0f5;
padding-top: 16px;
}
@media (max-width: 480px) {
.card { padding: 20px 16px; }
.btn { padding: 6px 14px; font-size: 0.8rem; }
}
</style>
</head>
<body>
<div class="card">
<h1>
<span>📂 菜单脚本</span>
<span style="font-size:0.9rem; font-weight:400; color:#64748b; margin-left:auto;">交互式</span>
</h1>
<div class="subhead">
<span>点击展开 / 折叠</span>
<span style="display:flex; gap:4px;">🔽 嵌套 · 实时</span>
</div>
<!-- 菜单渲染容器 -->
<div id="menuRoot" class="menu-container">
<!-- 动态由 js 生成 -->
</div>
<!-- 操作按钮组 -->
<div class="action-bar">
<button class="btn btn-outline" id="expandAllBtn" title="展开所有节点">➕ 全部展开</button>
<button class="btn btn-outline" id="collapseAllBtn" title="折叠所有节点">➖ 全部折叠</button>
<button class="btn btn-primary" id="getStateBtn">📋 获取状态</button>
</div>
<!-- 状态显示 -->
<div class="status-area">
<span class="status-icon">📌</span>
<span>当前选中:</span>
<span id="statusText">(暂无)</span>
</div>
<div class="footer-note">
💡 点击菜单行 · 递归组件 / 动态状态读取
</div>
</div>
<script>
(function() {
// ----- 定义菜单数据结构 (嵌套) -----
const menuData = [
{
label: '📄 文件',
children: [
{
label: '📁 新建',
children: [
{ label: '📝 文本文档', children: [] },
{ label: '📊 电子表格', children: [] },
{ label: '📽️ 演示文稿', children: [] }
]
},
{
label: '📂 打开',
children: [
{ label: '🖼️ 图片浏览器', children: [] },
{ label: '📖 最近文件', children: [
{ label: 'report_final.pdf', children: [] },
{ label: 'notes.md', children: [] }
]}
]
},
{ label: '💾 保存', children: [] },
{
label: '⚙️ 导出',
children: [
{ label: '🌐 HTML', children: [] },
{ label: '📄 PDF', children: [] }
]
}
]
},
{
label: '✏️ 编辑',
children: [
{ label: '↩️ 撤销', children: [] },
{ label: '↪️ 重做', children: [] },
{
label: '✂️ 剪切', children: []
},
{
label: '📋 剪贴板',
children: [
{ label: '📑 复制', children: [] },
{ label: '📌 粘贴', children: [] },
{ label: '🔪 剪下', children: [] }
]
}
]
},
{
label: '👁️ 视图',
children: [
{
label: '🔍 缩放',
children: [
{ label: '🔎 放大', children: [] },
{ label: '🔍 缩小', children: [] },
{ label: '🔄 重置', children: [] }
]
},
{ label: '📏 标尺', children: [] },
{ label: '🧩 侧边栏', children: [] }
]
},
{
label: '❓ 帮助',
children: [
{ label: '📖 文档', children: [] },
{ label: '💬 #39;, children: [] }
]
}
];
// ----- 渲染函数 (递归) -----
function renderMenu(menuItems, parentElement, depth = 0) {
menuItems.forEach(item => {
const itemDiv = document.createElement('div');
itemDiv.className = 'menu-item';
// 行容器 (点击交互)
const row = document.createElement('div');
row.className = 'menu-row';
// 展开图标
const iconSpan = document.createElement('span');
iconSpan.className = 'expand-icon';
const hasChildren = item.children && item.children.length > 0;
if (hasChildren) {
iconSpan.textContent = '▶'; // 右三角 (折叠状态)
iconSpan.classList.add('expandable');
} else {
iconSpan.textContent = '·';
iconSpan.classList.add('leaf');
}
row.appendChild(iconSpan);
// 标签
const labelSpan = document.createElement('span');
labelSpan.className = 'menu-label';
labelSpan.textContent = item.label;
row.appendChild(labelSpan);
// 如果有子节点,加一个小 badge 数量
if (hasChildren) {
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = `${item.children.length} 项`;
row.appendChild(badge);
}
itemDiv.appendChild(row);
// 子节点容器 ( children-wrapper )
const childrenWrapper = document.createElement('div');
childrenWrapper.className = 'children-wrapper collapsed'; // 默认折叠
if (hasChildren) {
renderMenu(item.children, childrenWrapper, depth + 1);
} else {
// 没有子节点,可以留空或加占位
// 保持干净
}
itemDiv.appendChild(childrenWrapper);
// ----- 点击事件 (展开/折叠 + 状态更新) -----
row.addEventListener('click', function(e) {
e.stopPropagation(); // 防止向上冒泡触发父级的点击
// 1. 切换展开/折叠 (仅当有子节点)
if (hasChildren) {
const isCollapsed = childrenWrapper.classList.contains('collapsed');
if (isCollapsed) {
childrenWrapper.classList.remove('collapsed');
iconSpan.classList.add('rotate-icon'); // 旋转箭头
} else {
childrenWrapper.classList.add('collapsed');
iconSpan.classList.remove('rotate-icon');
}
}
// 2. 更新状态栏: 显示当前点击的菜单项
const statusText = document.getElementById('statusText');
if (statusText) {
statusText.textContent = `${item.label}`;
}
});
parentElement.appendChild(itemDiv);
});
}
// ----- 初始化渲染 -----
const menuRoot = document.getElementById('menuRoot');
menuRoot.innerHTML = '';
renderMenu(menuData, menuRoot, 0);
// ----- 工具函数:获取所有可折叠容器 (children-wrapper) 和图标 -----
function getAllTogglers() {
const wrappers = Array.from(document.querySelectorAll('#menuRoot .children-wrapper'));
const icons = Array.from(document.querySelectorAll('#menuRoot .expand-icon.expandable'));
return { wrappers, icons };
}
// ----- 全部展开 -----
document.getElementById('expandAllBtn').addEventListener('click', function() {
const { wrappers, icons } = getAllTogglers();
wrappers.forEach(w => w.classList.remove('collapsed'));
icons.forEach(icon => icon.classList.add('rotate-icon'));
// 更新状态 (不改变选中文本)
const statusText = document.getElementById('statusText');
if (statusText && !statusText.textContent.startsWith('全部')) {
statusText.textContent = '🔽 全部展开';
} else {
statusText.textContent = '🔽 全部展开';
}
});
// ----- 全部折叠 -----
document.getElementById('collapseAllBtn').addEventListener('click', function() {
const { wrappers, icons } = getAllTogglers();
wrappers.forEach(w => w.classList.add('collapsed'));
icons.forEach(icon => icon.classList.remove('rotate-icon'));
const statusText = document.getElementById('statusText');
statusText.textContent = '🔼 全部折叠';
});
// ----- 获取状态:读取所有菜单项的展开/折叠状态 -----
document.getElementById('getStateBtn').addEventListener('click', function() {
const menuItems = document.querySelectorAll('#menuRoot .menu-item');
const stateLines = [];
let idCounter = 0;
// 递归遍历实际DOM来获取结构和状态 (与数据同步)
function walkDOM(itemElement, depth = 0) {
const row = itemElement.querySelector(':scope > .menu-row');
const wrapper = itemElement.querySelector(':scope > .children-wrapper');
if (!row) return;
const label = row.querySelector('.menu-label')?.textContent || '未知';
const hasChildren = wrapper && wrapper.querySelector('.menu-item');
const isExpanded = wrapper ? !wrapper.classList.contains('collapsed') : false;
// 缩进
const indent = ' '.repeat(depth);
const expandStatus = hasChildren ? (isExpanded ? '📂 展开' : '📁 折叠') : '▪️ 叶子';
stateLines.push(`${indent}- ${label} [${expandStatus}]`);
// 递归子项
if (hasChildren && wrapper) {
const childItems = wrapper.querySelectorAll(':scope > .menu-item');
childItems.forEach(child => walkDOM(child, depth + 1));
}
}
const topItems = document.querySelectorAll('#menuRoot > .menu-item');
topItems.forEach(item => walkDOM(item, 0));
const statusText = document.getElementById('statusText');
if (stateLines.length > 0) {
// 显示概要 + 前几行
const summary = `📋 共 ${stateLines.length} 项菜单状态`;
// 展示前3行,并用...表示折叠(为了界面清爽)
let detail = stateLines.slice(0, 4).join(' · ');
if (stateLines.length > 4) detail += ' …';
statusText.textContent = `${summary} : ${detail}`;
} else {
statusText.textContent = '📭 未检测到菜单项';
}
});
// 点击空白处重置状态 (可选)
document.addEventListener('click', function(e) {
// 如果点击的不是菜单行或者按钮,可以选择不清空,但不做额外操作
// 保持状态为上一次点击的菜单项
});
// 设置初始状态: 默认全折叠,但为了展示交互,我们展开第一个顶级菜单“文件”作为示例
// 这样用户一打开就能看到交互效果
(function setInitialDemoState() {
const topItems = document.querySelectorAll('#menuRoot > .menu-item');
if (topItems.length > 0) {
// 展开第一个“文件”菜单及其直接子项 (仅演示)
const firstItem = topItems[0];
const row = firstItem.querySelector('.menu-row');
if (row) {
// 模拟点击第一个文件菜单
row.click(); // 触发点击,展开文件,并更新状态
}
// 再展开文件下的“新建”子项 (手动操作)
const fileChildrenWrapper = firstItem.querySelector('.children-wrapper');
if (fileChildrenWrapper) {
const newItem = fileChildrenWrapper.querySelector('.menu-item');
if (newItem) {
const newRow = newItem.querySelector('.menu-row');
if (newRow) {
setTimeout(() => {
newRow.click();
}, 60);
}
}
}
// 状态文字改成 “文件”
const statusText = document.getElementById('statusText');
if (statusText) {
statusText.textContent = '📄 文件';
}
}
})();
})();
</script>
</body>
</html>
菜单交互与状态管理
这个脚本的核心是让您能像操作真实菜单一样,直观地查看和管理嵌套结构,您可以轻松展开/折叠分支,并随时获取当前菜单的完整状态。

- 展开与折叠:点击任意带有子菜单的菜单项(右侧带有“▶”图标和数量标记),即可展开或收起其子菜单,展开时图标会旋转,视觉反馈清晰。
- 状态查看与操作:页面底部的状态栏会实时显示您最后点击的菜单项,您还可以使用“全部展开”和“全部折叠”按钮快速控制整体视图,或通过“获取状态”按钮查看当前所有菜单项的展开/折叠情况。
- 数据驱动渲染由 JavaScript 中的
menuData数组定义,您可以直接修改此数据结构来增删或调整菜单项,页面会自动重新渲染,便于测试和扩展。