refactor: 收口推荐流展示模型
This commit is contained in:
@@ -1241,6 +1241,14 @@
|
||||
- 验证方式:`npm run test -- src/components/rpg-entry/rpgEntryProfileDashboardPresentation.test.ts`、针对变更文件执行 ESLint、`npm run typecheck`、`npm run check:encoding`。
|
||||
- 关联文档:`docs/technical/【前端架构】ProfileDashboardPresentation收口计划-2026-06-03.md`。
|
||||
|
||||
## 2026-06-03 Recommend Feed ViewModel 收口
|
||||
|
||||
- 背景:推荐 feed 与正式 runtime 的上一条 / 下一条选择分别在 `RpgEntryHomeView.tsx` 和 `PlatformEntryFlowShellImpl.tsx` 手写公开作品去重、隐藏内容过滤、active key 兜底和相邻回环,存在推荐预览与 runtime 口径漂移风险。
|
||||
- 决策:在 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts` 追加推荐 feed Module Interface:`dedupePlatformPublicGalleryEntries`、`buildPlatformRecommendFeedEntries`、`selectPlatformRecommendFeedWindow`、`selectAdjacentPlatformRecommendEntry`;首页与 FlowShell 均消费该 Interface。
|
||||
- 影响范围:移动端首页推荐 swipe、发现页推荐频道、桌面推荐格、推荐 runtime 队列与上一条 / 下一条跳转。
|
||||
- 验证方式:`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-05-26 前端不外露图片模型名
|
||||
|
||||
- 背景:拼图与相关结果页、生成进度和错误提示里直接显示 `gpt-image-2`、`gemini-3.1-flash-image-preview`、`image-2` 等名称,会把内部模型路由暴露给普通用户。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
推荐 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)。
|
||||
|
||||
个人数据卡、钱包 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)。
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# 【前端架构】Recommend Feed ViewModel 收口计划
|
||||
|
||||
## 背景
|
||||
|
||||
平台首页推荐 feed、发现页推荐频道、桌面推荐格和正式 runtime 的上一条 / 下一条选择共用一批展示规则:公开作品跨来源去重、过滤寓教于乐隐藏内容、按精选优先再最新兜底、active key 失效时回到首项、前后相邻条目回环且单条目不自循环。原先这些规则分别散在 `RpgEntryHomeView.tsx` 与 `PlatformEntryFlowShellImpl.tsx` 的 **Implementation** 内,导致推荐预览与正式 runtime 之间存在口径漂移风险。
|
||||
|
||||
## 决策
|
||||
|
||||
在 `src/components/rpg-entry/rpgEntryPublicGalleryViewModel.ts` 追加推荐 feed **Interface**:
|
||||
|
||||
- `dedupePlatformPublicGalleryEntries(entries)`:统一公开作品按 `buildPublicGalleryCardKey` 去重,后出现来源覆盖旧值。
|
||||
- `buildPlatformRecommendFeedEntries(featuredEntries, latestEntries)`:统一推荐 feed 的精选 + 最新合并、隐藏寓教于乐内容与去重顺序。
|
||||
- `selectPlatformRecommendFeedWindow(entries, activeEntryKey)`:统一推荐页当前项、上一项、下一项和 active key 失效兜底。
|
||||
- `selectAdjacentPlatformRecommendEntry(entries, direction, baseEntryKey)`:统一正式 runtime 上一条 / 下一条回环选择,并避免单作品自循环。
|
||||
|
||||
`RpgEntryHomeView.tsx` 不再自建 `Map` 或手写取模;`PlatformEntryFlowShellImpl.tsx` 的 runtime 推荐条目也改用同一 **Module**。推荐 feed 的 **Locality** 回到 PublicGallery ViewModel,页面与 runtime 只保留 UI、动画和启动副作用。
|
||||
|
||||
## 约定
|
||||
|
||||
- 推荐 feed 仍只展示普通公开作品;寓教于乐内容由独立频道控制,不进入推荐 runtime 队列。
|
||||
- 去重保留既有“后出现来源覆盖旧值、插入位置不变”的行为。
|
||||
- active key 缺失或失效时,展示窗口回到首个推荐作品;单个作品没有上一条 / 下一条预览。
|
||||
|
||||
## 验证
|
||||
|
||||
- `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`
|
||||
@@ -359,6 +359,10 @@ import {
|
||||
isPersistedPuzzleDraftGenerating,
|
||||
resolvePuzzleWorkCoverImageSrc,
|
||||
} from '../custom-world-home/creationWorkShelf';
|
||||
import {
|
||||
buildPlatformRecommendFeedEntries,
|
||||
selectAdjacentPlatformRecommendEntry,
|
||||
} from '../rpg-entry/rpgEntryPublicGalleryViewModel';
|
||||
import {
|
||||
isBarkBattleGalleryEntry,
|
||||
isBigFishGalleryEntry,
|
||||
@@ -406,7 +410,6 @@ import {
|
||||
import {
|
||||
canExposePublicWork,
|
||||
EDUTAINMENT_HIDDEN_MESSAGE,
|
||||
filterGeneralPublicWorks,
|
||||
} from './platformEdutainmentVisibility';
|
||||
import { PlatformEntryCreationTypeModal } from './PlatformEntryCreationTypeModal';
|
||||
import type { PlatformCreationTypeId } from './platformEntryCreationTypes';
|
||||
@@ -5004,16 +5007,14 @@ export function PlatformEntryFlowShellImpl({
|
||||
woodenFishGalleryEntries,
|
||||
],
|
||||
);
|
||||
const recommendRuntimeEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
filterGeneralPublicWorks([
|
||||
...featuredGalleryEntries,
|
||||
...latestGalleryEntries,
|
||||
]).forEach((entry) => {
|
||||
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
|
||||
});
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredGalleryEntries, latestGalleryEntries]);
|
||||
const recommendRuntimeEntries = useMemo(
|
||||
() =>
|
||||
buildPlatformRecommendFeedEntries(
|
||||
featuredGalleryEntries,
|
||||
latestGalleryEntries,
|
||||
),
|
||||
[featuredGalleryEntries, latestGalleryEntries],
|
||||
);
|
||||
|
||||
const creationHubItems = useMemo<CustomWorldWorkSummary[]>(
|
||||
() =>
|
||||
@@ -14429,29 +14430,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
const selectAdjacentRecommendRuntimeEntry = useCallback(
|
||||
(direction: 1 | -1, baseEntryKey?: string | null) => {
|
||||
if (recommendRuntimeEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedBaseEntryKey =
|
||||
baseEntryKey?.trim() || activeRecommendEntryKey;
|
||||
const activeIndex = recommendRuntimeEntries.findIndex(
|
||||
(entry) =>
|
||||
getPlatformPublicGalleryEntryKey(entry) === normalizedBaseEntryKey,
|
||||
const nextEntry = selectAdjacentPlatformRecommendEntry(
|
||||
recommendRuntimeEntries,
|
||||
direction,
|
||||
normalizedBaseEntryKey,
|
||||
);
|
||||
const baseIndex = activeIndex >= 0 ? activeIndex : 0;
|
||||
const nextIndex =
|
||||
(baseIndex + direction + recommendRuntimeEntries.length) %
|
||||
recommendRuntimeEntries.length;
|
||||
const nextEntry = recommendRuntimeEntries[nextIndex];
|
||||
if (!nextEntry) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
getPlatformPublicGalleryEntryKey(nextEntry) === normalizedBaseEntryKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
void selectRecommendRuntimeEntry(nextEntry);
|
||||
},
|
||||
|
||||
@@ -150,8 +150,10 @@ import {
|
||||
} from './rpgEntryProfileTaskViewModel';
|
||||
import {
|
||||
buildPlatformRankingEntries,
|
||||
buildPlatformRecommendFeedEntries,
|
||||
buildPublicCategoryGroups,
|
||||
buildPublicGalleryCardKey,
|
||||
dedupePlatformPublicGalleryEntries,
|
||||
filterPlatformWorkSearchResults,
|
||||
filterTodayPublishedEntries,
|
||||
getAllPlatformPublicEntries,
|
||||
@@ -167,6 +169,7 @@ import {
|
||||
type PlatformCategoryKindFilter,
|
||||
type PlatformCategorySortMode,
|
||||
type PlatformRankingTab,
|
||||
selectPlatformRecommendFeedWindow,
|
||||
sortPlatformCategoryEntries,
|
||||
} from './rpgEntryPublicGalleryViewModel';
|
||||
import {
|
||||
@@ -4742,14 +4745,11 @@ export function RpgEntryHomeView({
|
||||
featuredShelf.length > 0 ? featuredShelf : generalLatestEntries
|
||||
).slice(0, 5);
|
||||
// 网页端保留原有宽屏布局,只把模块数据同步到移动端首页频道语义。
|
||||
const desktopRecommendEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
[...featuredShelf, ...generalLatestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, generalLatestEntries]);
|
||||
const recommendedFeedEntries = useMemo(
|
||||
() => buildPlatformRecommendFeedEntries(featuredShelf, generalLatestEntries),
|
||||
[featuredShelf, generalLatestEntries],
|
||||
);
|
||||
const desktopRecommendEntries = recommendedFeedEntries;
|
||||
const desktopTodayEntries = useMemo(
|
||||
() => filterTodayPublishedEntries(generalLatestEntries),
|
||||
[generalLatestEntries],
|
||||
@@ -4757,35 +4757,18 @@ export function RpgEntryHomeView({
|
||||
const desktopFeaturedGrid = desktopRecommendEntries.slice(0, 4);
|
||||
const desktopCategoryGrid = activeCategoryEntries.slice(0, 6);
|
||||
const desktopLibraryPreview = myEntries.slice(0, 2);
|
||||
const recommendedFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
[...featuredShelf, ...generalLatestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, generalLatestEntries]);
|
||||
const discoverFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
const sourceEntries =
|
||||
discoverChannel === 'recommend'
|
||||
? recommendedFeedEntries
|
||||
: filterTodayPublishedEntries(generalLatestEntries);
|
||||
|
||||
sourceEntries.forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
return dedupePlatformPublicGalleryEntries(sourceEntries);
|
||||
}, [discoverChannel, generalLatestEntries, recommendedFeedEntries]);
|
||||
const edutainmentFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
edutainmentEntries.forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [edutainmentEntries]);
|
||||
const edutainmentFeedEntries = useMemo(
|
||||
() => dedupePlatformPublicGalleryEntries(edutainmentEntries),
|
||||
[edutainmentEntries],
|
||||
);
|
||||
const mobileFeedCarouselEnabled =
|
||||
!isDesktopLayout &&
|
||||
activeTab === 'category' &&
|
||||
@@ -4883,41 +4866,24 @@ export function RpgEntryHomeView({
|
||||
buildPlatformRankingEntries(publicEntries, activeRankingTab).slice(0, 30),
|
||||
[activeRankingTab, publicEntries],
|
||||
);
|
||||
const activeRecommendEntry =
|
||||
recommendedFeedEntries.find(
|
||||
(entry) => buildPublicGalleryCardKey(entry) === activeRecommendEntryKey,
|
||||
) ??
|
||||
recommendedFeedEntries[0] ??
|
||||
null;
|
||||
const activeRecommendIndex = activeRecommendEntry
|
||||
? recommendedFeedEntries.findIndex(
|
||||
(entry) =>
|
||||
buildPublicGalleryCardKey(entry) ===
|
||||
buildPublicGalleryCardKey(activeRecommendEntry),
|
||||
)
|
||||
: -1;
|
||||
const previousRecommendEntry =
|
||||
activeRecommendIndex >= 0 && recommendedFeedEntries.length > 1
|
||||
? recommendedFeedEntries[
|
||||
(activeRecommendIndex - 1 + recommendedFeedEntries.length) %
|
||||
recommendedFeedEntries.length
|
||||
]
|
||||
: null;
|
||||
const nextRecommendEntry =
|
||||
activeRecommendIndex >= 0 && recommendedFeedEntries.length > 1
|
||||
? recommendedFeedEntries[
|
||||
(activeRecommendIndex + 1) % recommendedFeedEntries.length
|
||||
]
|
||||
: null;
|
||||
const recommendFeedWindow = useMemo(
|
||||
() =>
|
||||
selectPlatformRecommendFeedWindow(
|
||||
recommendedFeedEntries,
|
||||
activeRecommendEntryKey,
|
||||
),
|
||||
[activeRecommendEntryKey, recommendedFeedEntries],
|
||||
);
|
||||
const activeRecommendEntry = recommendFeedWindow.activeEntry;
|
||||
const previousRecommendEntry = recommendFeedWindow.previousEntry;
|
||||
const nextRecommendEntry = recommendFeedWindow.nextEntry;
|
||||
const [recommendDragOffsetY, setRecommendDragOffsetY] = useState(0);
|
||||
const [recommendDragCommitDirection, setRecommendDragCommitDirection] =
|
||||
useState<1 | -1 | null>(null);
|
||||
const [recommendShareState, setRecommendShareState] = useState<
|
||||
'idle' | 'copied' | 'failed'
|
||||
>('idle');
|
||||
const activeRecommendEntryKeyForSelection = activeRecommendEntry
|
||||
? buildPublicGalleryCardKey(activeRecommendEntry)
|
||||
: null;
|
||||
const activeRecommendEntryKeyForSelection = recommendFeedWindow.activeEntryKey;
|
||||
const recommendShareResetTimerRef = useRef<number | null>(null);
|
||||
const recommendCardStageRef = useRef<HTMLDivElement | null>(null);
|
||||
const recommendDragStartRef = useRef<{
|
||||
|
||||
@@ -3,8 +3,10 @@ import { expect, test } from 'vitest';
|
||||
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
||||
import {
|
||||
buildPlatformRankingEntries,
|
||||
buildPlatformRecommendFeedEntries,
|
||||
buildPublicCategoryGroups,
|
||||
buildPublicGalleryCardKey,
|
||||
dedupePlatformPublicGalleryEntries,
|
||||
filterPlatformWorkSearchResults,
|
||||
filterTodayPublishedEntries,
|
||||
getPlatformCategoryKindFilter,
|
||||
@@ -13,6 +15,8 @@ import {
|
||||
getPlatformRankingMetricValue,
|
||||
matchesPlatformCategoryKindFilter,
|
||||
parsePlatformEntryTimestamp,
|
||||
selectAdjacentPlatformRecommendEntry,
|
||||
selectPlatformRecommendFeedWindow,
|
||||
sortPlatformCategoryEntries,
|
||||
} from './rpgEntryPublicGalleryViewModel';
|
||||
import type {
|
||||
@@ -146,6 +150,100 @@ test('public gallery ViewModel dedupes merged public entries by latest source',
|
||||
});
|
||||
});
|
||||
|
||||
test('public gallery ViewModel builds recommend feed from general public entries', () => {
|
||||
const featuredPuzzle = buildPuzzleEntry({
|
||||
profileId: 'shared',
|
||||
worldName: '精选旧拼图',
|
||||
});
|
||||
const latestPuzzle = buildPuzzleEntry({
|
||||
profileId: 'shared',
|
||||
worldName: '最新拼图',
|
||||
});
|
||||
const edutainmentPuzzle = buildPuzzleEntry({
|
||||
profileId: 'edutainment',
|
||||
themeTags: ['寓教于乐'],
|
||||
});
|
||||
const jumpHopEntry = buildJumpHopEntry({ profileId: 'jump-hop' });
|
||||
|
||||
expect(
|
||||
buildPlatformRecommendFeedEntries(
|
||||
[featuredPuzzle, edutainmentPuzzle],
|
||||
[latestPuzzle, jumpHopEntry],
|
||||
),
|
||||
).toEqual([latestPuzzle, jumpHopEntry]);
|
||||
expect(
|
||||
dedupePlatformPublicGalleryEntries([featuredPuzzle, latestPuzzle]),
|
||||
).toEqual([latestPuzzle]);
|
||||
});
|
||||
|
||||
test('public gallery ViewModel selects recommend feed window with wraparound neighbors', () => {
|
||||
const firstEntry = buildPuzzleEntry({ profileId: 'first' });
|
||||
const secondEntry = buildJumpHopEntry({ profileId: 'second' });
|
||||
const thirdEntry = buildWoodenFishEntry({ profileId: 'third' });
|
||||
const entries = [firstEntry, secondEntry, thirdEntry];
|
||||
|
||||
expect(selectPlatformRecommendFeedWindow([], 'missing')).toEqual({
|
||||
activeEntry: null,
|
||||
activeEntryKey: null,
|
||||
activeIndex: -1,
|
||||
nextEntry: null,
|
||||
previousEntry: null,
|
||||
});
|
||||
expect(selectPlatformRecommendFeedWindow([firstEntry], null)).toEqual({
|
||||
activeEntry: firstEntry,
|
||||
activeEntryKey: buildPublicGalleryCardKey(firstEntry),
|
||||
activeIndex: 0,
|
||||
nextEntry: null,
|
||||
previousEntry: null,
|
||||
});
|
||||
expect(
|
||||
selectPlatformRecommendFeedWindow(
|
||||
entries,
|
||||
buildPublicGalleryCardKey(secondEntry),
|
||||
),
|
||||
).toEqual({
|
||||
activeEntry: secondEntry,
|
||||
activeEntryKey: buildPublicGalleryCardKey(secondEntry),
|
||||
activeIndex: 1,
|
||||
nextEntry: thirdEntry,
|
||||
previousEntry: firstEntry,
|
||||
});
|
||||
|
||||
expect(selectPlatformRecommendFeedWindow(entries, 'missing')).toEqual({
|
||||
activeEntry: firstEntry,
|
||||
activeEntryKey: buildPublicGalleryCardKey(firstEntry),
|
||||
activeIndex: 0,
|
||||
nextEntry: secondEntry,
|
||||
previousEntry: thirdEntry,
|
||||
});
|
||||
expect(selectPlatformRecommendFeedWindow(entries, null)).toEqual({
|
||||
activeEntry: firstEntry,
|
||||
activeEntryKey: buildPublicGalleryCardKey(firstEntry),
|
||||
activeIndex: 0,
|
||||
nextEntry: secondEntry,
|
||||
previousEntry: thirdEntry,
|
||||
});
|
||||
});
|
||||
|
||||
test('public gallery ViewModel selects adjacent recommend entry without self-loop', () => {
|
||||
const onlyEntry = buildPuzzleEntry({ profileId: 'only' });
|
||||
const nextEntry = buildJumpHopEntry({ profileId: 'next' });
|
||||
|
||||
expect(
|
||||
selectAdjacentPlatformRecommendEntry([onlyEntry], 1, 'missing'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
selectAdjacentPlatformRecommendEntry(
|
||||
[onlyEntry, nextEntry],
|
||||
1,
|
||||
buildPublicGalleryCardKey(onlyEntry),
|
||||
),
|
||||
).toBe(nextEntry);
|
||||
expect(
|
||||
selectAdjacentPlatformRecommendEntry([onlyEntry, nextEntry], -1, 'missing'),
|
||||
).toBe(nextEntry);
|
||||
});
|
||||
|
||||
test('public gallery ViewModel searches compact work codes and ranks name prefix first', () => {
|
||||
const nameMatch = buildPuzzleEntry({
|
||||
profileId: 'name-match',
|
||||
|
||||
@@ -33,24 +33,119 @@ export type PlatformPublicCategoryGroup = {
|
||||
entries: PlatformPublicGalleryCard[];
|
||||
};
|
||||
|
||||
export type PlatformRecommendFeedWindow = {
|
||||
activeEntry: PlatformPublicGalleryCard | null;
|
||||
activeEntryKey: string | null;
|
||||
activeIndex: number;
|
||||
nextEntry: PlatformPublicGalleryCard | null;
|
||||
previousEntry: PlatformPublicGalleryCard | null;
|
||||
};
|
||||
|
||||
export function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
|
||||
return getPlatformPublicGalleryEntryKey(entry);
|
||||
}
|
||||
|
||||
export function dedupePlatformPublicGalleryEntries(
|
||||
entries: PlatformPublicGalleryCard[],
|
||||
) {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
entries.forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}
|
||||
|
||||
export function buildPlatformRecommendFeedEntries(
|
||||
featuredEntries: PlatformPublicGalleryCard[],
|
||||
latestEntries: PlatformPublicGalleryCard[],
|
||||
) {
|
||||
return dedupePlatformPublicGalleryEntries(
|
||||
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]),
|
||||
);
|
||||
}
|
||||
|
||||
export function selectAdjacentPlatformRecommendEntry(
|
||||
entries: PlatformPublicGalleryCard[],
|
||||
direction: 1 | -1,
|
||||
baseEntryKey?: string | null,
|
||||
) {
|
||||
if (entries.length <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedBaseEntryKey = baseEntryKey?.trim() ?? '';
|
||||
const activeIndex = normalizedBaseEntryKey
|
||||
? entries.findIndex(
|
||||
(entry) => buildPublicGalleryCardKey(entry) === normalizedBaseEntryKey,
|
||||
)
|
||||
: -1;
|
||||
const baseIndex = activeIndex >= 0 ? activeIndex : 0;
|
||||
const nextIndex =
|
||||
(baseIndex + direction + entries.length) % entries.length;
|
||||
const nextEntry = entries[nextIndex] ?? null;
|
||||
if (
|
||||
nextEntry &&
|
||||
normalizedBaseEntryKey &&
|
||||
buildPublicGalleryCardKey(nextEntry) === normalizedBaseEntryKey
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return nextEntry;
|
||||
}
|
||||
|
||||
export function selectPlatformRecommendFeedWindow(
|
||||
entries: PlatformPublicGalleryCard[],
|
||||
activeEntryKey?: string | null,
|
||||
): PlatformRecommendFeedWindow {
|
||||
const normalizedActiveEntryKey = activeEntryKey?.trim() ?? '';
|
||||
const activeEntry =
|
||||
(normalizedActiveEntryKey
|
||||
? entries.find(
|
||||
(entry) =>
|
||||
buildPublicGalleryCardKey(entry) === normalizedActiveEntryKey,
|
||||
)
|
||||
: null) ??
|
||||
entries[0] ??
|
||||
null;
|
||||
const selectedActiveEntryKey = activeEntry
|
||||
? buildPublicGalleryCardKey(activeEntry)
|
||||
: null;
|
||||
const activeIndex = selectedActiveEntryKey
|
||||
? entries.findIndex(
|
||||
(entry) => buildPublicGalleryCardKey(entry) === selectedActiveEntryKey,
|
||||
)
|
||||
: -1;
|
||||
|
||||
return {
|
||||
activeEntry,
|
||||
activeEntryKey: selectedActiveEntryKey,
|
||||
activeIndex,
|
||||
nextEntry: selectAdjacentPlatformRecommendEntry(
|
||||
entries,
|
||||
1,
|
||||
selectedActiveEntryKey,
|
||||
),
|
||||
previousEntry: selectAdjacentPlatformRecommendEntry(
|
||||
entries,
|
||||
-1,
|
||||
selectedActiveEntryKey,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPublicCategoryGroups(
|
||||
featuredEntries: PlatformPublicGalleryCard[],
|
||||
latestEntries: PlatformPublicGalleryCard[],
|
||||
): PlatformPublicCategoryGroup[] {
|
||||
const publicEntryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
|
||||
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach(
|
||||
(entry) => {
|
||||
publicEntryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
},
|
||||
const publicEntries = buildPlatformRecommendFeedEntries(
|
||||
featuredEntries,
|
||||
latestEntries,
|
||||
);
|
||||
|
||||
const categoryMap = new Map<string, PlatformPublicGalleryCard[]>();
|
||||
Array.from(publicEntryMap.values()).forEach((entry) => {
|
||||
publicEntries.forEach((entry) => {
|
||||
const tags = buildPlatformWorldDisplayTags(entry, 3);
|
||||
const normalizedTags = tags.length > 0 ? tags : ['回响'];
|
||||
|
||||
@@ -76,28 +171,17 @@ export function getPlatformPublicEntries(
|
||||
featuredEntries: PlatformPublicGalleryCard[],
|
||||
latestEntries: PlatformPublicGalleryCard[],
|
||||
) {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
|
||||
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach(
|
||||
(entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
},
|
||||
);
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
return buildPlatformRecommendFeedEntries(featuredEntries, latestEntries);
|
||||
}
|
||||
|
||||
export function getAllPlatformPublicEntries(
|
||||
featuredEntries: PlatformPublicGalleryCard[],
|
||||
latestEntries: PlatformPublicGalleryCard[],
|
||||
) {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
|
||||
[...featuredEntries, ...latestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
return dedupePlatformPublicGalleryEntries([
|
||||
...featuredEntries,
|
||||
...latestEntries,
|
||||
]);
|
||||
}
|
||||
|
||||
function normalizePlatformSearchText(value: string | null | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user