248 lines
4.8 KiB
Vue
248 lines
4.8 KiB
Vue
<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>
|
|
<aside class="left-menu">
|
|
<div class="brand">
|
|
<div class="brand-mark">AI</div>
|
|
<span>AI 文档模板系统</span>
|
|
</div>
|
|
|
|
<nav class="menu-list">
|
|
<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 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>
|
|
|
|
<style scoped>
|
|
.left-menu {
|
|
min-width: 0;
|
|
border-right: 1px solid var(--c-border);
|
|
background: var(--c-surface);
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
height: 52px;
|
|
padding: 0 16px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.brand-mark {
|
|
display: grid;
|
|
width: 26px;
|
|
height: 26px;
|
|
place-items: center;
|
|
border-radius: 5px;
|
|
color: #fff;
|
|
background: var(--c-primary);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.menu-list {
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.menu-group {
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.menu-item {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
gap: 8px;
|
|
padding: 8px 16px;
|
|
border: 0;
|
|
color: var(--c-text-secondary);
|
|
background: transparent;
|
|
cursor: pointer;
|
|
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);
|
|
}
|
|
|
|
.menu-item.active {
|
|
color: var(--c-primary);
|
|
background: var(--c-primary-soft);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.menu-item.active::before {
|
|
position: absolute;
|
|
top: 4px;
|
|
bottom: 4px;
|
|
left: 0;
|
|
width: 3px;
|
|
border-radius: 0 2px 2px 0;
|
|
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>
|