docs: document the template domain model, API, and shell layout
Records the decisions that are not visible from the code alone:
- why section_field has no parent_id, and what reuse that buys;
- that template_field.sort is the single source of display order, and
that repeats and ties are deliberately legal;
- that templates reference library fields rather than copying them, which
is what makes a rename propagate;
- that TiDB parses FOREIGN KEY and ignores it, so the integrity lives in
the application layer.
Also adds the API table, the frontend route table, and the seed script.
This commit is contained in:
+144
-4
@@ -5,6 +5,10 @@ sections, figures, and references that go into a paper. The name reflects the
|
|||||||
document-centric core of the workflow — a paper is composed from structured
|
document-centric core of the workflow — a paper is composed from structured
|
||||||
documents rather than written in one pass.
|
documents rather than written in one pass.
|
||||||
|
|
||||||
|
The first feature to land is the **template configuration**: a reusable library
|
||||||
|
of section fields plus named templates built from them, so that a paper is
|
||||||
|
written by filling in a known structure instead of deciding one.
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
**Backend**
|
**Backend**
|
||||||
@@ -45,29 +49,152 @@ Browser ──HTTP/JSON──▶ FastAPI ──SQLAlchemy──▶ TiDB (k3s)
|
|||||||
└── Vite dev server ───┘ (proxy /api)
|
└── Vite dev server ───┘ (proxy /api)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Domain Model
|
||||||
|
|
||||||
|
Three tables, and one rule that everything else follows from.
|
||||||
|
|
||||||
|
```
|
||||||
|
section_field the reusable heading library
|
||||||
|
id, name, level, font_size, font_color
|
||||||
|
|
||||||
|
paper_template a named outline
|
||||||
|
id, name, abstract
|
||||||
|
|
||||||
|
template_field the join, and the only home of display order
|
||||||
|
id, template_id → paper_template, field_id → section_field, sort
|
||||||
|
```
|
||||||
|
|
||||||
|
### The field library is flat, not a tree
|
||||||
|
|
||||||
|
`section_field` has no `parent_id`. Hierarchy is expressed only by `level`
|
||||||
|
(1 for `1.`, 2 for `1.1`), which is a *rendering hint*: it drives indentation
|
||||||
|
and numbering semantics in the UI and nothing else.
|
||||||
|
|
||||||
|
The reason is reuse. With a parent pointer a level-2 heading would belong to
|
||||||
|
exactly one level-1 heading, so "Background" could not sit under both
|
||||||
|
`1. Introduction` and `2. Related Work`. Flat fields are attached to templates,
|
||||||
|
not to each other, so one field can appear in any number of templates, in a
|
||||||
|
different position in each.
|
||||||
|
|
||||||
|
The numbering a reader sees is part of `name` and is written by the user
|
||||||
|
(`"1. Introduction"`, `"0 Abstract"`). Nothing derives or rewrites it.
|
||||||
|
|
||||||
|
### Display order lives in the join table
|
||||||
|
|
||||||
|
`template_field.sort` is a plain ascending integer and is the **only** thing
|
||||||
|
that decides what order a template's fields appear in. The order the user
|
||||||
|
clicked fields in is never stored, so selecting `字段1, 字段2.1, 字段2, 字段1.1`
|
||||||
|
and setting sorts `1, 4, 3, 2` renders as `字段1, 字段1.1, 字段2, 字段2.1`.
|
||||||
|
|
||||||
|
Two consequences are deliberate:
|
||||||
|
|
||||||
|
- **There is no unique constraint on `(template_id, field_id)`.** Placing the
|
||||||
|
same field twice in one template is a legitimate layout — the same level-2
|
||||||
|
heading under two different level-1 headings. The UI warns about a repeat; it
|
||||||
|
does not forbid one.
|
||||||
|
- **Ties are legal.** Equal `sort` values are broken by insertion order, so the
|
||||||
|
ordering is always total and stable. The UI warns about ties too, and offers a
|
||||||
|
button that renumbers the selection `1..N`.
|
||||||
|
|
||||||
|
### Templates hold references, not copies
|
||||||
|
|
||||||
|
`template_field` points at a library field; it does not snapshot its name or
|
||||||
|
typography. Renaming or restyling a field therefore updates every template that
|
||||||
|
places it, which is what makes "fix the template and the section names follow"
|
||||||
|
work.
|
||||||
|
|
||||||
|
### TiDB does not enforce foreign keys
|
||||||
|
|
||||||
|
TiDB parses `FOREIGN KEY` for compatibility and then ignores it. The constraints
|
||||||
|
are declared to document the relationships, and the integrity they would provide
|
||||||
|
is enforced in the application layer instead:
|
||||||
|
|
||||||
|
- deleting a field still placed in a template is refused with `409`, naming the
|
||||||
|
field and how many templates use it;
|
||||||
|
- creating a template that references a missing field is refused with `400`;
|
||||||
|
- `PaperTemplate.items` uses `cascade="all, delete-orphan"`, so deleting a
|
||||||
|
template removes its rows from the join table.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
All routes are mounted under `/api`. Interactive docs at `/docs`.
|
||||||
|
|
||||||
|
| Method | Path | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET` | `/health` | liveness plus a TiDB probe |
|
||||||
|
| `GET` | `/section-fields` | `keyword`, `level`, `page`, `page_size` |
|
||||||
|
| `POST` | `/section-fields` | create |
|
||||||
|
| `GET` `PATCH` `DELETE` | `/section-fields/{id}` | read / partial update / delete |
|
||||||
|
| `POST` | `/section-fields/batch-delete` | body `{ "ids": [...] }` |
|
||||||
|
| `GET` | `/templates` | `keyword` matches name **or** abstract |
|
||||||
|
| `POST` | `/templates` | name + abstract + ordered `fields` |
|
||||||
|
| `GET` `PATCH` `DELETE` | `/templates/{id}` | `PATCH` with `fields` replaces the selection |
|
||||||
|
| `POST` | `/templates/batch-delete` | body `{ "ids": [...] }` |
|
||||||
|
|
||||||
|
Conventions worth knowing:
|
||||||
|
|
||||||
|
- `font_color` is stored and returned as canonical `#RRGGBB`. The API also
|
||||||
|
accepts `rgb(r, g, b)`, `#rgb` and bare `aabbcc`, and normalises them on write.
|
||||||
|
- `font_size` is a JSON number, not a string — a client should not have to parse
|
||||||
|
it before using it in a CSS rule.
|
||||||
|
- List endpoints return `{ items, total, page, page_size, pages }`.
|
||||||
|
- A template read returns `fields` already ordered by `sort`; clients never sort.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### Shell
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ [book mark] paper-doc 论文 模板 [mark] │ header
|
||||||
|
├──────────────────────────────────────┬───────────────────┤
|
||||||
|
│ │ 模板配置 │ second-level
|
||||||
|
│ <RouterView> │ 模板列表 │ menu, right
|
||||||
|
│ │ 字段管理 │
|
||||||
|
└──────────────────────────────────────┴───────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The mark appears at both ends of the header. The second-level menu sits on the
|
||||||
|
**right** and is driven entirely by `route.meta.section`, so a route declares
|
||||||
|
which menu it belongs to and deep links render correctly on first paint. Routes
|
||||||
|
without a `section` (the welcome page) show no menu.
|
||||||
|
|
||||||
|
### Routes
|
||||||
|
|
||||||
|
| Path | View | Section |
|
||||||
|
|---|---|---|
|
||||||
|
| `/` | welcome | — |
|
||||||
|
| `/papers` | 论文 (content TBD) | `papers` |
|
||||||
|
| `/templates` | redirects to `/templates/list` | — |
|
||||||
|
| `/templates/list` | template table + CRUD | `templates` |
|
||||||
|
| `/templates/fields` | field library + CRUD | `templates` |
|
||||||
|
|
||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
paper-doc/
|
paper-doc/
|
||||||
├── backend/ # FastAPI application
|
├── backend/ # FastAPI application
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── api/ # route handlers (routers)
|
│ │ ├── api/routes/ # route handlers (health, section_fields, templates)
|
||||||
│ │ ├── core/ # settings and configuration
|
│ │ ├── core/ # settings and configuration
|
||||||
│ │ ├── crud/ # data-access helpers
|
│ │ ├── crud/ # data-access helpers
|
||||||
│ │ ├── db/ # engine, session, declarative base
|
│ │ ├── db/ # engine, session, declarative base
|
||||||
│ │ ├── models/ # SQLAlchemy models (intentionally empty for now)
|
│ │ ├── models/ # SQLAlchemy models
|
||||||
│ │ └── schemas/ # Pydantic request/response models
|
│ │ └── schemas/ # Pydantic request/response models
|
||||||
│ ├── alembic/ # migration environment and revisions
|
│ ├── alembic/ # migration environment and revisions
|
||||||
|
│ ├── scripts/seed.py # idempotent seed for the field library + templates
|
||||||
│ ├── alembic.ini
|
│ ├── alembic.ini
|
||||||
│ ├── requirements.txt
|
│ ├── requirements.txt
|
||||||
│ └── .env.example
|
│ └── .env.example
|
||||||
├── frontend/ # Vue 3 SPA
|
├── frontend/ # Vue 3 SPA
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── api/ # axios instance and endpoint modules
|
│ │ ├── api/ # axios instance and endpoint modules
|
||||||
|
│ │ ├── components/ # shell, field and template components
|
||||||
│ │ ├── router/ # vue-router configuration
|
│ │ ├── router/ # vue-router configuration
|
||||||
│ │ ├── stores/ # pinia stores (persisted)
|
│ │ ├── stores/ # pinia stores (persisted)
|
||||||
│ │ ├── views/ # route-level components
|
│ │ ├── styles/ # global reset and the shell viewport contract
|
||||||
│ │ └── components/ # reusable components
|
│ │ ├── utils/ # formatting helpers
|
||||||
|
│ │ └── views/ # route-level components
|
||||||
│ ├── package.json
|
│ ├── package.json
|
||||||
│ └── vite.config.ts
|
│ └── vite.config.ts
|
||||||
└── docs/ # project documentation
|
└── docs/ # project documentation
|
||||||
@@ -114,3 +241,16 @@ changes are applied through Alembic:
|
|||||||
cd backend
|
cd backend
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Seed data**
|
||||||
|
|
||||||
|
`scripts/seed.py` fills the field library with a standard academic outline (20
|
||||||
|
fields, from `0 Abstract` to `7 References`) and creates three starter
|
||||||
|
templates. It matches fields and templates by name, so running it twice adds
|
||||||
|
nothing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
.venv/bin/python scripts/seed.py # add anything missing
|
||||||
|
.venv/bin/python scripts/seed.py --reset # empty the tables first
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user