refactor: 深化公开作品详情状态策略

This commit is contained in:
2026-06-03 22:38:26 +08:00
parent 00820e6571
commit dd52848e9c
5 changed files with 199 additions and 20 deletions

View File

@@ -19,7 +19,7 @@
## 2026-06-03 平台入口公开作品详情 Strategy 收口
- 背景:平台壳层直接判断公开作品详情入口的玩法类型、是否需要补读完整详情,以及自有作品按钮显示“编辑”还是“改造”,导致统一作品详情的纯决策散落在巨型 Implementation 内。
- 决策:新增 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`,以 `getPlatformPublicWorkDetailKind``resolvePlatformPublicWorkDetailOpenStrategy``resolvePlatformPublicWorkActionMode` 收口公开作品详情 Strategy。`PlatformEntryFlowShellImpl.tsx` 只按 Strategy 调用现有详情读取 / 直接展示 Adapter启动、点赞、remix 和编辑副作用暂不抽走。
- 决策:新增 `src/components/platform-entry/platformPublicWorkDetailFlow.ts`,以 `getPlatformPublicWorkDetailKind``resolvePlatformPublicWorkDetailOpenStrategy``resolvePlatformPublicWorkActionMode``resolvePlatformPublicWorkDetailOpenDecision``resolveActivePlatformPublicWorkAuthorEntry` 收口公开作品详情 Strategy。`PlatformEntryFlowShellImpl.tsx` 只按 Strategy 调用现有详情读取 / 直接展示 Adapter,并保留作者请求竞态控制启动、点赞、remix 和编辑副作用暂不抽走。
- 影响范围:统一作品详情入口、公开详情打开策略、自有公开作品编辑 / 改造动作模式,以及后续新增玩法公开详情接入。
- 验证方式:`npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`、公开详情壳层交互回归、`npm run typecheck``npm run check:encoding`
- 关联文档:`docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md`

View File

@@ -11,6 +11,8 @@
- `getPlatformPublicWorkDetailKind(entry)`
- `resolvePlatformPublicWorkDetailOpenStrategy(entry)`
- `resolvePlatformPublicWorkActionMode(entry, viewerUserId)`
- `resolvePlatformPublicWorkDetailOpenDecision(entry, deps)`
- `resolveActivePlatformPublicWorkAuthorEntry(args)`
- `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter根据 open strategy 调用 `openPublicWorkDetail``openPuzzlePublicWorkDetail``openJumpHopPublicWorkDetail``openWoodenFishPublicWorkDetail``openVisualNovelPublicWorkDetail``openRpgPublicWorkDetail`
- 本次不抽 `startSelectedPublicWork``likePublicWork``remixPublicWork``editOwnedPublicWork`。这些函数牵涉运行态启动、计数写入、草稿恢复、作品架缓存和多路错误 setter若直接搬进一个 Hook会形成浅 Interface。
@@ -22,6 +24,8 @@
- 大鱼吃小鱼、抓大鹅、方洞挑战、汪汪声浪、宝贝识物和其它可直接展示的公开 entry 返回 `use-entry` strategy。
- RPG 返回 `load-rpg-detail` strategy由壳层 Adapter 继续调用 RPG 详情读取流程。
- `resolvePlatformPublicWorkActionMode` 只比较 `entry.ownerUserId` 与当前 viewer user id当前用户拥有该公开作品时返回 `edit`,否则返回 `remix`
- `resolvePlatformPublicWorkDetailOpenDecision` 只表达直接展示公开详情的打开 / 阻断结果、错误文案、目标 stage 与可写入历史的路径;真正执行 setter、push history 的副作用仍由壳层 Adapter 执行。
- `resolveActivePlatformPublicWorkAuthorEntry` 只在 `work-detail` 阶段选择统一公开详情 entry在 RPG `detail` 阶段只选择非 draft 的 RPG 详情 entry作者请求、竞态 request key 和缓存仍留壳层。
## Depth / Leverage / Locality

View File

