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:
2026-09-18 16:00:02 +08:00
parent 13e5fc2cc1
commit 098bfd1bfb
19 changed files with 2575 additions and 137 deletions
+63 -7
View File
@@ -78,9 +78,17 @@ samp {
}
/* --- application shell ------------------------------------------------------
.app-shell and .app-main are applied by src/App.vue. They live here because
the viewport contract spans html -> body -> #app -> shell -> main and only
holds together when it is declared in one place.
.app-shell, .app-body, .app-main and .app-aside are applied by src/App.vue
and src/components/SectionAside.vue. They live here because the viewport
contract spans html -> body -> #app -> shell -> body -> main and only holds
together when it is declared in one place.
The shape is:
.app-shell column: header over .app-body
.app-body row: .app-main over .app-aside (if present)
.app-main the single scroll container
.app-aside fixed-width rail, scrolls independently
--------------------------------------------------------------------------- */
.app-shell {
@@ -98,10 +106,21 @@ samp {
flex: 0 0 auto;
}
/* The row holding the content and the right-hand menu. */
.app-body {
flex: 1 1 auto;
min-height: 0;
/* el-container is a row by default and stays one because it holds an
el-aside; stating it keeps the layout readable. */
flex-direction: row;
}
.app-main {
/* min-height: 0 lets this flex child actually shrink, so overflow is
contained here instead of escaping to the window. */
flex: 1 1 auto;
min-width: 0;
min-height: 0;
overflow-x: hidden;
@@ -115,24 +134,40 @@ samp {
}
/* WebKit / Blink. */
.app-main::-webkit-scrollbar {
.app-main::-webkit-scrollbar,
.app-aside::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.app-main::-webkit-scrollbar-track {
.app-main::-webkit-scrollbar-track,
.app-aside::-webkit-scrollbar-track {
background: transparent;
}
.app-main::-webkit-scrollbar-thumb {
.app-main::-webkit-scrollbar-thumb,
.app-aside::-webkit-scrollbar-thumb {
background-color: var(--el-border-color-darker);
border-radius: 4px;
}
.app-main::-webkit-scrollbar-thumb:hover {
.app-main::-webkit-scrollbar-thumb:hover,
.app-aside::-webkit-scrollbar-thumb:hover {
background-color: var(--el-text-color-placeholder);
}
/* The second-level menu on the right. Its own width is set by the component,
so only the shrink behaviour belongs here. */
.app-aside {
flex: 0 0 auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--el-border-color-darker) transparent;
transition: width 0.2s ease;
}
/* --- page-level helpers -----------------------------------------------------
A view's outermost element uses .page to get a consistent vertical rhythm
inside the main gutter. */
@@ -143,3 +178,24 @@ samp {
gap: 16px;
min-width: 0;
}
/* Toolbar above a table: filters on the left, actions on the right, wrapping
on narrow screens instead of overflowing the gutter. */
.toolbar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.toolbar-spacer {
flex: 1 1 auto;
min-width: 0;
}
/* Right-aligned pagination under a table. */
.table-pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}