From 5fecceef4f64b36cbfa6cee3cba662b37ad2b296 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 3 Jun 2026 18:18:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E4=BD=9C=E5=93=81=E5=B1=95=E7=A4=BA=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 8 ++ docs/README.md | 2 + ...】PublicWorkPresentation收口计划-2026-06-03.md | 28 +++++++ src/components/rpg-entry/RpgEntryHomeView.tsx | 81 +++++-------------- .../rpgEntryWorldPresentation.test.ts | 47 ++++++++++- .../rpg-entry/rpgEntryWorldPresentation.ts | 48 ++++++++++- 6 files changed, 150 insertions(+), 64 deletions(-) create mode 100644 docs/technical/【前端架构】PublicWorkPresentation收口计划-2026-06-03.md diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index fe0d7927..12c85716 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -1249,6 +1249,14 @@ - 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|edutainment"`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "logged out home recommendation next starts the next puzzle work"`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 - 关联文档:`docs/technical/【前端架构】RecommendFeedViewModel收口计划-2026-06-03.md`。 +## 2026-06-03 Public Work Presentation 收口 + +- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。 +- 决策:在 `src/components/rpg-entry/rpgEntryWorldPresentation.ts` 追加单作品展示 Interface:`describePlatformPublicWorkKind` 与 `formatPlatformCompactCount`;页面删除本地实现。集合筛选、排序和指标选择继续留在 `rpgEntryPublicGalleryViewModel.ts`。 +- 影响范围:公开作品卡片 aria label、推荐点赞 / 改造文案、排行数值、分类主指标、搜索结果和桌面 hero 玩法 label。 +- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryWorldPresentation.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|ranking|category"`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】PublicWorkPresentation收口计划-2026-06-03.md`。 + ## 2026-06-03 Profile Funds ViewModel 收口 - 背景:个人资金展示规则散在 `RpgEntryHomeView.tsx`,且账单来源 label 表漏掉后端契约已有的 `puzzle_author_incentive_claim`,会把原始枚举值直接外显。 diff --git a/docs/README.md b/docs/README.md index 28cc8586..92dbefdd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,6 +47,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 公开作品分类、搜索、跨来源去重、今日筛选、排行排序和时间戳解析收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】PublicGalleryViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91PublicGalleryViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 +公开作品的玩法类型 label 与游玩 / 改造 / 点赞等紧凑计数格式收口到 `src/components/rpg-entry/rpgEntryWorldPresentation.ts`,规则见 [【前端架构】PublicWorkPresentation收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91PublicWorkPresentation%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 + 推荐 feed 的公开作品去重、普通内容过滤、active 窗口与上一条 / 下一条回环选择也收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】RecommendFeedViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RecommendFeedViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 每日任务卡片与任务中心弹窗的任务选择、进度、状态标签和按钮文案收口到 `src/components/rpg-entry/rpgEntryProfileTaskViewModel.ts`,规则见 [【前端架构】ProfileTaskViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91ProfileTaskViewModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】PublicWorkPresentation收口计划-2026-06-03.md b/docs/technical/【前端架构】PublicWorkPresentation收口计划-2026-06-03.md new file mode 100644 index 00000000..4feea2e9 --- /dev/null +++ b/docs/technical/【前端架构】PublicWorkPresentation收口计划-2026-06-03.md @@ -0,0 +1,28 @@ +# 【前端架构】Public Work Presentation 收口计划 + +## 背景 + +`RpgEntryHomeView.tsx` 的作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用公开作品玩法类型 label 与紧凑计数格式。原先 `describePublicGalleryCardKind` 与 `formatCompactCount` 放在页面 **Implementation** 内,导致新增玩法或调整数字展示时需要穿过多段 JSX。 + +## 决策 + +在 `src/components/rpg-entry/rpgEntryWorldPresentation.ts` 追加单作品展示 **Interface**: + +- `describePlatformPublicWorkKind(entry)`:统一公开作品玩法类型 label,并继续复用 `formatPlatformWorkDisplayTag` 的 4 字截断口径。 +- `formatPlatformCompactCount(value)`:统一游玩、改造、点赞、排行和分类指标的紧凑数字展示。 + +`RpgEntryHomeView.tsx` 删除本地类型 label 与紧凑计数 **Implementation**,仅消费 `rpgEntryWorldPresentation.ts`。集合筛选、排序和指标选择仍留在 `rpgEntryPublicGalleryViewModel.ts`,避免单作品展示 **Module** 与集合 **Module** 混杂。 + +## 约定 + +- 紧凑计数保留既有口径:`10000` 显示 `1.0万`,`100000000` 显示 `1.0亿`,一万以下不加千分位。 +- 玩法类型 label 继续遵循 4 字展示限制,例如“大鱼吃小鱼”外显为“大鱼吃小”。 +- 本次不迁移排行 metric label / value 配对;该规则属于集合排序 **Module** 的后续切片。 + +## 验证 + +- `npm run test -- src/components/rpg-entry/rpgEntryWorldPresentation.test.ts` +- `npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "recommend|ranking|category"` +- 针对变更文件执行 ESLint +- `npm run typecheck` +- `npm run check:encoding` diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index e681d018..c273bf00 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -180,19 +180,17 @@ import { } from './rpgEntryPublicGalleryViewModel'; import { buildPlatformWorldDisplayTags, + describePlatformPublicWorkKind, describePlatformThemeLabel, + formatPlatformCompactCount, formatPlatformWorkDisplayName, formatPlatformWorkDisplayTag, formatPlatformWorldTime, isBarkBattleGalleryEntry, isBigFishGalleryEntry, isEdutainmentGalleryEntry, - isJumpHopGalleryEntry, - isMatch3DGalleryEntry, isPuzzleGalleryEntry, - isSquareHoleGalleryEntry, isVisualNovelGalleryEntry, - isWoodenFishGalleryEntry, type PlatformPublicGalleryCard, resolvePlatformPublicWorkCode, resolvePlatformWorkAuthorDisplayName, @@ -638,14 +636,14 @@ function WorldCard({ const playCount = getPlatformWorldPlayCount(entry); const remixCount = getPlatformWorldRemixCount(entry); const likeCount = getPlatformWorldLikeCount(entry); - const typeLabel = describePublicGalleryCardKind(entry); + const typeLabel = describePlatformPublicWorkKind(entry); const authorName = resolvePlatformWorkAuthorDisplayName( entry, authorSummary, ); const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName); const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? ''; - const cardLabel = `${entry.worldName},${typeLabel},${formatCompactCount(playCount)}游玩,${formatCompactCount(remixCount)}改造,${formatCompactCount(likeCount)}点赞`; + const cardLabel = `${entry.worldName},${typeLabel},${formatPlatformCompactCount(playCount)}游玩,${formatPlatformCompactCount(remixCount)}改造,${formatPlatformCompactCount(likeCount)}点赞`; const coverStats = [ { label: '游玩', @@ -716,7 +714,7 @@ function WorldCard({ className={`h-3.5 w-3.5 ${label === '点赞' ? 'fill-current' : ''}`} aria-hidden="true" /> - {formatCompactCount(value)} + {formatPlatformCompactCount(value)} ))} @@ -769,7 +767,7 @@ function WorldCard({ )) ) : ( - {describePublicGalleryCardKind(entry)} + {describePlatformPublicWorkKind(entry)} )} @@ -892,7 +890,7 @@ function RecommendRuntimePreviewCard({ }) { const coverImage = resolvePlatformWorldCoverImage(entry); const displayName = formatPlatformWorkDisplayName(entry.worldName); - const typeLabel = describePublicGalleryCardKind(entry); + const typeLabel = describePlatformPublicWorkKind(entry); return (
- {formatCompactCount(metricValue)} + {formatPlatformCompactCount(metricValue)} {metricLabel} · - {describePublicGalleryCardKind(entry)} + {describePlatformPublicWorkKind(entry)}
{tags.map((tag, index) => ( @@ -1440,7 +1438,7 @@ function PlatformCategoryGameItem({ const tags = buildPlatformWorldDisplayTags(entry, 2); const metric = getPlatformCategoryPrimaryMetric(entry); const metaParts = [ - describePublicGalleryCardKind(entry), + describePlatformPublicWorkKind(entry), ...tags.filter((tag) => tag !== categoryTag), ].slice(0, 3); const actionLabel = @@ -1481,7 +1479,7 @@ function PlatformCategoryGameItem({
- {formatCompactCount(metric.value)} + {formatPlatformCompactCount(metric.value)} {metric.label} {metaParts.length > 0 ? {metaParts.join(' · ')} : null} @@ -1672,7 +1670,7 @@ function PlatformWorkSearchResults({
- {describePublicGalleryCardKind(entry)} + {describePlatformPublicWorkKind(entry)} ); @@ -1711,51 +1709,10 @@ async function getPublicWorkAuthorSummary( return null; } -function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) { - if (isBigFishGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('大鱼吃小鱼'); - } - if (isPuzzleGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('拼图'); - } - if (isMatch3DGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('抓大鹅'); - } - if (isSquareHoleGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('方洞挑战'); - } - if (isJumpHopGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('跳一跳'); - } - if (isWoodenFishGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('敲木鱼'); - } - if (isVisualNovelGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('视觉小说'); - } - if (isBarkBattleGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag('汪汪声浪'); - } - if (isEdutainmentGalleryEntry(entry)) { - return formatPlatformWorkDisplayTag(entry.templateName); - } - return formatPlatformWorkDisplayTag(describePlatformThemeLabel(entry.themeMode)); -} function getPublicAuthorAvatarLabel(authorDisplayName: string) { return Array.from(authorDisplayName.trim() || '玩')[0] ?? '玩'; } -function formatCompactCount(value: number) { - const normalizedValue = Math.max(0, Math.round(value)); - if (normalizedValue >= 100000000) { - return `${(normalizedValue / 100000000).toFixed(1)}亿`; - } - if (normalizedValue >= 10000) { - return `${(normalizedValue / 10000).toFixed(1)}万`; - } - return `${normalizedValue}`; -} - function formatSnapshotTime(value: string | null | undefined) { if (!value) { return '刚刚保存'; @@ -6041,7 +5998,7 @@ export function RpgEntryHomeView({ {leadPublicEntry - ? describePublicGalleryCardKind(leadPublicEntry) + ? describePlatformPublicWorkKind(leadPublicEntry) : '作品'} diff --git a/src/components/rpg-entry/rpgEntryWorldPresentation.test.ts b/src/components/rpg-entry/rpgEntryWorldPresentation.test.ts index 765d4436..ba1361b1 100644 --- a/src/components/rpg-entry/rpgEntryWorldPresentation.test.ts +++ b/src/components/rpg-entry/rpgEntryWorldPresentation.test.ts @@ -3,8 +3,10 @@ import { expect, test } from 'vitest'; import { buildPlatformWorldDisplayTags, buildPuzzleWorkCoverSlides, + describePlatformPublicWorkKind, EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID, EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME, + formatPlatformCompactCount, formatPlatformWorkDisplayName, formatPlatformWorkDisplayTags, formatPlatformWorldTime, @@ -16,10 +18,11 @@ import { mapBarkBattleWorkToPlatformGalleryCard, mapVisualNovelWorkToPlatformGalleryCard, mapWoodenFishWorkToPlatformGalleryCard, + type PlatformBigFishGalleryCard, type PlatformEdutainmentGalleryCard, type PlatformPuzzleGalleryCard, - resolvePlatformWorkAuthorDisplayName, resolvePlatformPublicWorkCode, + resolvePlatformWorkAuthorDisplayName, resolvePlatformWorldFallbackCoverImage, } from './rpgEntryWorldPresentation'; @@ -53,6 +56,48 @@ test('platform work display text limits names and tags by character count', () = ).toEqual(['超长机关', '星桥']); }); +test('platform public work presentation formats compact counts and kind labels', () => { + const puzzleCard: PlatformPuzzleGalleryCard = { + sourceType: 'puzzle', + workId: 'puzzle-work-kind', + profileId: 'puzzle-profile-kind', + publicWorkCode: 'PZ-KIND', + ownerUserId: 'user-1', + authorDisplayName: '玩家', + worldName: '机关拼图', + subtitle: '拼图关卡', + summaryText: '公开作品', + coverImageSrc: null, + themeTags: ['拼图'], + visibility: 'published', + publishedAt: '2026-05-18T00:00:00.000Z', + updatedAt: '2026-05-18T00:00:00.000Z', + }; + const bigFishCard: PlatformBigFishGalleryCard = { + sourceType: 'big-fish', + workId: 'big-fish-work-kind', + profileId: 'big-fish-profile-kind', + publicWorkCode: 'BF-KIND', + ownerUserId: 'user-1', + authorDisplayName: '玩家', + worldName: '大鱼海湾', + subtitle: '大鱼关卡', + summaryText: '公开作品', + coverImageSrc: null, + themeTags: ['大鱼'], + visibility: 'published', + publishedAt: '2026-05-18T00:00:00.000Z', + updatedAt: '2026-05-18T00:00:00.000Z', + }; + + expect(formatPlatformCompactCount(-1)).toBe('0'); + expect(formatPlatformCompactCount(9999)).toBe('9999'); + expect(formatPlatformCompactCount(10000)).toBe('1.0万'); + expect(formatPlatformCompactCount(100000000)).toBe('1.0亿'); + expect(describePlatformPublicWorkKind(puzzleCard)).toBe('拼图'); + expect(describePlatformPublicWorkKind(bigFishCard)).toBe('大鱼吃小'); +}); + test('platform public cards use play type reference images as cover fallback', () => { const puzzleCard: PlatformPuzzleGalleryCard = { sourceType: 'puzzle', diff --git a/src/components/rpg-entry/rpgEntryWorldPresentation.ts b/src/components/rpg-entry/rpgEntryWorldPresentation.ts index ff4b511e..245b955c 100644 --- a/src/components/rpg-entry/rpgEntryWorldPresentation.ts +++ b/src/components/rpg-entry/rpgEntryWorldPresentation.ts @@ -1,5 +1,5 @@ -import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth'; +import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle'; import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; import { BABY_OBJECT_MATCH_EDUTAINMENT_TAG } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; @@ -863,6 +863,52 @@ export function formatPlatformWorkDisplayTags( ].slice(0, limit); } +export function formatPlatformCompactCount(value: number) { + const normalizedValue = Math.max(0, Math.round(value)); + if (normalizedValue >= 100000000) { + return `${(normalizedValue / 100000000).toFixed(1)}亿`; + } + if (normalizedValue >= 10000) { + return `${(normalizedValue / 10000).toFixed(1)}万`; + } + + return `${normalizedValue}`; +} + +export function describePlatformPublicWorkKind( + entry: PlatformPublicGalleryCard, +) { + if (isBigFishGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('大鱼吃小鱼'); + } + if (isPuzzleGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('拼图'); + } + if (isMatch3DGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('抓大鹅'); + } + if (isSquareHoleGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('方洞挑战'); + } + if (isJumpHopGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('跳一跳'); + } + if (isWoodenFishGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('敲木鱼'); + } + if (isVisualNovelGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('视觉小说'); + } + if (isBarkBattleGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag('汪汪声浪'); + } + if (isEdutainmentGalleryEntry(entry)) { + return formatPlatformWorkDisplayTag(entry.templateName); + } + + return formatPlatformWorkDisplayTag(describePlatformThemeLabel(entry.themeMode)); +} + export function resolvePlatformWorkAuthorDisplayName( entry: PlatformPublicGalleryCard, authorSummary?: PublicUserSummary | null,