refactor: 深化公开作品详情状态策略
This commit is contained in:
@@ -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`。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user