feat: add interactive left menu

This commit is contained in:
zwt13703
2026-07-01 20:36:04 +08:00
parent b4e128c227
commit ffa869e086
3 changed files with 168 additions and 26 deletions
@@ -57,9 +57,9 @@
| 038 | 实现预览区 shell 组件(A4 纸效果) | 前端 | 0.25d | ✅ 已完成 | | 038 | 实现预览区 shell 组件(A4 纸效果) | 前端 | 0.25d | ✅ 已完成 |
| 039 | 实现结构树 shell 组件 | 前端 | 0.25d | ✅ 已完成 | | 039 | 实现结构树 shell 组件 | 前端 | 0.25d | ✅ 已完成 |
| | **左侧菜单** | | | | | | **左侧菜单** | | | |
| 040 | 菜单数据模型(JSON+ 子菜单配置 | 前端 | 0.25d | ⏳ 未完成 | | 040 | 菜单数据模型(JSON+ 子菜单配置 | 前端 | 0.25d | ✅ 已完成 |
| 041 | 菜单展开/收起交互 + 箭头旋转动画 | 前端 | 0.25d | ⏳ 未完成 | | 041 | 菜单展开/收起交互 + 箭头旋转动画 | 前端 | 0.25d | ✅ 已完成 |
| 042 | 菜单选中高亮(蓝色左侧竖条) | 前端 | 0.25d | ⏳ 未完成 | | 042 | 菜单选中高亮(蓝色左侧竖条) | 前端 | 0.25d | ✅ 已完成 |
| | **顶部工具栏** | | | | | | **顶部工具栏** | | | |
| 043 | 面包屑导航渲染 | 前端 | 0.25d | ⏳ 未完成 | | 043 | 面包屑导航渲染 | 前端 | 0.25d | ⏳ 未完成 |
| 044 | 模板名称 + 版本号标签展示 | 前端 | 0.25d | ⏳ 未完成 | | 044 | 模板名称 + 版本号标签展示 | 前端 | 0.25d | ⏳ 未完成 |
+11
View File
@@ -72,3 +72,14 @@
6. 新增 `StructureTree.vue`,实现结构树 shell。 6. 新增 `StructureTree.vue`,实现结构树 shell。
7. 增加 `/templates/:id/edit` 路由,并执行前端构建和浏览器 DOM 检查。 7. 增加 `/templates/:id/edit` 路由,并执行前端构建和浏览器 DOM 检查。
- **执行结果**: 完成任务 034-039,编辑页基础四栏工作台已可访问。 - **执行结果**: 完成任务 034-039,编辑页基础四栏工作台已可访问。
## 会话 ID: 20260701-left-menu
- [2026-07-01 20:35:45]
- **执行原因**: 按任务清单继续完成左侧菜单 040-042。
- **执行过程**:
1. 将左侧菜单改为 JSON 数据模型驱动,包含一级菜单和子菜单配置。
2. 增加分组展开/收起状态,点击一级菜单可切换子菜单显示。
3. 增加箭头旋转动画,展示当前展开状态。
4. 增加选中状态管理,子菜单选中后显示高亮样式。
5. 执行前端构建和浏览器交互检查。
- **执行结果**: 完成任务 040-042,左侧菜单支持数据驱动、展开收起与选中高亮。
+154 -23
View File
@@ -1,10 +1,65 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from "vue";
import { import {
AppstoreOutlined, AppstoreOutlined,
DatabaseOutlined, DatabaseOutlined,
DownOutlined,
FileTextOutlined, FileTextOutlined,
SettingOutlined, SettingOutlined,
} from "@ant-design/icons-vue"; } from "@ant-design/icons-vue";
type MenuChild = {
key: string;
label: string;
status?: "done" | "empty";
};
type MenuItem = {
key: string;
label: string;
icon: "file" | "app" | "database" | "setting";
children?: MenuChild[];
};
const menuItems: MenuItem[] = [
{
key: "template",
label: "模板管理",
icon: "file",
children: [
{ key: "template-edit", label: "模板标注", status: "done" },
{ key: "template-center", label: "模板中心", status: "empty" },
],
},
{
key: "assets",
label: "资料与配置",
icon: "database",
children: [
{ key: "data-source", label: "数据源", status: "empty" },
{ key: "system-config", label: "系统配置", status: "empty" },
],
},
{
key: "workspace",
label: "工作台",
icon: "app",
},
];
const openKeys = reactive<Record<string, boolean>>({
template: true,
assets: true,
});
const activeKey = ref("template-edit");
function toggleMenu(key: string) {
openKeys[key] = !openKeys[key];
}
function selectItem(key: string) {
activeKey.value = key;
}
</script> </script>
<template> <template>
@@ -15,25 +70,39 @@ import {
</div> </div>
<nav class="menu-list"> <nav class="menu-list">
<div class="menu-group-title">模板管理</div> <div v-for="item in menuItems" :key="item.key" class="menu-group">
<button class="menu-item active" type="button"> <button
<FileTextOutlined /> class="menu-item"
模板标注 :class="{ active: activeKey === item.key }"
</button> type="button"
<button class="menu-item" type="button"> @click="item.children ? toggleMenu(item.key) : selectItem(item.key)"
<AppstoreOutlined /> >
模板中心 <FileTextOutlined v-if="item.icon === 'file'" />
</button> <AppstoreOutlined v-else-if="item.icon === 'app'" />
<DatabaseOutlined v-else-if="item.icon === 'database'" />
<SettingOutlined v-else />
<span>{{ item.label }}</span>
<DownOutlined
v-if="item.children"
class="menu-arrow"
:class="{ open: openKeys[item.key] }"
/>
</button>
<div class="menu-group-title">资料与配置</div> <div v-if="item.children" class="sub-menu" :class="{ open: openKeys[item.key] }">
<button class="menu-item" type="button"> <button
<DatabaseOutlined /> v-for="child in item.children"
数据源 :key="child.key"
</button> class="sub-menu-item"
<button class="menu-item" type="button"> :class="{ active: activeKey === child.key }"
<SettingOutlined /> type="button"
系统配置 @click="selectItem(child.key)"
</button> >
<span class="sub-dot" :class="child.status"></span>
<span>{{ child.label }}</span>
</button>
</div>
</div>
</nav> </nav>
</aside> </aside>
</template> </template>
@@ -71,11 +140,8 @@ import {
padding: 8px 0; padding: 8px 0;
} }
.menu-group-title { .menu-group {
padding: 8px 16px 4px; margin-bottom: 2px;
color: var(--c-text-muted);
font-size: 11px;
font-weight: 500;
} }
.menu-item { .menu-item {
@@ -92,6 +158,12 @@ import {
text-align: left; text-align: left;
} }
.menu-item span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.menu-item:hover { .menu-item:hover {
color: var(--c-text); color: var(--c-text);
background: var(--c-surface-soft); background: var(--c-surface-soft);
@@ -113,4 +185,63 @@ import {
background: var(--c-primary); background: var(--c-primary);
content: ""; content: "";
} }
.menu-arrow {
margin-left: auto;
color: var(--c-text-muted);
font-size: 12px;
transition: transform 0.18s ease;
}
.menu-arrow.open {
transform: rotate(180deg);
}
.sub-menu {
max-height: 0;
overflow: hidden;
transition: max-height 0.22s ease;
}
.sub-menu.open {
max-height: 220px;
}
.sub-menu-item {
display: flex;
align-items: center;
width: 100%;
gap: 8px;
padding: 6px 16px 6px 32px;
border: 0;
color: var(--c-text-muted);
background: transparent;
cursor: pointer;
text-align: left;
}
.sub-menu-item:hover {
color: var(--c-text);
background: var(--c-surface-soft);
}
.sub-menu-item.active {
color: var(--c-primary);
font-weight: 500;
}
.sub-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--c-text-muted);
}
.sub-dot.done {
background: var(--c-success);
}
.sub-dot.empty {
background: var(--c-border);
}
</style> </style>