Files
crearte-monorepo/docs/plans/2026-09-29-author-page.md
T
XingfenD d814914e21 docs(plan): P4 author page implementation plan
- docs/plans/2026-09-29-author-page.md: 3 TDD tasks (AuthorView+route,
  entry links, e2e+changelog) with containerized npm commands per
  HANDOFF toolchain notes
- docs/ROADMAP.md: register plan in doc index, P4 status update
2026-09-29 16:04:20 +08:00

162 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 状态与文档索引。