0d0aa20be2
The logo block stopped wherever its text happened to end (~140px) while the rail below it is 208px, so the left edge was a step rather than a column. Both now read `asideWidth()` from src/layout.ts, so they cannot drift apart, and collapsing the rail narrows the logo block with it — the wordmark is dropped and the mark centres in the 56px strip. The block also carries the rail's separator up through the header, so the column reads as one piece. The header's own 20px padding had to go for this to work: the logo block has to start at x=0 to be flush with the rail, so the nav and the right-hand mark now own their insets. The mark's inset comes from the same `ASIDE_INSET` as the rail's section title, so the two line up. A side effect worth keeping: the rail is 208px and the top nav's first item and .el-main are each inset a further 20px by their own Element Plus defaults, so the nav text, the page body and the page header all start on one vertical line. The widths live in a module rather than CSS custom properties because el-aside takes its width through a prop and would otherwise fight an inline style.
160 lines
4.0 KiB
Vue
160 lines
4.0 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* The second-level menu, pinned to the left of the content area.
|
|
*
|
|
* It is driven entirely by `route.meta.section`, so the menu a URL belongs to
|
|
* is a property of the route table rather than of component state — deep
|
|
* linking to `/templates/fields` shows the right menu on first paint, and the
|
|
* aside disappears on routes that declare no section (the welcome page).
|
|
*/
|
|
import { computed, type Component } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { Document, Expand, Files, Fold, Setting } from '@element-plus/icons-vue'
|
|
|
|
import { ASIDE_INSET, asideWidth } from '@/layout'
|
|
import { useAppStore } from '@/stores/app'
|
|
|
|
interface AsideItem {
|
|
index: string
|
|
label: string
|
|
icon: Component
|
|
hint?: string
|
|
}
|
|
|
|
interface AsideMenu {
|
|
title: string
|
|
items: AsideItem[]
|
|
}
|
|
|
|
const props = defineProps<{ section: string }>()
|
|
|
|
const route = useRoute()
|
|
const appStore = useAppStore()
|
|
|
|
const MENUS: Record<string, AsideMenu> = {
|
|
papers: {
|
|
title: '论文',
|
|
items: [{ index: '/papers', label: '我的论文', icon: Document, hint: '待定' }],
|
|
},
|
|
templates: {
|
|
title: '模板配置',
|
|
items: [
|
|
{ index: '/templates/list', label: '模板列表', icon: Files },
|
|
{ index: '/templates/fields', label: '字段管理', icon: Setting },
|
|
],
|
|
},
|
|
}
|
|
|
|
const menu = computed<AsideMenu | null>(() => MENUS[props.section] ?? null)
|
|
const collapsed = computed(() => appStore.asideCollapsed)
|
|
|
|
/**
|
|
* The rail's width, from the one place the header's logo block reads too, so
|
|
* the two cannot drift apart.
|
|
*/
|
|
const width = computed(() => asideWidth(collapsed.value))
|
|
const inset = ASIDE_INSET
|
|
</script>
|
|
|
|
<template>
|
|
<el-aside
|
|
v-if="menu"
|
|
class="app-aside"
|
|
:width="width"
|
|
:style="{ '--aside-inset': inset }"
|
|
>
|
|
<div class="aside-head" :class="{ 'is-collapsed': collapsed }">
|
|
<span v-if="!collapsed" class="aside-title">{{ menu.title }}</span>
|
|
<el-tooltip
|
|
:content="collapsed ? '展开菜单' : '收起菜单'"
|
|
placement="right"
|
|
:show-after="200"
|
|
>
|
|
<el-button
|
|
link
|
|
:icon="collapsed ? Expand : Fold"
|
|
class="aside-toggle"
|
|
@click="appStore.toggleAside()"
|
|
/>
|
|
</el-tooltip>
|
|
</div>
|
|
|
|
<el-menu
|
|
:default-active="route.path"
|
|
:collapse="collapsed"
|
|
:collapse-transition="false"
|
|
router
|
|
class="aside-menu"
|
|
>
|
|
<el-menu-item v-for="item in menu.items" :key="item.index" :index="item.index">
|
|
<el-icon><component :is="item.icon" /></el-icon>
|
|
<template #title>
|
|
<span class="aside-label">{{ item.label }}</span>
|
|
<el-tag v-if="item.hint" size="small" type="info" effect="plain">
|
|
{{ item.hint }}
|
|
</el-tag>
|
|
</template>
|
|
</el-menu-item>
|
|
</el-menu>
|
|
</el-aside>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.app-aside {
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* The wrapper owns the border so it stays put while the menu scrolls. The
|
|
rail sits at the left edge, so the separator goes on its right. */
|
|
border-right: 1px solid var(--el-border-color);
|
|
background-color: var(--el-bg-color);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.aside-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
height: 48px;
|
|
/* Left inset comes from the shared metric so the section title lines up with
|
|
the mark in the header above it. */
|
|
padding: 0 8px 0 var(--aside-inset);
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.aside-head.is-collapsed {
|
|
justify-content: center;
|
|
padding: 0;
|
|
}
|
|
|
|
.aside-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--el-text-color-regular);
|
|
letter-spacing: 0.02em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.aside-toggle {
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
.aside-menu {
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
border-right: none;
|
|
}
|
|
|
|
/* A collapsed el-menu keeps a fixed 64px width that no longer matches the
|
|
56px rail, so the items would sit off-centre. */
|
|
.aside-menu.el-menu--collapse {
|
|
width: 100%;
|
|
}
|
|
|
|
.aside-label {
|
|
margin-right: 6px;
|
|
}
|
|
</style>
|