098bfd1bfb
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.
202 lines
5.8 KiB
CSS
202 lines
5.8 KiB
CSS
/* ==========================================================================
|
|
* Global base styles — imported once from src/main.ts, after Element Plus.
|
|
*
|
|
* This file owns exactly two things: a minimal reset, and the application
|
|
* shell's viewport contract. Every view inherits both, so no page has to
|
|
* re-declare margins, heights or scroll behaviour.
|
|
*
|
|
* Why it exists
|
|
* -------------
|
|
* Element Plus ships component styles only: dist/index.css contains no
|
|
* `html` / `body` reset. The browser default `body { margin: 8px }` therefore
|
|
* applied on top of a shell declared as `min-height: 100vh`. The document
|
|
* measured one viewport PLUS 8px, which produced:
|
|
*
|
|
* - a permanently visible vertical scrollbar on the right edge, even on a
|
|
* page whose content is far shorter than the viewport;
|
|
* - every page shifted 8px away from the intended 20px gutter;
|
|
* - two competing scroll containers (the window and .el-main).
|
|
*
|
|
* The contract now
|
|
* ----------------
|
|
* html / body / #app / .app-shell exactly one viewport tall, zero margin
|
|
* body never scrolls -> the window has no bar
|
|
* .app-main the single scroll container, showing a
|
|
* slim bar only when content really does
|
|
* exceed the viewport
|
|
* page gutter el-header / el-main padding (20px),
|
|
* which all content stays inside
|
|
* ========================================================================== */
|
|
|
|
/* --- reset ----------------------------------------------------------------- */
|
|
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
height: 100%;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
/* Element Plus declares --el-font-family but never applies it to the
|
|
document, so page-level text would otherwise fall back to the browser
|
|
default. */
|
|
font-family: var(--el-font-family);
|
|
background-color: var(--el-bg-color-page);
|
|
color: var(--el-text-color-primary);
|
|
|
|
/* The shell below is exactly one viewport tall and owns all scrolling.
|
|
The window itself must never scroll, so no bar appears on the right. */
|
|
overflow: hidden;
|
|
}
|
|
|
|
#app {
|
|
height: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* Media can never be the reason a page overflows its padding. */
|
|
img,
|
|
svg,
|
|
video,
|
|
canvas {
|
|
max-width: 100%;
|
|
}
|
|
|
|
/* Long unbroken strings — DSNs, URLs, hashes — wrap instead of pushing the
|
|
content box wider than the page gutter. */
|
|
code,
|
|
kbd,
|
|
samp {
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* --- application shell ------------------------------------------------------
|
|
.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 {
|
|
/* Fill the viewport exactly. Not `min-height: 100vh`: that grows with
|
|
content and re-introduces the window scrollbar. */
|
|
height: 100%;
|
|
min-height: 0;
|
|
|
|
/* el-container defaults to a row; the shell stacks header over content. */
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* The header keeps its height; only the content area scrolls. */
|
|
.app-shell > .app-header {
|
|
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;
|
|
overflow-y: auto;
|
|
overscroll-behavior: contain;
|
|
|
|
/* Slim, low-contrast scrollbar: invisible while content fits, unobtrusive
|
|
when it does not. Firefox. */
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--el-border-color-darker) transparent;
|
|
}
|
|
|
|
/* WebKit / Blink. */
|
|
.app-main::-webkit-scrollbar,
|
|
.app-aside::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
.app-main::-webkit-scrollbar-track,
|
|
.app-aside::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.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-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. */
|
|
|
|
.page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
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;
|
|
}
|