From dd52848e9cccf3dfafa647c234fd60008a6a21a8 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 3 Jun 2026 22:38:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=B7=B1=E5=8C=96=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E4=BD=9C=E5=93=81=E8=AF=A6=E6=83=85=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 2 +- ...atformPublicWorkDetailFlow收口计划-2026-06-03.md | 4 + .../PlatformEntryFlowShellImpl.tsx | 36 ++++---- .../platformPublicWorkDetailFlow.test.ts | 90 +++++++++++++++++++ .../platformPublicWorkDetailFlow.ts | 87 ++++++++++++++++++ 5 files changed, 199 insertions(+), 20 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index d9f8192f..decd8733 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -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`。 diff --git a/docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md b/docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md index effc3c3a..1d5bb942 100644 --- a/docs/technical/【前端架构】PlatformPublicWorkDetailFlow收口计划-2026-06-03.md +++ b/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 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 98366607..3efa1b72 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -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(); diff --git a/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts b/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts index 994fb7b0..2b8df398 100644 --- a/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts +++ b/src/components/platform-entry/platformPublicWorkDetailFlow.test.ts @@ -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(); +}); diff --git a/src/components/platform-entry/platformPublicWorkDetailFlow.ts b/src/components/platform-entry/platformPublicWorkDetailFlow.ts index 942f220e..a99c3bda 100644 --- a/src/components/platform-entry/platformPublicWorkDetailFlow.ts +++ b/src/components/platform-entry/platformPublicWorkDetailFlow.ts @@ -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; +}