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(likeCount)}
+ {formatPlatformCompactCount(likeCount)}
- {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,