Files
paper-doc/frontend/src/components/SectionAside.vue
T
govin 0d0aa20be2 frontend: make the logo block exactly as wide as the second-level menu
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.
2026-09-18 16:43:30 +08:00

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>