diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index 9ce515a..d7182d6 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -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 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 **Backend** @@ -45,29 +49,152 @@ Browser ──HTTP/JSON──▶ FastAPI ──SQLAlchemy──▶ TiDB (k3s) └── 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 +│ │ 模板列表 │ 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 ``` paper-doc/ ├── backend/ # FastAPI application │ ├── app/ -│ │ ├── api/ # route handlers (routers) +│ │ ├── api/routes/ # route handlers (health, section_fields, templates) │ │ ├── core/ # settings and configuration │ │ ├── crud/ # data-access helpers │ │ ├── db/ # engine, session, declarative base -│ │ ├── models/ # SQLAlchemy models (intentionally empty for now) +│ │ ├── models/ # SQLAlchemy models │ │ └── schemas/ # Pydantic request/response models │ ├── alembic/ # migration environment and revisions +│ ├── scripts/seed.py # idempotent seed for the field library + templates │ ├── alembic.ini │ ├── requirements.txt │ └── .env.example ├── frontend/ # Vue 3 SPA │ ├── src/ │ │ ├── api/ # axios instance and endpoint modules +│ │ ├── components/ # shell, field and template components │ │ ├── router/ # vue-router configuration │ │ ├── stores/ # pinia stores (persisted) -│ │ ├── views/ # route-level components -│ │ └── components/ # reusable components +│ │ ├── styles/ # global reset and the shell viewport contract +│ │ ├── utils/ # formatting helpers +│ │ └── views/ # route-level components │ ├── package.json │ └── vite.config.ts └── docs/ # project documentation @@ -114,3 +241,16 @@ changes are applied through Alembic: cd backend 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 +```