feat: add interactive left menu
This commit is contained in:
@@ -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 | ⏳ 未完成 |
|
||||||
|
|||||||
@@ -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,左侧菜单支持数据驱动、展开收起与选中高亮。
|
||||||
|
|||||||
@@ -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'" />
|
||||||
|
<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>
|
</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)"
|
||||||
|
>
|
||||||
|
<span class="sub-dot" :class="child.status"></span>
|
||||||
|
<span>{{ child.label }}</span>
|
||||||
</button>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user