@@ -516,7 +516,9 @@ import {
type RecommendRuntimeKind,
} from './platformPublicGalleryFlow';
import {
resolveActivePlatformPublicWorkAuthorEntry,
resolvePlatformPublicWorkActionMode,
resolvePlatformPublicWorkDetailOpenDecision,
resolvePlatformPublicWorkDetailOpenStrategy,
} from './platformPublicWorkDetailFlow';
import {
@@ -10908,20 +10910,19 @@ export function PlatformEntryFlowShellImpl({
const openPublicWorkDetail = useCallback(
(entry: PlatformPublicGalleryCard) => {
if (!canExposePublicWork(entry)) {
setSelectedPublicWorkDetail(null);
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
setSelectionStage('platform');
const decision = resolvePlatformPublicWorkDetailOpenDecision(entry);
if (decision.type === 'blocked') {
setSelectedPublicWorkDetail(decision.selectedDetail);
setPublicWorkDetailError(decision.errorMessage);
setSelectionStage(decision.selectionStage);
return;
}
setSelectedPublicWorkDetail(entry);
setPublicWorkDetailError(null);
setSelectionStage('work-detail');
if (entry.publicWorkCode?.trim()) {
pushAppHistoryPath(
buildPublicWorkStagePath('work-detail', entry.publicWorkCode),
);
setSelectedPublicWorkDetail(decision.selectedDetail);
setPublicWorkDetailError(decision.errorMessage);
setSelectionStage(decision.selectionStage);
if (decision.historyPath) {
pushAppHistoryPath(decision.historyPath);
}
},
[setSelectionStage],
@@ -11118,14 +11119,11 @@ export function PlatformEntryFlowShellImpl({
);
useEffect(() => {
const detailEntry =
selectionStage === 'work-detail'
? selectedPublicWorkDetail
: selectionStage === 'detail' &&
selectedDetailEntry &&
selectedDetailEntry.visibility !== 'draft'
? mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)
: null;
const detailEntry = resolveActivePlatformPublicWorkAuthorEntry({
selectionStage,
selectedPublicWorkDetail,
selectedRpgDetailEntry: selectedDetailEntry,
});
if (!detailEntry) {
clearSelectedPublicWorkAuthor();

View File

@@ -10,7 +10,9 @@ import {
getPlatformPublicWorkDetailKind,
type PlatformPublicWorkDetailKind,
type PlatformPublicWorkDetailOpenStrategy,
resolveActivePlatformPublicWorkAuthorEntry,
resolvePlatformPublicWorkActionMode,
resolvePlatformPublicWorkDetailOpenDecision,
resolvePlatformPublicWorkDetailOpenStrategy,
} from './platformPublicWorkDetailFlow';
@@ -227,3 +229,91 @@ test('platform public work detail flow resolves edit mode only for owned works',
expect(resolvePlatformPublicWorkActionMode(entry, 'user-2')).toBe('remix');
expect(resolvePlatformPublicWorkActionMode(entry, null)).toBe('remix');
});
test('platform public work detail flow resolves direct open decision', () => {
const entry = buildTypedEntry('match3d', {
publicWorkCode: ' M3D-001 ',
});
const buildWorkDetailPath = (publicWorkCode: string) =>
`/works/detail?work=${publicWorkCode.trim()}`;
expect(
resolvePlatformPublicWorkDetailOpenDecision(entry, {
buildWorkDetailPath,
}),
).toEqual({
type: 'open',
selectedDetail: entry,
errorMessage: null,
selectionStage: 'work-detail',
historyPath: '/works/detail?work=M3D-001',
});
expect(
resolvePlatformPublicWorkDetailOpenDecision(
buildTypedEntry('match3d', { publicWorkCode: ' ' }),
{
buildWorkDetailPath,
},
),
).toEqual({
type: 'open',
selectedDetail: buildTypedEntry('match3d', { publicWorkCode: ' ' }),
errorMessage: null,
selectionStage: 'work-detail',
historyPath: null,
});
expect(
resolvePlatformPublicWorkDetailOpenDecision(entry, {
canExposeEntry: () => false,
hiddenMessage: '隐藏',
buildWorkDetailPath,
}),
).toEqual({
type: 'blocked',
selectedDetail: null,
errorMessage: '隐藏',
selectionStage: 'platform',
historyPath: null,
});
});
test('platform public work detail flow selects author lookup entry by stage', () => {
const selectedPublicWorkDetail = buildTypedEntry('puzzle');
const publishedRpgEntry = buildRpgEntry({
visibility: 'published',
profileId: 'published-rpg-profile',
});
const draftRpgEntry = buildRpgEntry({
visibility: 'draft',
profileId: 'draft-rpg-profile',
});
expect(
resolveActivePlatformPublicWorkAuthorEntry({
selectionStage: 'work-detail',
selectedPublicWorkDetail,
selectedRpgDetailEntry: publishedRpgEntry,
}),
).toBe(selectedPublicWorkDetail);
expect(
resolveActivePlatformPublicWorkAuthorEntry({
selectionStage: 'detail',
selectedPublicWorkDetail: null,
selectedRpgDetailEntry: publishedRpgEntry,
}),
).toBe(publishedRpgEntry);
expect(
resolveActivePlatformPublicWorkAuthorEntry({
selectionStage: 'detail',
selectedPublicWorkDetail: null,
selectedRpgDetailEntry: draftRpgEntry,
}),
).toBeNull();
expect(
resolveActivePlatformPublicWorkAuthorEntry({
selectionStage: 'platform',
selectedPublicWorkDetail,
selectedRpgDetailEntry: publishedRpgEntry,
}),
).toBeNull();
});

View File

@@ -1,4 +1,5 @@
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
import {
isBarkBattleGalleryEntry,
isBigFishGalleryEntry,
@@ -11,6 +12,10 @@ import {
isWoodenFishGalleryEntry,
type PlatformPublicGalleryCard,
} from '../rpg-entry/rpgEntryWorldPresentation';
import {
canExposePublicWork,
EDUTAINMENT_HIDDEN_MESSAGE,
} from './platformEdutainmentVisibility';
export type PlatformPublicWorkDetailKind =
| 'bark-battle'
@@ -55,6 +60,34 @@ export type PlatformPublicWorkDetailOpenStrategy =
export type PlatformPublicWorkActionMode = 'edit' | 'remix';
export type PlatformPublicWorkDetailOpenDecision =
| {
type: 'blocked';
selectedDetail: null;
errorMessage: string;
selectionStage: 'platform';
historyPath: null;
}
| {
type: 'open';
selectedDetail: PlatformPublicGalleryCard;
errorMessage: null;
selectionStage: 'work-detail';
historyPath: string | null;
};
export type PlatformPublicWorkDetailOpenDecisionDeps = {
canExposeEntry?: (entry: PlatformPublicGalleryCard) => boolean;
hiddenMessage?: string;
buildWorkDetailPath?: (publicWorkCode: string) => string;
};
export type ActivePlatformPublicWorkAuthorEntryInput = {
selectionStage: string;
selectedPublicWorkDetail: PlatformPublicGalleryCard | null;
selectedRpgDetailEntry: CustomWorldGalleryCard | null;
};
export function isRpgPublicWorkDetailEntry(
entry: PlatformPublicGalleryCard,
): entry is CustomWorldGalleryCard {
@@ -188,3 +221,57 @@ export function resolvePlatformPublicWorkActionMode(
? 'edit'
: 'remix';
}
export function resolvePlatformPublicWorkDetailOpenDecision(
entry: PlatformPublicGalleryCard,
deps: PlatformPublicWorkDetailOpenDecisionDeps = {},
): PlatformPublicWorkDetailOpenDecision {
const canExposeEntry = deps.canExposeEntry ?? canExposePublicWork;
const hiddenMessage = deps.hiddenMessage ?? EDUTAINMENT_HIDDEN_MESSAGE;
const buildWorkDetailPath =
deps.buildWorkDetailPath ??
((publicWorkCode: string) =>
buildPublicWorkStagePath('work-detail', publicWorkCode));
if (!canExposeEntry(entry)) {
return {
type: 'blocked',
selectedDetail: null,
errorMessage: hiddenMessage,
selectionStage: 'platform',
historyPath: null,
};
}
const publicWorkCode = entry.publicWorkCode?.trim()
? entry.publicWorkCode
: null;
return {
type: 'open',
selectedDetail: entry,
errorMessage: null,
selectionStage: 'work-detail',
historyPath: publicWorkCode ? buildWorkDetailPath(publicWorkCode) : null,
};
}
export function resolveActivePlatformPublicWorkAuthorEntry({
selectionStage,
selectedPublicWorkDetail,
selectedRpgDetailEntry,
}: ActivePlatformPublicWorkAuthorEntryInput): PlatformPublicGalleryCard | null {
if (selectionStage === 'work-detail') {
return selectedPublicWorkDetail;
}
if (
selectionStage === 'detail' &&
selectedRpgDetailEntry &&
selectedRpgDetailEntry.visibility !== 'draft'
) {
return selectedRpgDetailEntry;
}
return null;
}