feat: add interactive left menu
This commit is contained in:
@@ -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 />
|
||||
模板中心
|
||||
</button>
|
||||
<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 />
|
||||
系统配置
|
||||
</button>
|
||||
<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