refactor: 收口排行配置模型
This commit is contained in:
@@ -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 Ranking ViewModel 收口
|
||||
|
||||
- 背景:排行 tab 的文案、metric label 与空态文案在 `RpgEntryHomeView.tsx`,排序和 metric value 在 `rpgEntryPublicGalleryViewModel.ts`,同一 `PlatformRankingTab` 的 Interface 分散且页面需要类型断言取 active config。
|
||||
- 决策:在 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts` 收口 `DEFAULT_PLATFORM_RANKING_TAB`、`PLATFORM_RANKING_TABS`、`getPlatformRankingTabConfig` 与 `getPlatformRankingMetric`;页面仅保留 active tab 状态和渲染。
|
||||
- 影响范围:发现页排行频道 tab 顺序、tab 文案、空态文案、排行项指标 label/value。
|
||||
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "bottom category tab becomes ranking and switches ranking metrics|ranking"`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||
- 关联文档:`docs/technical/【前端架构】RankingViewModel收口计划-2026-06-03.md`。
|
||||
|
||||
## 2026-06-03 Public Work Presentation 收口
|
||||
|
||||
- 背景:作品卡、推荐 runtime meta、排行项、分类项、搜索结果和桌面 hero 共用玩法类型 label 与紧凑计数格式,但规则仍在 `RpgEntryHomeView.tsx` 页面 Implementation 内。
|
||||
|
||||
@@ -51,6 +51,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_
|
||||
|
||||
推荐 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)。
|
||||
|
||||
排行频道的默认 tab、tab 文案、空态文案、排序字段与指标 label/value 收口到 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts`,规则见 [【前端架构】RankingViewModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91RankingViewModel%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)。
|
||||
|
||||
个人数据卡、钱包 chip 与“玩过”弹窗的计数、时长、作品类型和作品号展示收口到 `src/components/rpg-entry/rpgEntryProfileDashboardPresentation.ts`,规则见 [【前端架构】ProfileDashboardPresentation收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91ProfileDashboardPresentation%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。
|
||||
|
||||
32
docs/technical/【前端架构】RankingViewModel收口计划-2026-06-03.md
Normal file
32
docs/technical/【前端架构】RankingViewModel收口计划-2026-06-03.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 【前端架构】Ranking ViewModel 收口计划
|
||||
|
||||
## 背景
|
||||
|
||||
平台发现页排行频道以 `PlatformRankingTab` 决定 tab 文案、空态文案、排序字段和指标展示。原先排序与指标取值在 `rpgEntryPublicGalleryViewModel.ts`,而 tab label、metric label 与 empty text 留在 `RpgEntryHomeView.tsx`,页面还用类型断言寻找 active config,导致同一个排行语义的 **Interface** 分散。
|
||||
|
||||
## 决策
|
||||
|
||||
在 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts` 收口排行 **Interface**:
|
||||
|
||||
- `DEFAULT_PLATFORM_RANKING_TAB` 与 `PLATFORM_RANKING_TABS`:统一 tab 顺序、tab label、metric label 与空态文案。
|
||||
- `getPlatformRankingTabConfig(tab)`:统一 active tab 配置兜底。
|
||||
- `getPlatformRankingMetric(entry, tab)`:统一 metric label 与 value,避免 label/value 漂移。
|
||||
- `buildPlatformRankingEntries(entries, tab)` 继续承载排序规则。
|
||||
|
||||
`RpgEntryHomeView.tsx` 只保留 active tab 状态、点击与渲染,不再理解“热门榜=游玩值”“新品榜=近 7 日值”等映射。排行规则的 **Locality** 收口到 PublicGallery ViewModel。
|
||||
|
||||
## 约定
|
||||
|
||||
- 默认排行 tab 保持 `hot`。
|
||||
- tab 顺序保持“热门榜 / 改造榜 / 新品榜 / 点赞榜”。
|
||||
- 排序口径保持:`hot=playCount`、`remix=remixCount`、`new=recentPlayCount7d`、`like=likeCount`。
|
||||
- “新品榜”仍按近 7 日游玩数排序,不改为发布时间排序。
|
||||
- 页面层继续保留最多显示 30 条的展示限制。
|
||||
|
||||
## 验证
|
||||
|
||||
- `npm run test -- src/components/rpg-entry/rpgEntryPublicGalleryViewModel.test.ts`
|
||||
- `npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "bottom category tab becomes ranking and switches ranking metrics|ranking"`
|
||||
- 针对变更文件执行 ESLint
|
||||
- `npm run typecheck`
|
||||
- `npm run check:encoding`
|
||||
@@ -160,20 +160,24 @@ import {
|
||||
buildPublicCategoryGroups,
|
||||
buildPublicGalleryCardKey,
|
||||
dedupePlatformPublicGalleryEntries,
|
||||
DEFAULT_PLATFORM_RANKING_TAB,
|
||||
filterPlatformWorkSearchResults,
|
||||
filterTodayPublishedEntries,
|
||||
getAllPlatformPublicEntries,
|
||||
getPlatformCategoryPrimaryMetric,
|
||||
getPlatformPublicEntries,
|
||||
getPlatformRankingMetricValue,
|
||||
getPlatformRankingMetric,
|
||||
getPlatformRankingTabConfig,
|
||||
getPlatformSearchableWorkIds,
|
||||
getPlatformWorldLikeCount,
|
||||
getPlatformWorldPlayCount,
|
||||
getPlatformWorldRemixCount,
|
||||
isExactPublicWorkCodeSearch,
|
||||
matchesPlatformCategoryKindFilter,
|
||||
PLATFORM_RANKING_TABS,
|
||||
type PlatformCategoryKindFilter,
|
||||
type PlatformCategorySortMode,
|
||||
type PlatformRankingMetric,
|
||||
type PlatformRankingTab,
|
||||
selectPlatformRecommendFeedWindow,
|
||||
sortPlatformCategoryEntries,
|
||||
@@ -405,37 +409,6 @@ const CHILD_MOTION_DEMO_DEFAULT_CARD = {
|
||||
summary: '站位、招手和左右手活动。',
|
||||
};
|
||||
|
||||
const PLATFORM_RANKING_TABS: Array<{
|
||||
id: PlatformRankingTab;
|
||||
label: string;
|
||||
metricLabel: string;
|
||||
emptyText: string;
|
||||
}> = [
|
||||
{
|
||||
id: 'hot',
|
||||
label: '热门榜',
|
||||
metricLabel: '游玩',
|
||||
emptyText: '公开广场暂时还没有热门作品。',
|
||||
},
|
||||
{
|
||||
id: 'remix',
|
||||
label: '改造榜',
|
||||
metricLabel: '改造',
|
||||
emptyText: '公开广场暂时还没有改造作品。',
|
||||
},
|
||||
{
|
||||
id: 'new',
|
||||
label: '新品榜',
|
||||
metricLabel: '近7日',
|
||||
emptyText: '近 7 日暂时还没有新品。',
|
||||
},
|
||||
{
|
||||
id: 'like',
|
||||
label: '点赞榜',
|
||||
metricLabel: '点赞',
|
||||
emptyText: '公开广场暂时还没有点赞作品。',
|
||||
},
|
||||
];
|
||||
function ResolvedAssetBackdrop({
|
||||
src,
|
||||
fallbackSrc,
|
||||
@@ -1362,14 +1335,12 @@ function DesktopTrendingItem({
|
||||
function PlatformRankingItem({
|
||||
entry,
|
||||
rank,
|
||||
metricLabel,
|
||||
metricValue,
|
||||
metric,
|
||||
onClick,
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
rank: number;
|
||||
metricLabel: string;
|
||||
metricValue: number;
|
||||
metric: PlatformRankingMetric;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
@@ -1405,9 +1376,9 @@ function PlatformRankingItem({
|
||||
</div>
|
||||
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-xs font-semibold text-[var(--platform-text-soft)]">
|
||||
<span className="text-[var(--platform-warm-text)]">
|
||||
{formatPlatformCompactCount(metricValue)}
|
||||
{formatPlatformCompactCount(metric.value)}
|
||||
</span>
|
||||
<span>{metricLabel}</span>
|
||||
<span>{metric.label}</span>
|
||||
<span>·</span>
|
||||
<span>{describePlatformPublicWorkKind(entry)}</span>
|
||||
</div>
|
||||
@@ -3567,8 +3538,9 @@ export function RpgEntryHomeView({
|
||||
const [publicAuthorSummariesByKey, setPublicAuthorSummariesByKey] = useState<
|
||||
Record<string, PublicUserSummary | null>
|
||||
>({});
|
||||
const [activeRankingTab, setActiveRankingTab] =
|
||||
useState<PlatformRankingTab>('hot');
|
||||
const [activeRankingTab, setActiveRankingTab] = useState<PlatformRankingTab>(
|
||||
DEFAULT_PLATFORM_RANKING_TAB,
|
||||
);
|
||||
const [visitedTabs, setVisitedTabs] = useState<Set<PlatformHomeTab>>(
|
||||
() => new Set([activeTab]),
|
||||
);
|
||||
@@ -4794,9 +4766,7 @@ export function RpgEntryHomeView({
|
||||
activeTab,
|
||||
mobileFeedCarouselEnabled,
|
||||
]);
|
||||
const activeRankingConfig = PLATFORM_RANKING_TABS.find(
|
||||
(tab) => tab.id === activeRankingTab,
|
||||
) as (typeof PLATFORM_RANKING_TABS)[number];
|
||||
const activeRankingConfig = getPlatformRankingTabConfig(activeRankingTab);
|
||||
const rankingEntries = useMemo(
|
||||
() =>
|
||||
buildPlatformRankingEntries(publicEntries, activeRankingTab).slice(0, 30),
|
||||
@@ -5037,11 +5007,7 @@ export function RpgEntryHomeView({
|
||||
key={`${buildPublicGalleryCardKey(entry)}:ranking:${activeRankingTab}`}
|
||||
entry={entry}
|
||||
rank={index + 1}
|
||||
metricLabel={activeRankingConfig.metricLabel}
|
||||
metricValue={getPlatformRankingMetricValue(
|
||||
entry,
|
||||
activeRankingTab,
|
||||
)}
|
||||
metric={getPlatformRankingMetric(entry, activeRankingTab)}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -7,14 +7,18 @@ import {
|
||||
buildPublicCategoryGroups,
|
||||
buildPublicGalleryCardKey,
|
||||
dedupePlatformPublicGalleryEntries,
|
||||
DEFAULT_PLATFORM_RANKING_TAB,
|
||||
filterPlatformWorkSearchResults,
|
||||
filterTodayPublishedEntries,
|
||||
getPlatformCategoryKindFilter,
|
||||
getPlatformCategoryPrimaryMetric,
|
||||
getPlatformPublicEntries,
|
||||
getPlatformRankingMetric,
|
||||
getPlatformRankingMetricValue,
|
||||
getPlatformRankingTabConfig,
|
||||
matchesPlatformCategoryKindFilter,
|
||||
parsePlatformEntryTimestamp,
|
||||
PLATFORM_RANKING_TABS,
|
||||
selectAdjacentPlatformRecommendEntry,
|
||||
selectPlatformRecommendFeedWindow,
|
||||
sortPlatformCategoryEntries,
|
||||
@@ -335,11 +339,32 @@ test('public gallery ViewModel ranks entries by selected metric', () => {
|
||||
});
|
||||
const entries = [recentWinner, remixWinner, likeWinner, playWinner];
|
||||
|
||||
expect(DEFAULT_PLATFORM_RANKING_TAB).toBe('hot');
|
||||
expect(PLATFORM_RANKING_TABS.map((tab) => tab.label)).toEqual([
|
||||
'热门榜',
|
||||
'改造榜',
|
||||
'新品榜',
|
||||
'点赞榜',
|
||||
]);
|
||||
expect(getPlatformRankingTabConfig('new')).toEqual({
|
||||
id: 'new',
|
||||
label: '新品榜',
|
||||
metricLabel: '近7日',
|
||||
emptyText: '近 7 日暂时还没有新品。',
|
||||
});
|
||||
expect(buildPlatformRankingEntries(entries, 'hot')[0]).toBe(playWinner);
|
||||
expect(buildPlatformRankingEntries(entries, 'remix')[0]).toBe(remixWinner);
|
||||
expect(buildPlatformRankingEntries(entries, 'new')[0]).toBe(recentWinner);
|
||||
expect(buildPlatformRankingEntries(entries, 'like')[0]).toBe(likeWinner);
|
||||
expect(getPlatformRankingMetricValue(likeWinner, 'like')).toBe(40);
|
||||
expect(getPlatformRankingMetric(recentWinner, 'new')).toEqual({
|
||||
label: '近7日',
|
||||
value: 30,
|
||||
});
|
||||
expect(getPlatformRankingMetric(playWinner, 'hot')).toEqual({
|
||||
label: '游玩',
|
||||
value: 100,
|
||||
});
|
||||
});
|
||||
|
||||
test('public gallery ViewModel sorts category entries and exposes primary metric', () => {
|
||||
|
||||
@@ -15,6 +15,16 @@ import {
|
||||
} from './rpgEntryWorldPresentation';
|
||||
|
||||
export type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
|
||||
export type PlatformRankingTabConfig = {
|
||||
emptyText: string;
|
||||
id: PlatformRankingTab;
|
||||
label: string;
|
||||
metricLabel: string;
|
||||
};
|
||||
export type PlatformRankingMetric = {
|
||||
label: string;
|
||||
value: number;
|
||||
};
|
||||
export type PlatformCategoryKindFilter =
|
||||
| 'all'
|
||||
| 'puzzle'
|
||||
@@ -33,6 +43,40 @@ export type PlatformPublicCategoryGroup = {
|
||||
entries: PlatformPublicGalleryCard[];
|
||||
};
|
||||
|
||||
export const DEFAULT_PLATFORM_RANKING_TAB: PlatformRankingTab = 'hot';
|
||||
|
||||
export const PLATFORM_RANKING_TABS: PlatformRankingTabConfig[] = [
|
||||
{
|
||||
id: 'hot',
|
||||
label: '热门榜',
|
||||
metricLabel: '游玩',
|
||||
emptyText: '公开广场暂时还没有热门作品。',
|
||||
},
|
||||
{
|
||||
id: 'remix',
|
||||
label: '改造榜',
|
||||
metricLabel: '改造',
|
||||
emptyText: '公开广场暂时还没有改造作品。',
|
||||
},
|
||||
{
|
||||
id: 'new',
|
||||
label: '新品榜',
|
||||
metricLabel: '近7日',
|
||||
emptyText: '近 7 日暂时还没有新品。',
|
||||
},
|
||||
{
|
||||
id: 'like',
|
||||
label: '点赞榜',
|
||||
metricLabel: '点赞',
|
||||
emptyText: '公开广场暂时还没有点赞作品。',
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_PLATFORM_RANKING_CONFIG =
|
||||
PLATFORM_RANKING_TABS.find(
|
||||
(config) => config.id === DEFAULT_PLATFORM_RANKING_TAB,
|
||||
) ?? PLATFORM_RANKING_TABS[0]!;
|
||||
|
||||
export type PlatformRecommendFeedWindow = {
|
||||
activeEntry: PlatformPublicGalleryCard | null;
|
||||
activeEntryKey: string | null;
|
||||
@@ -372,6 +416,15 @@ export function buildPlatformRankingEntries(
|
||||
return sortEntriesByMetric(entries, getPlatformWorldRecentPlayCount);
|
||||
}
|
||||
|
||||
export function getPlatformRankingTabConfig(
|
||||
tab: PlatformRankingTab,
|
||||
): PlatformRankingTabConfig {
|
||||
return (
|
||||
PLATFORM_RANKING_TABS.find((config) => config.id === tab) ??
|
||||
DEFAULT_PLATFORM_RANKING_CONFIG
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlatformRankingMetricValue(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
tab: PlatformRankingTab,
|
||||
@@ -391,6 +444,16 @@ export function getPlatformRankingMetricValue(
|
||||
return getPlatformWorldPlayCount(entry);
|
||||
}
|
||||
|
||||
export function getPlatformRankingMetric(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
tab: PlatformRankingTab,
|
||||
): PlatformRankingMetric {
|
||||
return {
|
||||
label: getPlatformRankingTabConfig(tab).metricLabel,
|
||||
value: getPlatformRankingMetricValue(entry, tab),
|
||||
};
|
||||
}
|
||||
|
||||
function getPlatformCategoryCompositeScore(entry: PlatformPublicGalleryCard) {
|
||||
// 分类频道只使用公开读模型已经返回的指标做前端排序,不在 UI 层伪造评分数据。
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user