feat: add interactive left menu
This commit is contained in:
@@ -57,9 +57,9 @@
|
||||
| 038 | 实现预览区 shell 组件(A4 纸效果) | 前端 | 0.25d | ✅ 已完成 |
|
||||
| 039 | 实现结构树 shell 组件 | 前端 | 0.25d | ✅ 已完成 |
|
||||
| | **左侧菜单** | | | |
|
||||
| 040 | 菜单数据模型(JSON)+ 子菜单配置 | 前端 | 0.25d | ⏳ 未完成 |
|
||||
| 041 | 菜单展开/收起交互 + 箭头旋转动画 | 前端 | 0.25d | ⏳ 未完成 |
|
||||
| 042 | 菜单选中高亮(蓝色左侧竖条) | 前端 | 0.25d | ⏳ 未完成 |
|
||||
| 040 | 菜单数据模型(JSON)+ 子菜单配置 | 前端 | 0.25d | ✅ 已完成 |
|
||||
| 041 | 菜单展开/收起交互 + 箭头旋转动画 | 前端 | 0.25d | ✅ 已完成 |
|
||||
| 042 | 菜单选中高亮(蓝色左侧竖条) | 前端 | 0.25d | ✅ 已完成 |
|
||||
| | **顶部工具栏** | | | |
|
||||
| 043 | 面包屑导航渲染 | 前端 | 0.25d | ⏳ 未完成 |
|
||||
| 044 | 模板名称 + 版本号标签展示 | 前端 | 0.25d | ⏳ 未完成 |
|
||||
|
||||
@@ -72,3 +72,14 @@
|
||||
6. 新增 `StructureTree.vue`,实现结构树 shell。
|
||||
7. 增加 `/templates/:id/edit` 路由,并执行前端构建和浏览器 DOM 检查。
|
||||
- **执行结果**: 完成任务 034-039,编辑页基础四栏工作台已可访问。
|
||||
|
||||
## 会话 ID: 20260701-left-menu
|
||||
- [2026-07-01 20:35:45]
|
||||
- **执行原因**: 按任务清单继续完成左侧菜单 040-042。
|
||||
- **执行过程**:
|
||||
1. 将左侧菜单改为 JSON 数据模型驱动,包含一级菜单和子菜单配置。
|
||||
2. 增加分组展开/收起状态,点击一级菜单可切换子菜单显示。
|
||||
3. 增加箭头旋转动画,展示当前展开状态。
|
||||
4. 增加选中状态管理,子菜单选中后显示高亮样式。
|
||||
5. 执行前端构建和浏览器交互检查。
|
||||
- **执行结果**: 完成任务 040-042,左侧菜单支持数据驱动、展开收起与选中高亮。
|
||||
|
||||
@@ -1,10 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
DatabaseOutlined,
|
||||
DownOutlined,
|
||||
FileTextOutlined,
|
||||
SettingOutlined,
|
||||
} 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>
|
||||
|
||||
<template>
|
||||
@@ -15,25 +70,39 @@ import {
|
||||
</div>
|
||||
|
||||
<nav class="menu-list">
|
||||
<div class="menu-group-title">模板管理</div>
|
||||
<button class="menu-item active" type="button">
|
||||
<FileTextOutlined />
|
||||
模板标注
|
||||
</button>
|
||||
<button class="menu-item" type="button">
|
||||
<AppstoreOutlined />
|
||||
模板中心
|
||||
<div v-for="item in menuItems" :key="item.key" class="menu-group">
|
||||
<button
|
||||
class="menu-item"
|
||||
:class="{ active: activeKey === item.key }"
|
||||
type="button"
|
||||
@click="item.children ? toggleMenu(item.key) : selectItem(item.key)"
|
||||
>
|
||||
<FileTextOutlined v-if="item.icon === 'file'" />
|
||||
<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>
|
||||
<button class="menu-item" type="button">
|
||||
<DatabaseOutlined />
|
||||
数据源
|
||||
</button>
|
||||
<button class="menu-item" type="button">
|
||||
<SettingOutlined />
|
||||
系统配置
|
||||
<div v-if="item.children" class="sub-menu" :class="{ open: openKeys[item.key] }">
|
||||
<button
|
||||
v-for="child in item.children"
|
||||
:key="child.key"
|
||||
class="sub-menu-item"
|
||||
:class="{ active: activeKey === child.key }"
|
||||
type="button"
|
||||
@click="selectItem(child.key)"
|
||||
>
|
||||
<span class="sub-dot" :class="child.status"></span>
|
||||
<span>{{ child.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -71,11 +140,8 @@ import {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.menu-group-title {
|
||||
padding: 8px 16px 4px;
|
||||
color: var(--c-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
.menu-group {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
@@ -92,6 +158,12 @@ import {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-item span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
color: var(--c-text);
|
||||
background: var(--c-surface-soft);
|
||||
@@ -113,4 +185,63 @@ import {
|
||||
background: var(--c-primary);
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user