docs: P9 UX batch-1 spec — browser feedback pack (router-level document.title two-phase + game-page description + skeleton variant align); B/C batches parked (owner picked A from UX survey)

This commit is contained in:
2026-10-01 12:01:44 +08:00
parent d6e08e1d03
commit 1fca6f1c53
@@ -0,0 +1,113 @@
# P9 用户体验增强(第一批:浏览器反馈包)· 设计 spec
日期:2026-10-01 | 决策:owner UX 摸底三包(A 浏览器反馈 / B 效率可访问 / C 暗色模式)选 **A**;B、C 留后续批次。「继续增强该项目,从用户体验方面」为总纲。
## 0. 一句话
让浏览器标签页「说人话」:每个路由有自己的 `document.title`(异步数据到达后精化),作品详情页带 `meta description`;列表加载骨架与实际网格对齐、不再误导非网格页。
## 1. 现状勘查(grounded,2026-10-01)
- `document.title` 全站无人设置:任何页面浏览器标签恒为 `index.html:10` 的 `crearte 创艺`(`grep -rn "document.title" app/` 零命中)。
- `index.html` 无 `meta name=description`。
- `e2e/landing.spec.ts:6` 钉住 home 标题**恰为** `crearte 创艺` —— home 无后缀既是现状也是本设计的兼容约束。
- `StatePanel.vue` 骨架 `v-for="n in 3"`,网格消费页实际 `sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`(CatalogView:70、LandingView:62、AuthorView:36);骨架的游戏卡形状还被 7 个非网格页复用(Docs、Admin、AdminUsers、AdminAudit、SubmitList、CreatorCenter、Game 详情),加载瞬间显示的是「不存在的卡片」。
- 数据面锚点:`GameView` 有 `game.name/description`(`data/types.ts` Game extends GameSummary,description 可选);`CatalogView` 经 `useFilterState` 持 `state.q`(route.query 派生,天然响应);`DocsView` 有 `doc?.title`;`AuthorView` 有 works 列表→`authorDisplayName()`(labels.ts:21)与 `props.user` 兜底。
- 路由 19 个 name(router/index.ts),其中 8 个纯静态(login/register/account/creator/outbound/admin*/submit*)无数据精化需求。
## 2. 决策记录
| # | 岔路 | 决定 | 理由 / 放弃项 |
|---|------|------|--------------|
| D-A | title 所有权 | **两段式**:router `afterEach` 按路由名设静态基线;4 个数据页(game/catalog/docs/author)watch 数据后精化覆盖 | 游戏名/文档名是异步数据,纯 `meta.title` 方案拿不到;纯 watch 方案则导航瞬间标签页还是旧题。放弃全站 meta 表驱动。 |
| D-B | home 标题 | 保持恰为 `crearte 创艺`(无后缀);其余页 `段 · crearte 创艺` | 不破坏 `landing.spec.ts:6` 既有钉桩与用户书签;也是品牌页的合理特例。 |
| D-C | description 范围 | **仅作品详情页**写 `game.description`(截 120 码点;缺省回落默认文案),离开该页恢复 index.html 默认 | SEO 增益主要落在可分享的作品页;全站 per-page description 属 YAGNI(SPA、无抓取压力)。 |
| D-D | 骨架策略 | cards 变体 **6 张**(3→6,覆盖 2/3/4 列断点各≥1.5 行);新增 `variant="lines"`(3 行横线、无封面纵横比结构)给 7 个非网格消费页 | 行数固定 6 而非按视口动态生成:列数已由 CSS 断点处理,JS 测视口属过度工程。放弃「所有页维持游戏卡」。 |
| D-E | 仓库面 | 纯 crearte 前端;server/deploy 零改动;版本 0.22.0 | 全部为客户端 DOM 写操作。 |
## 3. 设计
### 3.1 新文件 `app/lib/pageTitle.ts`(纯 builder + 两个 DOM setter)
```ts
export const SITE = 'crearte 创艺'
export const DEFAULT_DESCRIPTION =
'crearte 创艺——互动小说与浏览器小游戏托管社区,收录可直接游玩的作品目录。'
// joinTitle('作品') === '作品 · crearte 创艺';joinTitle() === 'crearte 创艺'(home/D-B)
export function joinTitle(...parts: Array<string | '' | undefined | null>): string
export function gameTitle(name: string): string // `${name} · SITE`
export function gameNotFoundTitle(): string // `未找到的作品 · SITE`
export function catalogTitle(q: string): string // q ? `搜索「${clip(q, 40)}」 · 作品 · SITE` : `作品 · SITE`
export function docsTitle(docTitle?: string): string // docTitle ? `${docTitle} · 文档 · SITE` : `文档 · SITE`
export function authorTitle(displayName: string): string // `${displayName} · 创作者 · SITE`
export function setPageTitle(t: string): void // document.title = t
export function setPageDescription(d: string): void // 找 meta[name=description](无则建入 head),content = clip(d || DEFAULT_DESCRIPTION, 120 码点)
```
`clip` 用 `Array.from` 码点截断加 `…`,防长名炸标签页。纯函数无 DOM 依赖,setter 只在 happy-dom/浏览器可达。
### 3.2 路由基线(同文件内 `sectionTitleOf`)
19 个 name 全表:home→`''`(经 joinTitle 得 D-B 整串);catalog/game→作品;docs/doc→文档;author→创作者;creator→创作者中心;login→登录;register→注册;account→我的账号;submit→我的投稿;submit-new→提交作品;submit-edit→编辑投稿;outbound→离开本站;admin→审核;admin-submission→投稿审核;admin-users→用户管理;admin-audit→审计日志;not-found→页面不存在。**未名中路由回退 `SITE`**(新增路由忘配时优雅降级)。
`router/index.ts` 挂:
```ts
router.afterEach((to) => {
setPageTitle(joinTitle(sectionTitleOf(to.name)))
})
```
### 3.3 数据页精化(watch,进路由基线之后覆盖)
- **GameView**:`watch(game)`:notFound 计算态→`gameNotFoundTitle()`;有数据→`gameTitle(name)` + `setPageDescription(description ?? '')`。loading/网络 error 期保持基线不闪。
- **CatalogView**:`watch(() => state.value.q)`→`catalogTitle(q)`。筛选变化即时反映(update() 走 router.replace,state 天然响应)。
- **DocsView**:`watch(doc)`→`docsTitle(doc?.title)`;`watch(docs)` 仅列表就绪且无 slug 时保持 `文档` 基线(路由 replace 到首篇后走 doc 精化)。
- **AuthorView**:`watch(games)`:取首个作品 `authorDisplayName()`,列表空/缺 author→`props.user` 兜底。
其余 15 页零改动,基线即终态。
### 3.4 StatePanel 骨架
```ts
defineProps<{ loading: boolean; error: Error | null; variant?: 'cards' | 'lines' }>()
```
- `cards`(默认,Catalog/Landing/Author 不改即得):`v-for="n in 6"`(3→6),其余结构原样。
- `variant="lines"`:border-2 surface 面板 ×3 行(h-4 w-2/3 + h-3 w-full + h-3 w-1/2 线组,`animate-skeleton`,无 aspect-video 封面块);GameView/DocsView/AdminView/AdminUsersView/AdminAuditView/SubmitListView 六页显式传 `variant="lines"`(StatePanel 消费页共九个:三个网格页保持 cards,此六页换 lines)。
- error/重试段零改动。
### 3.5 index.html
head 加 `<meta name="description" content="…默认文案…">`(`setPageDescription` 的回落锚,也让首屏无 JS 时爬虫可读)。
## 4. 错误处理与边界
- `game.description` 缺省/空串→DEFAULT;`q` 为纯空白→按空处理(parseFilterState 已 trim)。
- 路由 `to.name` 为 undefined(catch-all 之外的怪值)→ sectionTitleOf 返回 `''` → 恰 home 同款整串,可接受。
- afterEach 在守卫 redirect 后对最终路由触发(afterEach 收 to=重定向后目标),登录回跳 `?next=` 不影响标题正确性。
- 不设 `beforeunload` 还原——SPA 内 afterEach 天然逐页重置。
## 5. 测试计划
**vitest(happy-dom)**
- 新 `app/lib/pageTitle.test.ts`:joinTitle 空段/去重拼接、各 builder、clip(中英混/emoji 码点)、setter 写 document.title 与 meta(自动建 head 节点)、DEFAULT 回落。
- 扩 `app/router/index.test.ts`:`/`→`crearte 创艺` 整串;`/games`→`作品 · crearte 创艺`;`/account`→`我的账号 · crearte 创艺`;`/no-such`→`页面不存在 · crearte 创艺`。
- 扩 `GameView.test.ts`:加载后 `document.title === '最小作品 · crearte 创艺'` 且 meta description=作品描述(截断腿);notFound 腿标题为未找到款。
- 扩/新 `CatalogView` 腿:mount 后 push `/games?q=2048`→标题含 `搜索「2048」`。
- 新增 `StatePanel.test.ts`:默认渲染 6 个 aspect-video 卡;`variant="lines"` 零 aspect-video、3 个行块。
- 扩 `AuthorView.test.ts` 标题精化断言 1 条;新建 `CatalogView.test.ts` 与 `DocsView.test.ts`(两者现无测试文件,mock 惯例照抄 AuthorView.test.ts 的 vi.hoisted+vi.mock 模式),各钉标题精化 1 条(Catalog 另钉骨架 6 卡)。
**e2e(Playwright)**
- `landing.spec.ts` 首条**不动**(D-B 兼容回归即它的现有断言)。
- 新增 1 条到 `landing.spec.ts`:`/games/fixture/2048` → `toHaveTitle(/^2048 · crearte 创艺$/)`;`/games?q=2048` → `toHaveTitle(/搜索「2048」/)`。
- 实施时核对 `author-page.noauth.spec.ts` 与主套件有无标题假设(预期无)。
**验收腿**:vitest 全绿(基线 512+新增)、vue-tsc 零错、主 e2e(71+新增 1 条+1 skip)、noauth 4。
## 6. 版本与记账
crearte `0.22.0`:CHANGELOG 双语(Added:路由级标题/描述、骨架对齐;P9 第一批标注)。wrapper:本 spec + ROADMAP 加 P9 行(第一批完成、B/C 批次挂账)。server/deploy 零改动。