refactor: 深化公开作品详情状态策略
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
## 2026-06-03 平台入口公开作品详情 Strategy 收口
|
## 2026-06-03 平台入口公开作品详情 Strategy 收口
|
||||||
|
|
||||||
- 背景:平台壳层直接判断公开作品详情入口的玩法类型、是否需要补读完整详情,以及自有作品按钮显示“编辑”还是“改造”,导致统一作品详情的纯决策散落在巨型 Implementation 内。
|
- 背景:平台壳层直接判断公开作品详情入口的玩法类型、是否需要补读完整详情,以及自有作品按钮显示“编辑”还是“改造”,导致统一作品详情的纯决策散落在巨型 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`。
|
- 验证方式:`npm run test -- src/components/platform-entry/platformPublicWorkDetailFlow.test.ts`、公开详情壳层交互回归、`npm run typecheck`、`npm run check:encoding`。
|
||||||
- 关联文档:`docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md`。
|
- 关联文档:`docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md`。
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
- `getPlatformPublicWorkDetailKind(entry)`
|
- `getPlatformPublicWorkDetailKind(entry)`
|
||||||
- `resolvePlatformPublicWorkDetailOpenStrategy(entry)`
|
- `resolvePlatformPublicWorkDetailOpenStrategy(entry)`
|
||||||
- `resolvePlatformPublicWorkActionMode(entry, viewerUserId)`
|
- `resolvePlatformPublicWorkActionMode(entry, viewerUserId)`
|
||||||
|
- `resolvePlatformPublicWorkDetailOpenDecision(entry, deps)`
|
||||||
|
- `resolveActivePlatformPublicWorkAuthorEntry(args)`
|
||||||
- `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter:根据 open strategy 调用 `openPublicWorkDetail`、`openPuzzlePublicWorkDetail`、`openJumpHopPublicWorkDetail`、`openWoodenFishPublicWorkDetail`、`openVisualNovelPublicWorkDetail` 或 `openRpgPublicWorkDetail`。
|
- `PlatformEntryFlowShellImpl.tsx` 继续作为 Adapter:根据 open strategy 调用 `openPublicWorkDetail`、`openPuzzlePublicWorkDetail`、`openJumpHopPublicWorkDetail`、`openWoodenFishPublicWorkDetail`、`openVisualNovelPublicWorkDetail` 或 `openRpgPublicWorkDetail`。
|
||||||
- 本次不抽 `startSelectedPublicWork`、`likePublicWork`、`remixPublicWork`、`editOwnedPublicWork`。这些函数牵涉运行态启动、计数写入、草稿恢复、作品架缓存和多路错误 setter;若直接搬进一个 Hook,会形成浅 Interface。
|
- 本次不抽 `startSelectedPublicWork`、`likePublicWork`、`remixPublicWork`、`editOwnedPublicWork`。这些函数牵涉运行态启动、计数写入、草稿恢复、作品架缓存和多路错误 setter;若直接搬进一个 Hook,会形成浅 Interface。
|
||||||
|
|
||||||
@@ -22,6 +24,8 @@
|
|||||||
- 大鱼吃小鱼、抓大鹅、方洞挑战、汪汪声浪、宝贝识物和其它可直接展示的公开 entry 返回 `use-entry` strategy。
|
- 大鱼吃小鱼、抓大鹅、方洞挑战、汪汪声浪、宝贝识物和其它可直接展示的公开 entry 返回 `use-entry` strategy。
|
||||||
- RPG 返回 `load-rpg-detail` strategy,由壳层 Adapter 继续调用 RPG 详情读取流程。
|
- RPG 返回 `load-rpg-detail` strategy,由壳层 Adapter 继续调用 RPG 详情读取流程。
|
||||||
- `resolvePlatformPublicWorkActionMode` 只比较 `entry.ownerUserId` 与当前 viewer user id;当前用户拥有该公开作品时返回 `edit`,否则返回 `remix`。
|
- `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
|
## Depth / Leverage / Locality
|
||||||
|
|
||||||
|
|||||||
@@ -516,7 +516,9 @@ import {
|
|||||||
type RecommendRuntimeKind,
|
type RecommendRuntimeKind,
|
||||||
} from './platformPublicGalleryFlow';
|
} from './platformPublicGalleryFlow';
|
||||||
import {
|
import {
|
||||||
|
resolveActivePlatformPublicWorkAuthorEntry,
|
||||||
resolvePlatformPublicWorkActionMode,
|
resolvePlatformPublicWorkActionMode,
|
||||||
|
resolvePlatformPublicWorkDetailOpenDecision,
|
||||||
resolvePlatformPublicWorkDetailOpenStrategy,
|
resolvePlatformPublicWorkDetailOpenStrategy,
|
||||||
} from './platformPublicWorkDetailFlow';
|
} from './platformPublicWorkDetailFlow';
|
||||||
import {
|
import {
|
||||||
@@ -10908,20 +10910,19 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
|
|
||||||
const openPublicWorkDetail = useCallback(
|
const openPublicWorkDetail = useCallback(
|
||||||
(entry: PlatformPublicGalleryCard) => {
|
(entry: PlatformPublicGalleryCard) => {
|
||||||
if (!canExposePublicWork(entry)) {
|
const decision = resolvePlatformPublicWorkDetailOpenDecision(entry);
|
||||||
setSelectedPublicWorkDetail(null);
|
if (decision.type === 'blocked') {
|
||||||
setPublicWorkDetailError(EDUTAINMENT_HIDDEN_MESSAGE);
|
setSelectedPublicWorkDetail(decision.selectedDetail);
|
||||||
setSelectionStage('platform');
|
setPublicWorkDetailError(decision.errorMessage);
|
||||||
|
setSelectionStage(decision.selectionStage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedPublicWorkDetail(entry);
|
setSelectedPublicWorkDetail(decision.selectedDetail);
|
||||||
setPublicWorkDetailError(null);
|
setPublicWorkDetailError(decision.errorMessage);
|
||||||
setSelectionStage('work-detail');
|
setSelectionStage(decision.selectionStage);
|
||||||
if (entry.publicWorkCode?.trim()) {
|
if (decision.historyPath) {
|
||||||
pushAppHistoryPath(
|
pushAppHistoryPath(decision.historyPath);
|
||||||
buildPublicWorkStagePath('work-detail', entry.publicWorkCode),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setSelectionStage],
|
[setSelectionStage],
|
||||||
@@ -11118,14 +11119,11 @@ export function PlatformEntryFlowShellImpl({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const detailEntry =
|
const detailEntry = resolveActivePlatformPublicWorkAuthorEntry({
|
||||||
selectionStage === 'work-detail'
|
selectionStage,
|
||||||
? selectedPublicWorkDetail
|
selectedPublicWorkDetail,
|
||||||
: selectionStage === 'detail' &&
|
selectedRpgDetailEntry: selectedDetailEntry,
|
||||||
selectedDetailEntry &&
|
});
|
||||||
selectedDetailEntry.visibility !== 'draft'
|
|
||||||
? mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!detailEntry) {
|
if (!detailEntry) {
|
||||||
clearSelectedPublicWorkAuthor();
|
clearSelectedPublicWorkAuthor();
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import {
|
|||||||
getPlatformPublicWorkDetailKind,
|
getPlatformPublicWorkDetailKind,
|
||||||
type PlatformPublicWorkDetailKind,
|
type PlatformPublicWorkDetailKind,
|
||||||
type PlatformPublicWorkDetailOpenStrategy,
|
type PlatformPublicWorkDetailOpenStrategy,
|
||||||
|
resolveActivePlatformPublicWorkAuthorEntry,
|
||||||
resolvePlatformPublicWorkActionMode,
|
resolvePlatformPublicWorkActionMode,
|
||||||
|
resolvePlatformPublicWorkDetailOpenDecision,
|
||||||
resolvePlatformPublicWorkDetailOpenStrategy,
|
resolvePlatformPublicWorkDetailOpenStrategy,
|
||||||
} from './platformPublicWorkDetailFlow';
|
} 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, 'user-2')).toBe('remix');
|
||||||
expect(resolvePlatformPublicWorkActionMode(entry, null)).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 type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
|
||||||
|
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
||||||
import {
|
import {
|
||||||
isBarkBattleGalleryEntry,
|
isBarkBattleGalleryEntry,
|
||||||
isBigFishGalleryEntry,
|
isBigFishGalleryEntry,
|
||||||
@@ -11,6 +12,10 @@ import {
|
|||||||
isWoodenFishGalleryEntry,
|
isWoodenFishGalleryEntry,
|
||||||
type PlatformPublicGalleryCard,
|
type PlatformPublicGalleryCard,
|
||||||
} from '../rpg-entry/rpgEntryWorldPresentation';
|
} from '../rpg-entry/rpgEntryWorldPresentation';
|
||||||
|
import {
|
||||||
|
canExposePublicWork,
|
||||||
|
EDUTAINMENT_HIDDEN_MESSAGE,
|
||||||
|
} from './platformEdutainmentVisibility';
|
||||||
|
|
||||||
export type PlatformPublicWorkDetailKind =
|
export type PlatformPublicWorkDetailKind =
|
||||||
| 'bark-battle'
|
| 'bark-battle'
|
||||||
@@ -55,6 +60,34 @@ export type PlatformPublicWorkDetailOpenStrategy =
|
|||||||
|
|
||||||
export type PlatformPublicWorkActionMode = 'edit' | 'remix';
|
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(
|
export function isRpgPublicWorkDetailEntry(
|
||||||
entry: PlatformPublicGalleryCard,
|
entry: PlatformPublicGalleryCard,
|
||||||
): entry is CustomWorldGalleryCard {
|
): entry is CustomWorldGalleryCard {
|
||||||
@@ -188,3 +221,57 @@ export function resolvePlatformPublicWorkActionMode(
|
|||||||
? 'edit'
|
? 'edit'
|
||||||
: 'remix';
|
: '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