Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bab22940bd | ||
|
|
8ba1d7e2a2 | ||
|
|
d814914e21 | ||
|
|
2f3bade817 | ||
|
|
0b25a39e7b |
@@ -1,5 +1,6 @@
|
||||
crearte/
|
||||
crearte-deploy/
|
||||
crearte-server/
|
||||
.superpowers/
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Repository role
|
||||
|
||||
Docker Compose orchestration for the crearte full stack. Three repos must stay siblings:
|
||||
Monorepo wrapper for the crearte full stack. This repo tracks `AGENTS.md`, `clone_all.sh`, and `docs/` (roadmap and all cross-repo design/plan docs). The three product repos are nested clones, each with its own git history and `AGENTS.md`:
|
||||
|
||||
```
|
||||
repos/
|
||||
crearte-monorepo/ # this repo (wrapper)
|
||||
├── crearte/ # frontend (build context for web-* images)
|
||||
├── crearte-server/ # backend Go API (build context for api-* images)
|
||||
└── crearte-deploy/ # this repo — compose only, no app code
|
||||
└── crearte-deploy/ # compose orchestration, no app code
|
||||
```
|
||||
|
||||
Everything runs in containers. Do not introduce host-run workflows (no `npm run dev` / `go run` on the host) in docs, scripts, or compose.
|
||||
@@ -16,9 +16,9 @@ Everything runs in containers. Do not introduce host-run workflows (no `npm run
|
||||
## Safety Rules
|
||||
|
||||
- Branch naming: `{feat|fix|docs|chore}/{branch-name}` (e.g. `fix/compose-port-clash`).
|
||||
- Before `git commit`: run `git branch --show-current`. If on `master`, do NOT commit — ask the user for a branch name, create it, and commit there.
|
||||
- Commits in this monorepo wrapper may go directly on `master` (user-approved 2026-09-29). In the inner repos (`crearte/`, `crearte-server/`, `crearte-deploy/`): before `git commit`, run `git branch --show-current`. If on `master`, do NOT commit — ask the user for a branch name, create it, and commit there.
|
||||
- `.env` is gitignored and must stay that way. Never commit secrets. Any new secret must land in `.env.example` as an empty placeholder (with a comment) in the same commit that introduces it.
|
||||
- Changes → `docs/CHANGELOG.md` (this repo is not under `web/`, so `CHANGELOG_webui.md` never applies). Higher versions on top.
|
||||
- Changes → `docs/CHANGELOG.md` of the repo being changed (this wrapper's `docs/CHANGELOG.md`, or an inner repo's `docs/CHANGELOG.md` for its own changes). This workspace is not under `web/`, so `CHANGELOG_webui.md` never applies. Higher versions on top.
|
||||
- CHANGELOG entry format: same entry has English line then Chinese line on consecutive lines (no blank line between them); different entries are separated by a blank line.
|
||||
- Merging into `master`: always create a merge commit — `git merge --no-ff <branch>` (never fast-forward, the integration point must be recorded). Delete the merged branch afterwards.
|
||||
- Compose changes that alter usage must update `README.md` in the same commit.
|
||||
@@ -41,8 +41,12 @@ Everything runs in containers. Do not introduce host-run workflows (no `npm run
|
||||
3. Smoke-test the affected path (e.g. `docker compose exec api-dev wget -qO- http://127.0.0.1:8080/healthz`, or the upload flow through http://localhost:8080).
|
||||
4. Tear down with plain `down` (never `-v`) unless the task is specifically about destroying data.
|
||||
|
||||
## Documentation
|
||||
|
||||
- All specs, implementation plans, roadmaps, and cross-repo coordination docs live in this wrapper's `docs/` (`docs/ROADMAP.md`, `docs/specs/`, `docs/plans/`). Do not scatter them into the inner repos; their `docs/superpowers/` copies are legacy (in `crearte` that path is gitignored). This supersedes the crearte-deploy 0.3.0 "canonical home" convention.
|
||||
- Register every new spec/plan in the doc index table at the bottom of `docs/ROADMAP.md`.
|
||||
|
||||
## Layout notes
|
||||
|
||||
- `docs/superpowers/` (plans/specs) is tracked in this repo — unlike in `crearte`, where it is gitignored.
|
||||
- No app code, no tests, no build scripts live here; those belong to `crearte` / `crearte-server`.
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Changelog / 更新日志
|
||||
|
||||
All notable changes to this repository should be documented in this file.
|
||||
本仓库的重要变更建议统一记录在此文件中。
|
||||
|
||||
The format loosely follows Keep a Changelog and can be adapted to the team's habits.
|
||||
本文档参考了 Keep a Changelog 的思路,也可以根据团队习惯调整。
|
||||
|
||||
## [0.2.0] - 2026-09-29
|
||||
|
||||
### Changed / 变更
|
||||
|
||||
- Roadmap delivered: P0 (known-defect cleanup — CORS `If-None-Match` fix in crearte-server `a8ea755`, compose key-store drift cleanup in crearte-deploy `27044be`) and P4 (author page `/users/:user`, crearte `f12cbf1`) are merged to their repos' master and pushed; roadmap statuses updated.
|
||||
- 路线图交付:P0(已知缺陷清理——crearte-server `a8ea755` 的 CORS `If-None-Match` 修复、crearte-deploy `27044be` 的 compose 密钥库遗留清理)与 P4(作者主页 `/users/:user`,crearte `f12cbf1`)均已合并至各仓 master 并推送;路线图状态已更新。
|
||||
|
||||
## [0.1.0] - 2026-09-29
|
||||
|
||||
### Added / 新增
|
||||
|
||||
- Added `docs/ROADMAP.md`: the cross-repo roadmap decomposed into sub-projects P0–P8 (ops foundation → product value → governance), with ordering rationale and a doc index for future specs and plans.
|
||||
- 新增 `docs/ROADMAP.md`:跨仓库路线图,分解为 P0–P8 子项目(上线底座 → 产品价值 → 治理长尾),含排序理由与后续 spec/计划的文档索引。
|
||||
|
||||
### Changed / 变更
|
||||
|
||||
- Doc convention: all specs, implementation plans and cross-repo coordination docs now live in the monorepo `docs/` instead of per-repo `docs/superpowers/`; this supersedes the crearte-deploy 0.3.0 "canonical home" convention. Commits in the monorepo wrapper may go directly to `master` (user-approved); the three inner repos keep their own branch rules.
|
||||
- 文档约定:所有 spec、实现计划与跨仓库协作文档统一收归 monorepo `docs/`,不再散落各内层仓库的 `docs/superpowers/`;此约定取代 crearte-deploy 0.3.0 的「canonical home」约定。monorepo wrapper 可直接提交 `master`(维护者确认),内层三仓仍遵循各自分支规范。
|
||||
@@ -0,0 +1,53 @@
|
||||
# crearte 路线图 / Roadmap
|
||||
|
||||
跨三个仓库(`crearte` 前端 / `crearte-server` 后端 / `crearte-deploy` 编排)的子项目路线图与索引,维护在 monorepo wrapper(本仓库)。
|
||||
|
||||
- **版本 v0**:2026-09-29 与维护者确认的分解与顺序。
|
||||
- **现状基线**:crearte 0.13.0 / crearte-server 0.11.0 / crearte-deploy 0.3.1。
|
||||
|
||||
## 分解原则
|
||||
|
||||
- 按「依赖关系 + 是否阻塞上线」排序;每个子项目的规模控制在一份 spec 能装下。
|
||||
- 每个子项目独立走 **spec → 实现计划 → 实现 → 验证** 的循环。
|
||||
- 隐藏复杂度出现时升级路径(拆出新子项目),不硬塞进当前子项目。
|
||||
|
||||
## 第一波:上线底座(运维 / 可靠性,顺序敏感)
|
||||
|
||||
| # | 子项目 | 内容 | 涉及仓库 | 依赖 | 状态 |
|
||||
|---|--------|------|----------|------|------|
|
||||
| P0 | 已知缺陷清理 | ① CORS `Access-Control-Allow-Headers` 补 `If-None-Match`(解锁 e2e:stack Step5:revoke 410 + 降级)② 清理 compose 配置漂移(`BUNDLE_KEY_STORE` 环境变量、`bundle-keys-*` 卷——密钥自 server 0.6.0 起在 Postgres)③ `crearte-deploy` CHANGELOG 补账 | server, deploy | 无 | **完成**(server `a8ea755` / deploy `27044be`,2026-09-29 合并推送) |
|
||||
| P1 | prod 写侧可用 | prod 栈补对象存储(prod MinIO 或外部 S3)+ `api-prod` 配 `STORAGE_S3_*` / `GAMES_BASE_DOMAIN` / `CORS_ALLOWED_ORIGINS`;密码默认值、TLS 起步。现状:prod 无对象存储时写侧路由(上传/投稿/审核/预览)不注册,生产只读 | deploy(少量 server) | P0 | 待启动 |
|
||||
| P2 | CI + 测试基线 | 后端 / 部署仓建 CI(gofmt/vet/test + `TEST_DATABASE_URL` 集成层 + e2e:stack 全链路);与前端已有 `validate.yml` 对齐。现状:后端/部署仓无 CI,Postgres 集成测试无 `TEST_DATABASE_URL` 时静默 skip | 三仓 | P0(e2e 依赖其修复) | 待启动 |
|
||||
| P3 | 备份 + 可观测 | `pg_dump` 定时备份 + MinIO 数据保护 + 恢复演练 runbook;结构化 / 请求日志 + `/metrics`;限流器单实例问题记录权衡 | server, deploy | P1(拓扑定型) | 待启动 |
|
||||
|
||||
## 第二波:产品价值(可与第一波部分并行)
|
||||
|
||||
| # | 子项目 | 内容 | 涉及仓库 | 依赖 | 状态 |
|
||||
|---|--------|------|----------|------|------|
|
||||
| P4 | 作者主页 | `/users/:user` 聚合页(该作者全部已上架作品,前端按命名空间过滤,**零后端改动**),按最新上架排序;顺带 `/games/:user/:slug` 面包屑与作者名互链。spec:`docs/specs/2026-09-29-author-page-design.md` | crearte(前端) | 无强依赖,可提前并行 | **完成**(crearte `f12cbf1`,2026-09-29 合并推送) |
|
||||
| P5 | 收藏 / 评分 | 新数据表 + 用户态 API + 目录/详情页 UI;产品线里最大的一块 | 三仓 | 建议 P2 先落地 | 待启动 |
|
||||
| P6 | hosted 作品投稿 | 范围模糊,需先定义 hosted 托管语义(只存 URL 还是真托管文件),再定投稿/审核流 | 三仓 | 需先决策 | 待澄清 |
|
||||
|
||||
## 第三波:治理与长尾(随时可插队)
|
||||
|
||||
| # | 子项目 | 内容 | 状态 |
|
||||
|---|--------|------|------|
|
||||
| P7 | 管理后台增强 | 用户列表 / 角色管理 UI(替代 CLI `user set-role`)+ 审计日志 | 待启动 |
|
||||
| P8 | 长尾打包 | 权限开关 UI 全量(`inlineStyle/wasm/coop/fullscreen/gamepad`)、后端 triage 小项(`games.Detail` 400 细分、admin 路由 slug 校验、approve 同名竞态测试)、账号注销、静态兜底目录、CHANGELOG 模板文案 | 待启动 |
|
||||
|
||||
## 排序理由
|
||||
|
||||
- **P0 最先**:几十行的修复,却解锁整条 e2e 回归链路——后面每一波都靠它做验证保障。
|
||||
- **P1 紧随**:不做则生产环境投稿 / 审核根本不可用,产品功能再丰富也上不了线。
|
||||
- **P4 可提前**:无依赖且范围清晰,若想先看到用户可感知的产品变化,可与第一波并行。
|
||||
|
||||
## 文档与提交约定
|
||||
|
||||
- 所有 spec / 实现计划 / 跨仓库协作文档统一放在本仓库 `docs/`(`docs/specs/`、`docs/plans/`、`docs/ROADMAP.md`),并在下表登记;内层仓库只保留各自的 `docs/CHANGELOG.md` 与 `docs/README.md`。此约定取代 crearte-deploy 0.3.0 确立的「crearte-deploy 为跨仓库文档 canonical home」。
|
||||
- monorepo(本仓库)可直接在 `master` 提交;内层三仓仍遵循各自 `AGENTS.md` 的分支与合并规范。
|
||||
|
||||
## 子项目文档索引
|
||||
|
||||
| 子项目 | spec | 实现计划 |
|
||||
|--------|------|----------|
|
||||
| P4 作者主页 | `docs/specs/2026-09-29-author-page-design.md` | `docs/plans/2026-09-29-author-page.md`(已执行,2026-09-29) |
|
||||
@@ -0,0 +1,161 @@
|
||||
# Author Page (`/users/:user`) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 新增作者主页 `/users/:user`:列出该作者全部已上架作品(最新在前),并从作品详情页与卡片提供入口。
|
||||
|
||||
**Architecture:** 纯前端实现 —— 新视图 `AuthorView.vue` 复用现有 `repo.listGames()`(API+static 双源合并、ETag 缓存、API 宕机降级),按 `resolveUserSlug(game).user` 过滤后交给 `filterGames` 的 `DEFAULT_FILTER`(`sort: 'new'`)排序渲染;路由 `/users/:user`(`props: true`)。crearte-server 零改动。
|
||||
|
||||
**Tech Stack:** Vue 3.5 `<script setup>` + TypeScript + vue-router 5 + Vitest/@vue/test-utils(happy-dom)+ Playwright(noauth 配置)。
|
||||
|
||||
**Spec:** `docs/specs/2026-09-29-author-page-design.md`(monorepo 仓)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **crearte-server 零改动**:本计划任何任务不得在 `crearte-server/` 产生 diff。
|
||||
- 页面只做纯聚合:无筛选器、无查询参数、无用户存在性探测。
|
||||
- 头部展示**账号 `username`**(路由参数),不从 `author.name` 推导;显示名统一用 `authorDisplayName(game)`(`@/lib/labels`),两者语义不混用。
|
||||
- 排序复用 `filterGames(games, DEFAULT_FILTER)` 的 `sort: 'new'`(`addedAt` 降序、`id` 决胜),**禁止复制比较器代码**。
|
||||
- TDD:每个任务先写失败测试、看它以正确理由失败,再实现;任务内提交。
|
||||
- 工作分支 `feat/author-page`(crearte 仓);CHANGELOG 条目为双语(英文行 + 中文行连续,条目间空行),版本 `0.14.0`。
|
||||
- **本机无 host node**:npm 命令一律容器内执行(见各任务 Run 命令;来源 `crearte-deploy/docs/HANDOFF.md` §2)。若 node_modules 卷为空,先在容器内 `npm ci`。
|
||||
- 别名 `@` → `src/app`。
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **旧式单段 id 作品**(无 `user`/`slug` 字段)不得错误匹配到任何作者页 —— Task 1 测试钉住 `resolveUserSlug` 回退(`user: ''` 不匹配)。
|
||||
2. **未知用户名**必须渲染空态(非 404、非报错)—— Task 1 测试钉住。
|
||||
3. **`author.name` 与 `username` 语义分离**:显示文本可以是 `author.name`,但链接目标永远是 `/users/<user>`;无 `user` 时不得产生内部链接 —— Task 2 测试钉住三种形态(内部链接 / 外链兜底 / 纯文本)。
|
||||
4. **GameCard 新增作者行**不得破坏既有卡片测试与布局断言 —— Task 2 跑全量前端测试。
|
||||
5. **加载失败可重试**:`repo.listGames()` reject 时进 `StatePanel` 错误态,`@retry` 重新加载 —— Task 1 测试钉住。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: AuthorView + 路由
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app/views/AuthorView.vue`
|
||||
- Modify: `src/app/router/index.ts`(`routes` 数组,注册在 `/:pathMatch(.*)*` 之前)
|
||||
- Test: `src/app/views/AuthorView.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `repo.listGames(): Promise<GameSummary[]>`、`resolveUserSlug(game): { user, slug }`(`@/data`)、`filterGames(games, state)` + `DEFAULT_FILTER`(`@/lib/filter`)、`useAsync<T>(loader, deps)`(`@/composables/useAsync`)、`StatePanel`(props `{ loading, error }`,emit `retry`)、`GameCard`(props `{ game, headingLevel? }`)、`BaseButton`。
|
||||
- Produces: 路由 `{ path: '/users/:user', name: 'author', props: true }`;`AuthorView` props `user: string`。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(`src/app/views/AuthorView.test.ts`,仿 `GameView.test.ts` 的 `vi.hoisted` + `vi.mock('@/data', …)` + memory-router + `RouterLink` stub 模式;`mount(AuthorView, { props: { user: 'alice' }, global: { plugins: [router], stubs: … } })`)
|
||||
|
||||
```ts
|
||||
// 夹具(模块内常量):matching(2 个 alice + 1 个 bob,addedAt 分别 2026-09-01/2026-09-03/2026-09-02)
|
||||
it('只显示该作者作品并按最新上架排序') // 渲染 2 张 GameCard,顺序为 addedAt 降序(09-03 在前)
|
||||
it('旧式单段 id 作品不匹配任何作者页') // { id: 'plain-legacy' /* 无 user/slug */ } 不出现在 alice 页
|
||||
it('复合 id 但缺 user 字段时按 id 前段解析') // { id: 'alice/legacy' /* 无 user/slug */ } 出现在 alice 页
|
||||
it('未知作者渲染空态而非报错') // props user='ghost' → 文案「该作者暂无已上架作品」可见,无 role="alert"
|
||||
it('头部展示 @username、作品数与面包屑') // 文本含 '@alice' 与 '2';面包屑「目录」链接 href='/games'
|
||||
it('加载失败进入错误态且可重试') // mockRejectedValue 一次 → StatePanel 错误;触发 retry 后 mockResolvedValue → 列表渲染
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `docker run --rm -v /root/workspace/crearte-monorepo/crearte:/repo -v crearte-deploy_mock_node_modules:/repo/src/node_modules -w /repo/src crearte:dev npm test -- AuthorView`
|
||||
Expected: FAIL(`AuthorView.vue` 不存在 / 路由名未注册),失败原因正确。
|
||||
|
||||
- [ ] **Step 3: 实现 `AuthorView.vue`**(结构镜像 `CatalogView.vue` 的 `StatePanel` 包裹 + `GameCard` 网格 + 空态块;无筛选 UI)
|
||||
|
||||
```ts
|
||||
const props = defineProps<{ user: string }>()
|
||||
const { data: games, error, loading, reload } = useAsync<GameSummary[]>(() => repo.listGames())
|
||||
const visible = computed(() =>
|
||||
filterGames((games.value ?? []).filter((g) => resolveUserSlug(g).user === props.user), DEFAULT_FILTER)
|
||||
)
|
||||
```
|
||||
|
||||
模板要点:面包屑「目录 / @user」(`RouterLink :to="{ name: 'catalog' }"`);`<h1>@{{ props.user }}</h1>`;作品数 `{{ visible.length }}`;`<GameCard v-for="game in visible" :key="game.id" :game="game" />` 网格(栅格类名抄 `CatalogView.vue:70`);空态块抄 `CatalogView.vue:73-85` 形态,文案「该作者暂无已上架作品」+ `BaseButton` 返回目录。
|
||||
|
||||
- [ ] **Step 4: 注册路由**(`src/app/router/index.ts`,catch-all 之前)
|
||||
|
||||
```ts
|
||||
{ path: '/users/:user', name: 'author', component: () => import('@/views/AuthorView.vue'), props: true },
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**:Step 2 同命令 → PASS;再跑全量 `… npm test` → 全绿。
|
||||
- [ ] **Step 6: 提交**
|
||||
|
||||
```bash
|
||||
git checkout -b feat/author-page # 若尚未创建
|
||||
git add src/app/views/AuthorView.vue src/app/views/AuthorView.test.ts src/app/router/index.ts
|
||||
git commit -m "feat(views): author page /users/:user aggregating a user's published works"
|
||||
```
|
||||
|
||||
### Task 2: 入口链接(GameView 作者名 + GameCard 作者行)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/views/GameView.vue:55-66`(作者段落)
|
||||
- Modify: `src/app/components/GameCard.vue:35` 之后(描述 `</p>` 与徽章行之间)
|
||||
- Test: `src/app/views/GameView.test.ts`(追加用例)、`src/app/components/GameCard.test.ts`(追加用例)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `resolveUserSlug`(`@/data`)、`authorDisplayName(game)`(`@/lib/labels`,返回 `author?.name || user || '佚名'`)、Task 1 的路由 `name: 'author'`。
|
||||
- Produces: 无新 API;链接契约 `RouterLink :to="'/users/' + user"`。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
GameView(三形态,显示文本统一 `authorDisplayName(game)`):
|
||||
```ts
|
||||
it('有命名空间时作者名链接到 /users/:user') // game{user:'alice', author:{name:'爱丽丝'}} → <a href="/users/alice">爱丽丝</a>
|
||||
it('无 user 但有 author.url 时保持外链') // game{author:{name:'X', url:'https://e.com'}} → href 含 toInterstitialIfExternal 结果(既有行为不回归)
|
||||
it('无 user 无 url 时纯文本') // 无 <a>,文本仍显示作者名
|
||||
```
|
||||
GameCard:
|
||||
```ts
|
||||
it('卡片渲染作者行并链接到作者页') // game{user:'alice'} → 文本含 authorDisplayName,href='/users/alice'
|
||||
it('无 user 的卡片作者行为纯文本') // 无 href='/users/' 前缀的链接
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `docker run --rm -v /root/workspace/crearte-monorepo/crearte:/repo -v crearte-deploy_mock_node_modules:/repo/src/node_modules -w /repo/src crearte:dev npm test -- GameView`
|
||||
Run: 同上 `… npm test -- GameCard`
|
||||
Expected: 两处 FAIL(作者行/内部链接不存在),失败原因正确。
|
||||
|
||||
- [ ] **Step 3: 实现**(优先级:`resolveUserSlug(game).user` 非空 → 内部 `RouterLink`;否则 `game.author?.url` → 既有外链(`toInterstitialIfExternal`);否则纯文本。GameCard 作者行放在描述与徽章行之间,`text-ink-soft` 小字,与卡片 `headingLevel` 无交互)
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**:Step 2 两命令 → PASS;全量 `… npm test` → 全绿(Review Focus #4:既有卡片/详情断言不回归)。
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add src/app/views/GameView.vue src/app/views/GameView.test.ts src/app/components/GameCard.vue src/app/components/GameCard.test.ts
|
||||
git commit -m "feat(views): link author names to the author page from detail and cards"
|
||||
```
|
||||
|
||||
### Task 3: e2e + CHANGELOG + 全量验证
|
||||
|
||||
**Files:**
|
||||
- Create: `src/e2e/author-page.noauth.spec.ts`
|
||||
- Modify: `crearte/docs/CHANGELOG.md`(顶部加 `[0.14.0]`)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `src/e2e/helpers.ts` 的既有辅助(`fixtureGame` 等);noauth 配置 `testMatch: /noauth\.spec\.ts/`、`baseURL: http://localhost:4174`(`src/playwright.noauth.config.ts`,webServer 自动 `build:e2e:noauth`)。
|
||||
- Produces: 无。
|
||||
|
||||
- [ ] **Step 1: 写 e2e 用例**(夹具 20 作品全部 `user: 'fixture'`)
|
||||
|
||||
```ts
|
||||
test('/users/fixture 列出该作者作品并可跳转详情') // goto '/users/fixture' → '@fixture' 可见、卡片数 > 0、每张卡链接前缀 '/games/fixture/'
|
||||
test('/users/ghost 显示空态') // 「该作者暂无已上架作品」可见
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑 e2e**
|
||||
|
||||
Run: `docker run --rm --ipc=host -v /root/workspace/crearte-monorepo/crearte:/repo -v crearte-e2e-nm:/repo/src/node_modules -w /repo/src crearte:e2e npm run e2e:noauth`
|
||||
Expected: PASS(新用例 + 既有 noauth 用例全过)。
|
||||
|
||||
- [ ] **Step 3: 全量验证**:`… crearte:dev npm test` 全绿;`… crearte:dev npm run typecheck` 无错误。
|
||||
- [ ] **Step 4: CHANGELOG 条目**(`[0.14.0]`,双语:EN 行 + ZH 行):新增 `/users/:user` 作者主页与作者名互链。
|
||||
- [ ] **Step 5: 提交**
|
||||
|
||||
```bash
|
||||
git add src/e2e/author-page.noauth.spec.ts docs/CHANGELOG.md
|
||||
git commit -m "test(e2e): author page noauth coverage; docs: changelog 0.14.0"
|
||||
```
|
||||
|
||||
**收尾(非任务)**:三个任务完成后走 superpowers:finishing-a-development-branch —— `feat/author-page` 以 `git merge --no-ff` 并回 `master`、删除分支;随后回 monorepo 更新 `docs/ROADMAP.md` P4 状态与文档索引。
|
||||
@@ -0,0 +1,81 @@
|
||||
# P4 作者主页(`/users/:user`)设计 / Author Page Design
|
||||
|
||||
- **状态**:设计已批准(2026-09-29),待 spec 审阅后转实现计划。
|
||||
- **路线图**:`docs/ROADMAP.md` P4(第二波)。
|
||||
- **范围定性**:纯作品聚合页,极简列表;**零后端改动、零数据迁移**。
|
||||
|
||||
## 1. 背景与意图
|
||||
|
||||
crearte 的作品链接已按用户命名空间组织(`/games/<user>/<slug>`),但站内没有任何按作者聚合的入口:读者读完一个作品想看同作者的其他作品,只能回目录页手动搜索。P4 补上这块:**给每位创作者一个可分享的主页 `/users/:user`,列出其全部已上架作品**。
|
||||
|
||||
成功标准:从作品详情页点作者名能到作者页,页面只显示该作者的作品,按最新上架排序;未知用户名不报错。
|
||||
|
||||
约束(与维护者确认):
|
||||
|
||||
- 范围只到"纯作品聚合页":不做作者简介/头像等档案字段(不碰 `users` 表、不碰上传链路)。
|
||||
- 列表极简:无筛选侧栏、无查询参数(YAGNI,日后需要再加)。
|
||||
|
||||
## 2. 方案选型
|
||||
|
||||
| 方案 | 内容 | 取舍 |
|
||||
|---|---|---|
|
||||
| **A. 纯前端过滤(已选)** | `AuthorView` 复用 `repo.listGames()`,按 `resolveUserSlug(game).user` 过滤 | 零后端改动;与目录页同样拉全量列表(现状即如此,ETag/304);分页化另立子项目时再迁服务端过滤 |
|
||||
| B. 后端 `GET /api/games?author=` | 服务端过滤 + ETag 按参数派生 | 为分页铺路,但当下 YAGNI;动 handler/service/repository 三层 |
|
||||
| C. 新端点 `GET /api/users/:user` | 聚合用户信息 + 作品 | 引入用户名存在性探测面,超出纯聚合范围 |
|
||||
|
||||
**对路线图的偏离**:`docs/ROADMAP.md` P4 原文写"API 列表按作者过滤"。设计阶段发现列表 DTO(`GameSummary`)已含 `user?`/`slug?` 与 `resolveUserSlug()`,前端过滤即可满足需求,因此 P4 **不修改 crearte-server**(顺带不占用 Postgres 迁移号段)。路线图措辞随本 spec 落地同步修订。
|
||||
|
||||
## 3. 设计详述
|
||||
|
||||
### 3.1 路由与页面
|
||||
|
||||
- 新路由 `/users/:user`(与 `/games/:user/:slug` 风格一致),注册于 `src/app/router/index.ts`。
|
||||
- 新视图 `src/app/views/AuthorView.vue`,复用既有组件:`GameCard`、`StatePanel`、`BaseButton` 等。
|
||||
- 页面头部:`@<username>` + 作品数。**语义注意**:展示的是账号 `username`(命名空间所有者),不是作品元数据 `author_name`(自由文本、可空);卡片上的 `author_name` 照旧展示,两者不混用、不互相推导。
|
||||
- 列表按"最新上架"倒序,复用 `src/app/lib/filter.ts` 的既有排序逻辑(优先调用 `filterGames` 或其导出的比较器,**不复制**排序代码)。
|
||||
- 面包屑「目录 / @user」。
|
||||
- 入口:`GameView` 详情页作者名、`GameCard` 作者字样 → `/users/:user`。
|
||||
|
||||
### 3.2 数据流
|
||||
|
||||
`AuthorView` → `useAsync(repo.listGames())`(`src/app/data/` 的 `mergeRepo`:API + static 双源合并,API 宕机降级 static,与目录页同一链路)→ 按 `resolveUserSlug(game).user` 精确过滤(兼容旧式单段 id 作品:取 `id` 中 `/` 前段)→ `sort: 'new'` 排序 → 渲染。
|
||||
|
||||
### 3.3 边界与错误处理
|
||||
|
||||
- 未知用户名 / 该作者无已上架作品:空态「该作者暂无已上架作品」+ 返回目录按钮。**不做**用户存在性探测(需要新端点,YAGNI)。
|
||||
- 可见性语义与目录页完全一致:列表接口只返回已上架未下架作品,作者页自动继承。
|
||||
- 加载 / 失败态复用 `useAsync` + `StatePanel` 既有模式,不做新错误面。
|
||||
|
||||
## 4. 测试
|
||||
|
||||
- **Vitest**:AuthorView 过滤正确性(含 legacy 单段 id 形态与 `user` 字段缺失的回退)、空态渲染、`sort: 'new'` 排序、作者名链接跳转。
|
||||
- **Playwright `noauth`**:`/users/:user` 只显示该作者作品(基于 `src/fixtures/catalog` 现成作品数据)。
|
||||
- **后端**:无(零后端改动)。
|
||||
|
||||
## 5. 影响文件(预估)
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/app/router/index.ts` | 注册 `/users/:user` |
|
||||
| `src/app/views/AuthorView.vue` | 新建 |
|
||||
| `src/app/views/GameView.vue` | 作者名加链接 |
|
||||
| `src/app/components/GameCard.vue` | 作者字样加链接(如展示作者) |
|
||||
| `src/app/lib/filter.ts` | 导出排序逻辑供 AuthorView 复用(如未导出) |
|
||||
| 测试文件 | 与视图测试同目录、同命名约定(co-located)新建 |
|
||||
| `docs/CHANGELOG.md`(crearte 仓) | 双语条目 |
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
1. `/users/:user` 只显示该作者的已上架作品(API+static 合并源),按最新上架倒序。
|
||||
2. 旧式单段 id 作品不破坏过滤逻辑(`resolveUserSlug` 回退路径覆盖)。
|
||||
3. 未知用户名 / 无作品:空态渲染,无报错、无 404 路由错误。
|
||||
4. `GameView` 与 `GameCard` 的作者入口可跳转到作者页。
|
||||
5. 新增 Vitest 与 `noauth` Playwright 用例通过,且**存量测试全绿**(含 `vue-tsc` 类型检查)。
|
||||
6. `crearte-server` 零改动(本子项目不产生任何后端 diff)。
|
||||
|
||||
## 7. 明确不做(Out of scope)
|
||||
|
||||
- 作者简介 / 头像 / 关注 / 统计等档案与社交面(对应路线图 P5 之后再议)。
|
||||
- 目录分页 / 服务端过滤(独立子项目)。
|
||||
- 用户存在性探测、`GET /api/users/:user` 类端点。
|
||||
- 静态兜底目录(P8)。
|
||||
Reference in New Issue
Block a user