refactor: 收口公开作品 ViewModel

This commit is contained in:
2026-06-03 16:23:11 +08:00
parent 4a185ac8c2
commit e9534baace
6 changed files with 833 additions and 413 deletions
@@ -0,0 +1,327 @@
import { expect, test } from 'vitest';
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
import {
buildPlatformRankingEntries,
buildPublicCategoryGroups,
buildPublicGalleryCardKey,
filterPlatformWorkSearchResults,
filterTodayPublishedEntries,
getPlatformCategoryKindFilter,
getPlatformCategoryPrimaryMetric,
getPlatformPublicEntries,
getPlatformRankingMetricValue,
matchesPlatformCategoryKindFilter,
parsePlatformEntryTimestamp,
sortPlatformCategoryEntries,
} from './rpgEntryPublicGalleryViewModel';
import type {
PlatformJumpHopGalleryCard,
PlatformPuzzleGalleryCard,
PlatformWoodenFishGalleryCard,
} from './rpgEntryWorldPresentation';
function buildPuzzleEntry(
overrides: Partial<PlatformPuzzleGalleryCard> = {},
): PlatformPuzzleGalleryCard {
return {
sourceType: 'puzzle',
workId: 'puzzle-work',
profileId: 'shared-profile',
publicWorkCode: 'PZ-SHARED',
ownerUserId: 'user-1',
authorDisplayName: '拼图作者',
worldName: '星桥拼图',
subtitle: '拼图副标题',
summaryText: '星桥机关摘要',
coverImageSrc: null,
themeTags: ['星桥', '机关'],
visibility: 'published',
publishedAt: '2026-05-01T00:00:00.000Z',
updatedAt: '2026-05-01T00:00:00.000Z',
...overrides,
};
}
function buildJumpHopEntry(
overrides: Partial<PlatformJumpHopGalleryCard> = {},
): PlatformJumpHopGalleryCard {
return {
sourceType: 'jump-hop',
workId: 'jump-hop-work',
profileId: 'shared-profile',
publicWorkCode: 'JH-SHARED',
ownerUserId: 'user-1',
authorDisplayName: '跳一跳作者',
worldName: '星桥跳一跳',
subtitle: '跳一跳副标题',
summaryText: '跳一跳摘要',
coverImageSrc: null,
themeTags: ['跳跃'],
visibility: 'published',
publishedAt: '2026-05-02T00:00:00.000Z',
updatedAt: '2026-05-02T00:00:00.000Z',
...overrides,
};
}
function buildWoodenFishEntry(
overrides: Partial<PlatformWoodenFishGalleryCard> = {},
): PlatformWoodenFishGalleryCard {
return {
sourceType: 'wooden-fish',
workId: 'wooden-fish-work',
profileId: 'shared-profile',
publicWorkCode: 'WF-SHARED',
ownerUserId: 'user-1',
authorDisplayName: '木鱼作者',
worldName: '星桥木鱼',
subtitle: '木鱼副标题',
summaryText: '木鱼摘要',
coverImageSrc: null,
themeTags: ['敲木鱼'],
visibility: 'published',
publishedAt: '2026-05-03T00:00:00.000Z',
updatedAt: '2026-05-03T00:00:00.000Z',
...overrides,
};
}
function buildRpgEntry(
overrides: Partial<CustomWorldGalleryCard> = {},
): CustomWorldGalleryCard {
return {
ownerUserId: 'user-1',
profileId: 'shared-profile',
publicWorkCode: 'CW-SHARED',
authorPublicUserCode: null,
visibility: 'published',
publishedAt: '2026-05-04T00:00:00.000Z',
updatedAt: '2026-05-04T00:00:00.000Z',
authorDisplayName: 'RPG 作者',
worldName: '星桥 RPG',
subtitle: 'RPG 副标题',
summaryText: 'RPG 摘要',
coverImageSrc: null,
themeMode: 'martial',
playableNpcCount: 1,
landmarkCount: 1,
...overrides,
};
}
test('public gallery ViewModel keeps play kinds distinct in card keys', () => {
expect(buildPublicGalleryCardKey(buildPuzzleEntry())).toBe(
'puzzle:user-1:shared-profile',
);
expect(buildPublicGalleryCardKey(buildJumpHopEntry())).toBe(
'jump-hop:user-1:shared-profile',
);
expect(buildPublicGalleryCardKey(buildWoodenFishEntry())).toBe(
'wooden-fish:user-1:shared-profile',
);
expect(buildPublicGalleryCardKey(buildRpgEntry())).toBe(
'rpg:user-1:shared-profile',
);
});
test('public gallery ViewModel dedupes merged public entries by latest source', () => {
const oldPuzzle = buildPuzzleEntry({
worldName: '旧拼图',
updatedAt: '2026-05-01T00:00:00.000Z',
});
const latestPuzzle = buildPuzzleEntry({
worldName: '新拼图',
updatedAt: '2026-05-02T00:00:00.000Z',
});
expect(getPlatformPublicEntries([oldPuzzle], [latestPuzzle])).toEqual([
latestPuzzle,
]);
const categoryGroups = buildPublicCategoryGroups([oldPuzzle], [latestPuzzle]);
expect(categoryGroups.find((group) => group.tag === '星桥')).toEqual({
tag: '星桥',
entries: [latestPuzzle],
});
});
test('public gallery ViewModel searches compact work codes and ranks name prefix first', () => {
const nameMatch = buildPuzzleEntry({
profileId: 'name-match',
publicWorkCode: 'PZ-OLDER',
worldName: '星桥拼图',
updatedAt: '2026-05-01T00:00:00.000Z',
});
const codeMatch = buildPuzzleEntry({
profileId: 'code-match',
publicWorkCode: 'PZ-XING-QIAO',
worldName: '海雾机关',
updatedAt: '2026-05-03T00:00:00.000Z',
});
const jumpHopCodeMatch = buildJumpHopEntry({
profileId: 'jump-code-match',
publicWorkCode: 'JH-XING-QIAO',
worldName: '海雾跳跃',
});
const woodenFishCodeMatch = buildWoodenFishEntry({
profileId: 'wooden-code-match',
publicWorkCode: 'WF-DEEP-CALM',
worldName: '静心木鱼',
});
expect(filterPlatformWorkSearchResults([codeMatch, nameMatch], '星桥')).toEqual(
[nameMatch, codeMatch],
);
expect(filterPlatformWorkSearchResults([codeMatch], 'pz xing_qiao')).toEqual([
codeMatch,
]);
expect(
filterPlatformWorkSearchResults([jumpHopCodeMatch], 'jh xing-qiao'),
).toEqual([jumpHopCodeMatch]);
expect(
filterPlatformWorkSearchResults([woodenFishCodeMatch], 'wf deep_calm'),
).toEqual([woodenFishCodeMatch]);
});
test('public gallery ViewModel keeps source kinds behind one category filter seam', () => {
const jumpHopEntry = buildJumpHopEntry();
const woodenFishEntry = buildWoodenFishEntry();
const rpgEntry = buildRpgEntry();
expect(getPlatformCategoryKindFilter(jumpHopEntry)).toBe('jump-hop');
expect(getPlatformCategoryKindFilter(woodenFishEntry)).toBe('wooden-fish');
expect(getPlatformCategoryKindFilter(rpgEntry)).toBe('custom-world');
expect(matchesPlatformCategoryKindFilter(jumpHopEntry, 'jump-hop')).toBe(
true,
);
expect(matchesPlatformCategoryKindFilter(woodenFishEntry, 'wooden-fish')).toBe(
true,
);
expect(matchesPlatformCategoryKindFilter(jumpHopEntry, 'custom-world')).toBe(
false,
);
expect(matchesPlatformCategoryKindFilter(woodenFishEntry, 'custom-world')).toBe(
false,
);
});
test('public gallery ViewModel ranks entries by selected metric', () => {
const playWinner = buildJumpHopEntry({
profileId: 'play-winner',
playCount: 100,
remixCount: 1,
likeCount: 1,
recentPlayCount7d: 1,
});
const remixWinner = buildPuzzleEntry({
profileId: 'remix-winner',
playCount: 2,
remixCount: 50,
likeCount: 2,
recentPlayCount7d: 2,
});
const recentWinner = buildPuzzleEntry({
profileId: 'recent-winner',
playCount: 3,
remixCount: 3,
likeCount: 3,
recentPlayCount7d: 30,
});
const likeWinner = buildWoodenFishEntry({
profileId: 'like-winner',
playCount: 4,
remixCount: 4,
likeCount: 40,
recentPlayCount7d: 4,
});
const entries = [recentWinner, remixWinner, likeWinner, playWinner];
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);
});
test('public gallery ViewModel sorts category entries and exposes primary metric', () => {
const latestEntry = buildWoodenFishEntry({
profileId: 'latest',
playCount: 1,
likeCount: 0,
recentPlayCount7d: 0,
publishedAt: '2026-05-05T00:00:00.000Z',
updatedAt: '2026-05-05T00:00:00.000Z',
});
const playEntry = buildJumpHopEntry({
profileId: 'play',
playCount: 100,
likeCount: 0,
recentPlayCount7d: 0,
publishedAt: '2026-05-03T00:00:00.000Z',
updatedAt: '2026-05-03T00:00:00.000Z',
});
const likeEntry = buildPuzzleEntry({
profileId: 'like',
playCount: 1,
likeCount: 20,
recentPlayCount7d: 0,
publishedAt: '2026-05-02T00:00:00.000Z',
updatedAt: '2026-05-02T00:00:00.000Z',
});
const compositeEntry = buildPuzzleEntry({
profileId: 'composite',
playCount: 30,
remixCount: 30,
likeCount: 30,
recentPlayCount7d: 30,
publishedAt: '2026-05-01T00:00:00.000Z',
updatedAt: '2026-05-01T00:00:00.000Z',
});
const entries = [likeEntry, latestEntry, compositeEntry, playEntry];
expect(sortPlatformCategoryEntries(entries, 'latest')[0]).toBe(latestEntry);
expect(sortPlatformCategoryEntries(entries, 'play')[0]).toBe(playEntry);
expect(sortPlatformCategoryEntries(entries, 'like')[0]).toBe(compositeEntry);
expect(sortPlatformCategoryEntries(entries, 'composite')[0]).toBe(
compositeEntry,
);
expect(getPlatformCategoryPrimaryMetric(likeEntry)).toEqual({
label: '点赞',
value: 20,
});
expect(
getPlatformCategoryPrimaryMetric(
buildPuzzleEntry({ likeCount: 0, recentPlayCount7d: 8, playCount: 2 }),
),
).toEqual({ label: '近7日', value: 8 });
});
test('public gallery ViewModel filters entries published on the local day', () => {
const now = new Date(2026, 5, 3, 12);
const todayEntry = buildPuzzleEntry({
profileId: 'today',
publishedAt: new Date(2026, 5, 3, 8).toISOString(),
});
const yesterdayEntry = buildPuzzleEntry({
profileId: 'yesterday',
publishedAt: new Date(2026, 5, 2, 8).toISOString(),
});
const unpublishedEntry = buildPuzzleEntry({
profileId: 'unpublished',
publishedAt: null,
});
expect(
filterTodayPublishedEntries(
[yesterdayEntry, todayEntry, unpublishedEntry],
now,
),
).toEqual([todayEntry]);
});
test('public gallery ViewModel parses backend numeric timestamps', () => {
expect(parsePlatformEntryTimestamp('1778457601.234567Z')).toBe(
1778457601234.567,
);
});