frontend: add the template configuration UI
The shell: the app mark at both ends of the header, the two top-level
entries between them, and the second-level menu on the right. That menu is
driven entirely by `route.meta.section`, so a route declares which menu it
belongs to and a deep link renders the right one on first paint; routes with
no section (the welcome page) show none.
Screens:
/ welcome, with live template and field counts so the
page doubles as a connectivity check
/papers 论文, content still to be decided
/templates/list the template table: search, create, edit, preview,
delete, batch delete, pagination
/templates/fields the field library: the same CRUD, plus level filter
The template form is the centre of it. Fields are chosen from the flat
library by clicking — any subset, any order, the same field more than once —
and the selection is always rendered sorted by `sort`, so editing a number
reorders the outline immediately. Repeats and duplicate sorts are surfaced as
warnings rather than blocked, because a repeat is often deliberate and a tie
is legal; a "自动排序" button renumbers the selection 1..N. The panel on the
right of the picker opens 字段管理 in a second tab, so a half-filled form is
never lost to a navigation.
The preview drawer renders the outline exactly as it will read, at each
field's own size and colour, with the sort value shown alongside — the
quickest way to confirm the ordering before writing against a template.
Also in this change:
- ApiError now extends Error. It was a plain object, so the common
`error instanceof Error ? error.message : ...` idiom fell through to a
generic message and threw away what the server actually said — such as
which template name is already taken. Server messages now reach the user.
- font colours are normalised client-side on blur, matching the backend,
so a hand-typed rgb(255, 0, 0) is tidied up rather than rejected later.
- api/health.ts is removed: the rewritten welcome page was its only
consumer, and the counts already prove connectivity.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* The second-level menu, pinned to the right 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 { 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)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-aside
|
||||
v-if="menu"
|
||||
class="app-aside"
|
||||
:width="collapsed ? '56px' : '208px'"
|
||||
>
|
||||
<div class="aside-head" :class="{ 'is-collapsed': collapsed }">
|
||||
<span v-if="!collapsed" class="aside-title">{{ menu.title }}</span>
|
||||
<el-tooltip
|
||||
:content="collapsed ? '展开菜单' : '收起菜单'"
|
||||
placement="left"
|
||||
: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. */
|
||||
border-left: 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;
|
||||
padding: 0 8px 0 16px;
|
||||
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>
|
||||
Reference in New Issue
Block a user