- 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
10 KiB
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
- 旧式单段 id 作品(无
user/slug字段)不得错误匹配到任何作者页 —— Task 1 测试钉住resolveUserSlug回退(user: ''不匹配)。 - 未知用户名必须渲染空态(非 404、非报错)—— Task 1 测试钉住。
author.name与username语义分离:显示文本可以是author.name,但链接目标永远是/users/<user>;无user时不得产生内部链接 —— Task 2 测试钉住三种形态(内部链接 / 外链兜底 / 纯文本)。- GameCard 新增作者行不得破坏既有卡片测试与布局断言 —— Task 2 跑全量前端测试。
- 加载失败可重试:
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 },emitretry)、GameCard(props{ game, headingLevel? })、BaseButton。 -
Produces: 路由
{ path: '/users/:user', name: 'author', props: true };AuthorViewpropsuser: string。 -
Step 1: 写失败测试(
src/app/views/AuthorView.test.ts,仿GameView.test.ts的vi.hoisted+vi.mock('@/data', …)+ memory-router +RouterLinkstub 模式;mount(AuthorView, { props: { user: 'alice' }, global: { plugins: [router], stubs: … } }))
// 夹具(模块内常量):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)
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 之前)
{ path: '/users/:user', name: 'author', component: () => import('@/views/AuthorView.vue'), props: true },
- Step 5: 跑测试确认通过:Step 2 同命令 → PASS;再跑全量
… npm test→ 全绿。 - Step 6: 提交
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)):
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:
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: 提交
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')
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: 提交
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 状态与文档索引。