diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 78854d31..43701f76 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-06-04 Creation Work Delete Flow 收口 + +- 背景:平台入口作品架删除入口在 RPG、拼图、抓大鹅、方洞挑战、大鱼吃小鱼、视觉小说和宝贝识物 handler 内重复计算确认标题、删除说明、草稿 notice key 与拼图派生稳定 ID,导致删除确认规则散在巨型壳层。 +- 决策:新增 `src/components/platform-entry/platformCreationWorkDeleteFlow.ts`,以 `resolvePlatformCreationWorkDeleteConfirmationModel(input)` 收口作品架删除确认纯模型;输出 `id/title/detail/noticeKeys`。`PlatformEntryFlowShellImpl.tsx` 仍作为副作用 Adapter,保留删除 API、刷新作品架 / 公开广场、错误状态、`markDraftNoticeSeen` 和页面跳转。 +- 影响范围:创作中心作品架删除确认弹窗、删除后生成 notice 清理、拼图稳定 result ID 清理、宝贝识物已发布删除说明,以及后续新增玩法作品架删除接入。 +- 验证方式:`npm run test -- src/components/platform-entry/platformCreationWorkDeleteFlow.test.ts`、`npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts`、针对新 Module 与平台壳执行 ESLint、`npm run typecheck`、`npm run check:encoding`。 +- 关联文档:`docs/technical/【前端架构】CreationWorkDeleteFlow收口计划-2026-06-04.md`。 + ## 2026-06-03 平台入口公开作品详情 Strategy 收口 - 背景:平台壳层直接判断公开作品详情入口的玩法类型、是否需要补读完整详情,以及自有作品按钮显示“编辑”还是“改造”,导致统一作品详情的纯决策散落在巨型 Implementation 内。 diff --git a/docs/README.md b/docs/README.md index 466e891a..a436abe6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -45,6 +45,8 @@ AI 文字游戏模板接入以 [AI_NATIVE_TEXT_GAME_TEMPLATE_MOKU_REFERENCE_PRD_ 创作中心作品架打开动作由 `CreationWorkShelfItem.actions.open` 统一承载,生产 Hub 只接收 `CreationWorkShelfItem[]` 与 UI 状态,不再接收各玩法 raw items 和回调列阵,规则见 [【前端架构】WorkShelfModule收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91WorkShelfModule%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 +作品架删除确认的标题、删除说明、草稿 notice key 和拼图派生稳定 ID 收口到 `src/components/platform-entry/platformCreationWorkDeleteFlow.ts`,平台壳只保留删除 API、刷新、错误和页面跳转副作用,规则见 [【前端架构】CreationWorkDeleteFlow收口计划-2026-06-04.md](./technical/【前端架构】CreationWorkDeleteFlow收口计划-2026-06-04.md)。 + 平台入口创作生成通知、pending 作品架占位、失败覆盖、拼图稳定 ID 和草稿 Tab 未读点收口到 `src/components/platform-entry/platformDraftGenerationShelfModel.ts`,规则见 [【前端架构】DraftGenerationShelfModel收口计划-2026-06-03.md](./technical/%E3%80%90%E5%89%8D%E7%AB%AF%E6%9E%B6%E6%9E%84%E3%80%91DraftGenerationShelfModel%E6%94%B6%E5%8F%A3%E8%AE%A1%E5%88%92-2026-06-03.md)。 平台入口创作恢复 URL 私有 query、拼图 runtime query 与拼图稳定身份互推收口到 `src/components/platform-entry/platformCreationUrlStateModel.ts` 和 `src/components/platform-entry/platformPuzzleIdentityModel.ts`,规则见 [【前端架构】CreationUrlStateModel收口计划-2026-06-03.md](./technical/【前端架构】CreationUrlStateModel收口计划-2026-06-03.md)。 diff --git a/docs/technical/【前端架构】CreationWorkDeleteFlow收口计划-2026-06-04.md b/docs/technical/【前端架构】CreationWorkDeleteFlow收口计划-2026-06-04.md new file mode 100644 index 00000000..ea4cd66d --- /dev/null +++ b/docs/technical/【前端架构】CreationWorkDeleteFlow收口计划-2026-06-04.md @@ -0,0 +1,33 @@ +# 【前端架构】Creation Work Delete Flow 收口计划 + +## 背景 + +平台入口作品架的删除入口覆盖 RPG、拼图、抓大鹅、方洞挑战、大鱼吃小鱼、视觉小说和宝贝识物。此前 `PlatformEntryFlowShellImpl.tsx` 在每个删除 handler 内重复计算确认框标题、删除说明、草稿 notice key 和拼图派生稳定 ID。壳层既要理解每种玩法的作品身份,又要承接异步删除、刷新列表、错误状态和页面跳转,导致删除确认规则缺少稳定测试面。 + +该 **Interface** 过浅:页面只想展示“删除哪个作品、会从哪里移除、删除成功后清哪些生成 notice”,却必须知道 `workId` / `profileId` / `sourceSessionId` / `draftId`、`status` / `publicationStatus` / `publishStatus` 和宝贝识物特殊公开去向。 + +## 决策 + +新增 `src/components/platform-entry/platformCreationWorkDeleteFlow.ts` 作为 Creation Work Delete Flow **Module**。其唯一公开 **Interface** 是 `resolvePlatformCreationWorkDeleteConfirmationModel(input)`,输入为带 `kind` 的 union,输出: + +- `id`:确认框和删除 busy 使用的稳定作品 ID。 +- `title`:确认框标题,含拼图、视觉小说和宝贝识物标题兜底。 +- `detail`:草稿 / 已发布删除说明,宝贝识物已发布使用“寓教于乐板块”文案。 +- `noticeKeys`:删除成功后应标记已读的草稿生成 notice keys,拼图包含 `buildPuzzleResultWorkId` / `buildPuzzleResultProfileId` 派生 key。 + +`PlatformEntryFlowShellImpl.tsx` 仍作为副作用 **Adapter**:负责鉴权保护、确认框 state、调用各玩法删除 API、清错误、刷新作品架 / 公开广场、`markDraftNoticeSeen` 和必要的页面跳转。`run` 不进入纯 **Module**,避免把网络副作用和 React state 写入藏入模型层。 + +## 约定 + +- 新玩法接入作品架删除时,先补齐后端删除链路、作品架 action 和本 **Module** 的确认模型,再开放删除按钮。 +- Jump Hop、Wooden Fish 和 Bark Battle 当前仅有作品架 action 预留,平台壳不传删除 handler;不得因本 Module 存在而默认开放删除。 +- 删除确认文案不得散回平台壳;若公开去向不是公开广场,应在本 **Module** 明确分支。 +- 草稿 notice key 的身份扩展必须复用 `collectDraftNoticeKeys`,保持 trim、去空和去重语义一致。 + +## 验证 + +- `npm run test -- src/components/platform-entry/platformCreationWorkDeleteFlow.test.ts` +- `npm run test -- src/components/platform-entry/platformDraftGenerationShelfModel.test.ts` +- `npx eslint src/components/platform-entry/platformCreationWorkDeleteFlow.ts src/components/platform-entry/platformCreationWorkDeleteFlow.test.ts src/components/platform-entry/PlatformEntryFlowShellImpl.tsx --quiet` +- `npm run typecheck` +- `npm run check:encoding` diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 479c91c2..323d3d2e 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -53,6 +53,7 @@ 9. 私有 generated 图片必须通过 `ResolvedAssetImage` / `/api/assets/read-url` 换签读取。 10. 敲木鱼作品架读取当前用户作品列表时走 `GET /api/creation/wooden-fish/works`;发布成功后平台壳必须同时刷新作品架与公开广场,避免作品刚发布时仍停留在旧列表。 11. 移动端草稿页整体禁止长按选择文字,避免误触系统选区;输入框、文本域和可编辑区域仍必须保留文本选择能力。 +12. 作品架删除确认的纯规则统一由 `platformCreationWorkDeleteFlow.ts` 解析,输出确认框 `id/title/detail` 与删除成功后清理的草稿 notice keys;平台壳只接回该模型执行删除 API、刷新列表、清错误和跳转。Jump Hop、Wooden Fish、Bark Battle 虽在作品架 action 层有预留删除入口,但未补齐删除 API 前不得传入删除 handler 或开放按钮。 发现页 / 推荐页公开作品卡的作者行只显示可读公开昵称;不得把手机号掩码、`SY-*` 陶泥号或作品号拼接进卡片作者名。陶泥号搜索、作品号复制和完整作品身份只在搜索、详情页或明确的复制入口展示,避免卡片列表暴露账号标识。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index c43ea772..c9b26af8 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -412,6 +412,7 @@ import { hasPuzzleRuntimeUrlStateValue, normalizeCreationUrlValue, } from './platformCreationUrlStateModel'; +import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow'; import { buildPlatformErrorDialogDismissKey, buildPlatformTaskCompletionDialogDismissKey, @@ -10212,12 +10213,16 @@ export function PlatformEntryFlowShellImpl({ return; } + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'rpg-library', + entry, + }); requestDeleteCreationWork({ - id: entry.profileId, - title: entry.worldName, - detail: '删除后会从你的作品列表和公开广场中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(entry.profileId); + setDeletingCreationWorkId(deleteModel.id); platformBootstrap.setPlatformError(null); void deleteRpgEntryWorldProfile(entry.profileId) @@ -10245,21 +10250,17 @@ export function PlatformEntryFlowShellImpl({ if (deletingCreationWorkId) { return; } - const noticeKeys = collectDraftNoticeKeys('rpg', [ - work.workId, - work.sessionId, - work.profileId, - ]); + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'rpg', + work, + }); requestDeleteCreationWork({ - id: work.workId, - title: work.title, - detail: - work.status === 'published' - ? '删除后会从你的作品列表和公开广场中移除。' - : '删除后会从你的作品列表中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(work.workId); + setDeletingCreationWorkId(deleteModel.id); platformBootstrap.setPlatformError(null); const deleteTask = @@ -10282,7 +10283,7 @@ export function PlatformEntryFlowShellImpl({ void deleteTask .then(async () => { - markDraftNoticeSeen(noticeKeys); + markDraftNoticeSeen(deleteModel.noticeKeys); await platformBootstrap.refreshPublishedGallery().catch(() => []); }) .catch((error) => { @@ -10309,25 +10310,22 @@ export function PlatformEntryFlowShellImpl({ if (deletingCreationWorkId) { return; } - const noticeKeys = collectDraftNoticeKeys('big-fish', [ - work.workId, - work.sourceSessionId, - ]); + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'big-fish', + work, + }); requestDeleteCreationWork({ - id: work.workId, - title: work.title, - detail: - work.status === 'published' - ? '删除后会从你的作品列表和公开广场中移除。' - : '删除后会从你的作品列表中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(work.workId); + setDeletingCreationWorkId(deleteModel.id); setBigFishError(null); void deleteBigFishWork(work.sourceSessionId) .then(async (response) => { - markDraftNoticeSeen(noticeKeys); + markDraftNoticeSeen(deleteModel.noticeKeys); setBigFishWorks(response.items); await refreshBigFishGallery().catch(() => []); }) @@ -10357,31 +10355,22 @@ export function PlatformEntryFlowShellImpl({ if (deletingCreationWorkId) { return; } - const noticeKeys = collectDraftNoticeKeys('puzzle', [ - work.workId, - work.profileId, - work.sourceSessionId, - buildPuzzleResultWorkId(work.sourceSessionId), - buildPuzzleResultProfileId(work.sourceSessionId), - ]); - - const displayName = - work.workTitle?.trim() || work.levelName.trim() || '未命名拼图'; + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'puzzle', + work, + }); requestDeleteCreationWork({ - id: work.workId, - title: displayName, - detail: - work.publicationStatus === 'published' - ? '删除后会从你的作品列表和公开广场中移除。' - : '删除后会从你的作品列表中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(work.workId); + setDeletingCreationWorkId(deleteModel.id); setPuzzleFormDraftPayload(null); setPuzzleError(null); void deletePuzzleWork(work.profileId) .then((response) => { - markDraftNoticeSeen(noticeKeys); + markDraftNoticeSeen(deleteModel.noticeKeys); setPuzzleWorks(response.items); void refreshPuzzleGallery(); }) @@ -10411,27 +10400,23 @@ export function PlatformEntryFlowShellImpl({ if (deletingCreationWorkId) { return; } - const noticeKeys = collectDraftNoticeKeys('match3d', [ - work.workId, - work.profileId, - work.sourceSessionId, - ]); + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'match3d', + work, + }); requestDeleteCreationWork({ - id: work.workId, - title: work.gameName, - detail: - work.publicationStatus === 'published' - ? '删除后会从你的作品列表和公开广场中移除。' - : '删除后会从你的作品列表中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(work.workId); + setDeletingCreationWorkId(deleteModel.id); setMatch3DFormDraftPayload(null); setMatch3DError(null); void deleteMatch3DWork(work.profileId) .then((response) => { - markDraftNoticeSeen(noticeKeys); + markDraftNoticeSeen(deleteModel.noticeKeys); setMatch3DWorks(mapMatch3DWorksForRuntimeUi(response.items)); void refreshMatch3DGallery(); }) @@ -10462,26 +10447,22 @@ export function PlatformEntryFlowShellImpl({ if (deletingCreationWorkId) { return; } - const noticeKeys = collectDraftNoticeKeys('square-hole', [ - work.workId, - work.profileId, - work.sourceSessionId, - ]); + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'square-hole', + work, + }); requestDeleteCreationWork({ - id: work.workId, - title: work.gameName, - detail: - work.publicationStatus === 'published' - ? '删除后会从你的作品列表和公开广场中移除。' - : '删除后会从你的作品列表中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(work.workId); + setDeletingCreationWorkId(deleteModel.id); setSquareHoleError(null); void deleteSquareHoleWork(work.profileId) .then((response) => { - markDraftNoticeSeen(noticeKeys); + markDraftNoticeSeen(deleteModel.noticeKeys); setSquareHoleWorks(response.items); void refreshSquareHoleGallery(); }) @@ -10511,24 +10492,22 @@ export function PlatformEntryFlowShellImpl({ if (deletingCreationWorkId) { return; } - const noticeKeys = collectDraftNoticeKeys('visual-novel', [ - work.profileId, - ]); + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'visual-novel', + work, + }); requestDeleteCreationWork({ - id: work.profileId, - title: work.title || '未命名视觉小说', - detail: - work.publishStatus === 'published' - ? '删除后会从你的作品列表和公开广场中移除。' - : '删除后会从你的作品列表中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(work.profileId); + setDeletingCreationWorkId(deleteModel.id); setVisualNovelError(null); void deleteVisualNovelWork(work.profileId) .then(async (response) => { - markDraftNoticeSeen(noticeKeys); + markDraftNoticeSeen(deleteModel.noticeKeys); setVisualNovelWorks(response.works); await refreshVisualNovelGallery(); }) @@ -10558,26 +10537,22 @@ export function PlatformEntryFlowShellImpl({ if (deletingCreationWorkId) { return; } - const noticeKeys = collectDraftNoticeKeys('baby-object-match', [ - work.profileId, - work.draftId, - ]); - const displayName = work.workTitle.trim() || work.templateName; + const deleteModel = resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'baby-object-match', + work, + }); requestDeleteCreationWork({ - id: work.profileId, - title: displayName, - detail: - work.publicationStatus === 'published' - ? '删除后会从你的作品列表和寓教于乐板块中移除。' - : '删除后会从你的作品列表中移除。', + id: deleteModel.id, + title: deleteModel.title, + detail: deleteModel.detail, run: () => { - setDeletingCreationWorkId(work.profileId); + setDeletingCreationWorkId(deleteModel.id); setBabyObjectMatchError(null); void deleteLocalBabyObjectMatchDraft(work.profileId) .then((nextDrafts) => { - markDraftNoticeSeen(noticeKeys); + markDraftNoticeSeen(deleteModel.noticeKeys); setBabyObjectMatchDrafts(nextDrafts); setBabyObjectMatchDraft((current) => current?.profileId === work.profileId ? null : current, diff --git a/src/components/platform-entry/platformCreationWorkDeleteFlow.test.ts b/src/components/platform-entry/platformCreationWorkDeleteFlow.test.ts new file mode 100644 index 00000000..e8cf726e --- /dev/null +++ b/src/components/platform-entry/platformCreationWorkDeleteFlow.test.ts @@ -0,0 +1,334 @@ +import { describe, expect, test } from 'vitest'; + +import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; +import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary'; +import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; +import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; +import { resolvePlatformCreationWorkDeleteConfirmationModel } from './platformCreationWorkDeleteFlow'; + +describe('platformCreationWorkDeleteFlow', () => { + test('resolves RPG library delete confirmation without draft notice keys', () => { + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'rpg-library', + entry: { + profileId: 'rpg-profile', + worldName: '潮雾列岛', + }, + }), + ).toEqual({ + id: 'rpg-profile', + title: '潮雾列岛', + detail: '删除后会从你的作品列表和公开广场中移除。', + noticeKeys: [], + }); + }); + + test('resolves RPG work delete detail and notice keys by work status', () => { + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'rpg', + work: buildRpgWork(), + }), + ).toEqual({ + id: 'rpg-work', + title: 'RPG 草稿', + detail: '删除后会从你的作品列表中移除。', + noticeKeys: ['rpg:rpg-work', 'rpg:rpg-session', 'rpg:rpg-profile'], + }); + + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'rpg', + work: buildRpgWork({ status: 'published' }), + }).detail, + ).toBe('删除后会从你的作品列表和公开广场中移除。'); + }); + + test('resolves mini game delete models with shared public and private detail copy', () => { + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'big-fish', + work: buildBigFishWork({ status: 'published' }), + }), + ).toMatchObject({ + id: 'big-fish-work', + title: '大鱼作品', + detail: '删除后会从你的作品列表和公开广场中移除。', + noticeKeys: ['big-fish:big-fish-work', 'big-fish:big-fish-session'], + }); + + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'match3d', + work: buildMatch3DWork(), + }).detail, + ).toBe('删除后会从你的作品列表中移除。'); + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'square-hole', + work: buildSquareHoleWork({ publicationStatus: 'published' }), + }).noticeKeys, + ).toEqual([ + 'square-hole:square-hole-work', + 'square-hole:square-hole-profile', + 'square-hole:square-hole-session', + ]); + }); + + test('resolves puzzle title fallback and stable result notice keys', () => { + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'puzzle', + work: buildPuzzleWork({ + workTitle: ' ', + levelName: ' 雾港第一关 ', + sourceSessionId: 'puzzle-session-ocean', + }), + }), + ).toEqual({ + id: 'puzzle-work', + title: '雾港第一关', + detail: '删除后会从你的作品列表中移除。', + noticeKeys: [ + 'puzzle:puzzle-work', + 'puzzle:puzzle-profile', + 'puzzle:puzzle-session-ocean', + 'puzzle:puzzle-work-ocean', + 'puzzle:puzzle-profile-ocean', + ], + }); + + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'puzzle', + work: buildPuzzleWork({ workTitle: '', levelName: ' ' }), + }).title, + ).toBe('未命名拼图'); + }); + + test('resolves visual novel and baby object match special delete copy', () => { + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'visual-novel', + work: buildVisualNovelWork({ title: '', publishStatus: 'published' }), + }), + ).toEqual({ + id: 'visual-novel-profile', + title: '未命名视觉小说', + detail: '删除后会从你的作品列表和公开广场中移除。', + noticeKeys: ['visual-novel:visual-novel-profile'], + }); + + expect( + resolvePlatformCreationWorkDeleteConfirmationModel({ + kind: 'baby-object-match', + work: buildBabyObjectMatchDraft({ + workTitle: ' ', + publicationStatus: 'published', + }), + }), + ).toEqual({ + id: 'baby-profile', + title: '宝贝识物', + detail: '删除后会从你的作品列表和寓教于乐板块中移除。', + noticeKeys: [ + 'baby-object-match:baby-profile', + 'baby-object-match:baby-draft', + ], + }); + }); +}); + +function buildRpgWork( + overrides: Partial = {}, +): CustomWorldWorkSummary { + return { + workId: 'rpg-work', + sourceType: 'agent_session', + status: 'draft', + title: 'RPG 草稿', + subtitle: '待完善', + summary: 'RPG 摘要。', + coverImageSrc: null, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: null, + stage: 'draft', + stageLabel: '草稿', + playableNpcCount: 1, + landmarkCount: 1, + sessionId: 'rpg-session', + profileId: 'rpg-profile', + canResume: true, + canEnterWorld: false, + ...overrides, + }; +} + +function buildBigFishWork( + overrides: Partial = {}, +): BigFishWorkSummary { + return { + workId: 'big-fish-work', + sourceSessionId: 'big-fish-session', + ownerUserId: 'user-1', + authorDisplayName: '玩家', + title: '大鱼作品', + subtitle: '大鱼吃小鱼', + summary: '大鱼摘要。', + coverImageSrc: null, + status: 'draft', + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: null, + publishReady: false, + levelCount: 1, + levelMainImageReadyCount: 0, + levelMotionReadyCount: 0, + backgroundReady: true, + ...overrides, + }; +} + +function buildPuzzleWork( + overrides: Partial = {}, +): PuzzleWorkSummary { + return { + workId: 'puzzle-work', + profileId: 'puzzle-profile', + ownerUserId: 'user-1', + sourceSessionId: 'puzzle-session', + authorDisplayName: '玩家', + workTitle: '拼图作品', + workDescription: '拼图摘要。', + levelName: '拼图第一关', + summary: '拼图摘要。', + themeTags: [], + coverImageSrc: null, + coverAssetId: null, + publicationStatus: 'draft', + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: null, + playCount: 0, + remixCount: 0, + likeCount: 0, + publishReady: false, + levels: [], + ...overrides, + }; +} + +function buildMatch3DWork( + overrides: Partial = {}, +): Match3DWorkSummary { + return { + workId: 'match3d-work', + profileId: 'match3d-profile', + ownerUserId: 'user-1', + sourceSessionId: 'match3d-session', + gameName: '抓大鹅作品', + themeText: '糖果厨房', + summary: '抓大鹅摘要。', + tags: [], + coverImageSrc: null, + referenceImageSrc: null, + clearCount: 12, + difficulty: 4, + publicationStatus: 'draft', + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: null, + publishReady: false, + generatedItemAssets: [], + ...overrides, + }; +} + +function buildSquareHoleWork( + overrides: Partial = {}, +): SquareHoleWorkSummary { + return { + workId: 'square-hole-work', + profileId: 'square-hole-profile', + ownerUserId: 'user-1', + sourceSessionId: 'square-hole-session', + gameName: '方洞作品', + themeText: '图形', + twistRule: '反直觉', + summary: '方洞摘要。', + tags: [], + coverImageSrc: null, + backgroundPrompt: '背景', + backgroundImageSrc: null, + shapeOptions: [], + holeOptions: [], + shapeCount: 8, + difficulty: 4, + publicationStatus: 'draft', + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: null, + publishReady: false, + ...overrides, + }; +} + +function buildVisualNovelWork( + overrides: Partial = {}, +): VisualNovelWorkSummary { + return { + runtimeKind: 'visual-novel', + profileId: 'visual-novel-profile', + ownerUserId: 'user-1', + title: '视觉小说作品', + description: '视觉小说摘要。', + coverImageSrc: null, + tags: [], + publishStatus: 'draft', + publishReady: false, + playCount: 0, + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: null, + ...overrides, + }; +} + +function buildBabyObjectMatchDraft( + overrides: Partial = {}, +): BabyObjectMatchDraft { + return { + draftId: 'baby-draft', + profileId: 'baby-profile', + templateId: 'baby-object-match', + templateName: '宝贝识物', + workTitle: '宝贝识物作品', + workDescription: '宝贝识物摘要。', + itemNames: ['苹果', '香蕉'], + itemAssets: [ + { + itemId: 'apple', + itemName: '苹果', + imageSrc: '/apple.png', + assetObjectId: null, + generationProvider: 'placeholder', + prompt: '苹果', + }, + { + itemId: 'banana', + itemName: '香蕉', + imageSrc: '/banana.png', + assetObjectId: null, + generationProvider: 'placeholder', + prompt: '香蕉', + }, + ], + themeTags: [], + publicationStatus: 'draft', + createdAt: '2026-06-04T00:00:00.000Z', + updatedAt: '2026-06-04T00:00:00.000Z', + publishedAt: null, + ...overrides, + }; +} diff --git a/src/components/platform-entry/platformCreationWorkDeleteFlow.ts b/src/components/platform-entry/platformCreationWorkDeleteFlow.ts new file mode 100644 index 00000000..fe75a735 --- /dev/null +++ b/src/components/platform-entry/platformCreationWorkDeleteFlow.ts @@ -0,0 +1,288 @@ +import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary'; +import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary'; +import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject'; +import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks'; +import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; +import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime'; +import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks'; +import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel'; +import { + buildPuzzleResultProfileId, + buildPuzzleResultWorkId, + collectDraftNoticeKeys, +} from './platformDraftGenerationShelfModel'; + +const PRIVATE_WORK_DELETE_DETAIL = '删除后会从你的作品列表中移除。'; +const PUBLIC_GALLERY_DELETE_DETAIL = '删除后会从你的作品列表和公开广场中移除。'; +const EDUTAINMENT_PUBLIC_DELETE_DETAIL = + '删除后会从你的作品列表和寓教于乐板块中移除。'; + +export type PlatformCreationWorkDeleteConfirmationModel = { + id: string; + title: string; + detail: string; + noticeKeys: string[]; +}; + +export type PlatformCreationWorkDeleteInput = + | { + kind: 'rpg-library'; + entry: Pick, 'profileId' | 'worldName'>; + } + | { + kind: 'rpg'; + work: Pick< + CustomWorldWorkSummary, + 'workId' | 'title' | 'status' | 'sessionId' | 'profileId' + >; + } + | { + kind: 'big-fish'; + work: Pick< + BigFishWorkSummary, + 'workId' | 'title' | 'status' | 'sourceSessionId' + >; + } + | { + kind: 'puzzle'; + work: Pick< + PuzzleWorkSummary, + | 'workId' + | 'profileId' + | 'sourceSessionId' + | 'workTitle' + | 'levelName' + | 'publicationStatus' + >; + } + | { + kind: 'match3d'; + work: Pick< + Match3DWorkSummary, + | 'workId' + | 'profileId' + | 'sourceSessionId' + | 'gameName' + | 'publicationStatus' + >; + } + | { + kind: 'square-hole'; + work: Pick< + SquareHoleWorkSummary, + | 'workId' + | 'profileId' + | 'sourceSessionId' + | 'gameName' + | 'publicationStatus' + >; + } + | { + kind: 'visual-novel'; + work: Pick< + VisualNovelWorkSummary, + 'profileId' | 'title' | 'publishStatus' + >; + } + | { + kind: 'baby-object-match'; + work: Pick< + BabyObjectMatchDraft, + | 'profileId' + | 'draftId' + | 'workTitle' + | 'templateName' + | 'publicationStatus' + >; + }; + +export function resolvePlatformCreationWorkDeleteConfirmationModel( + input: PlatformCreationWorkDeleteInput, +): PlatformCreationWorkDeleteConfirmationModel { + switch (input.kind) { + case 'rpg-library': + return resolveRpgLibraryDeleteConfirmationModel(input.entry); + case 'rpg': + return resolveRpgWorkDeleteConfirmationModel(input.work); + case 'big-fish': + return resolveBigFishWorkDeleteConfirmationModel(input.work); + case 'puzzle': + return resolvePuzzleWorkDeleteConfirmationModel(input.work); + case 'match3d': + return resolveMatch3DWorkDeleteConfirmationModel(input.work); + case 'square-hole': + return resolveSquareHoleWorkDeleteConfirmationModel(input.work); + case 'visual-novel': + return resolveVisualNovelWorkDeleteConfirmationModel(input.work); + case 'baby-object-match': + return resolveBabyObjectMatchDeleteConfirmationModel(input.work); + default: { + const exhaustive: never = input; + return exhaustive; + } + } +} + +function resolveStatusDeleteDetail( + status: string, + publishedDetail = PUBLIC_GALLERY_DELETE_DETAIL, +) { + return status === 'published' ? publishedDetail : PRIVATE_WORK_DELETE_DETAIL; +} + +function resolveTrimmedTitle( + value: string | null | undefined, + fallback: string, +) { + const trimmedValue = value?.trim(); + return trimmedValue || fallback; +} + +function resolveRpgLibraryDeleteConfirmationModel( + entry: Pick, 'profileId' | 'worldName'>, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: entry.profileId, + title: entry.worldName, + detail: PUBLIC_GALLERY_DELETE_DETAIL, + noticeKeys: [], + }; +} + +function resolveRpgWorkDeleteConfirmationModel( + work: Pick< + CustomWorldWorkSummary, + 'workId' | 'title' | 'status' | 'sessionId' | 'profileId' + >, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: work.workId, + title: work.title, + detail: resolveStatusDeleteDetail(work.status), + noticeKeys: collectDraftNoticeKeys('rpg', [ + work.workId, + work.sessionId, + work.profileId, + ]), + }; +} + +function resolveBigFishWorkDeleteConfirmationModel( + work: Pick< + BigFishWorkSummary, + 'workId' | 'title' | 'status' | 'sourceSessionId' + >, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: work.workId, + title: work.title, + detail: resolveStatusDeleteDetail(work.status), + noticeKeys: collectDraftNoticeKeys('big-fish', [ + work.workId, + work.sourceSessionId, + ]), + }; +} + +function resolvePuzzleWorkDeleteConfirmationModel( + work: Pick< + PuzzleWorkSummary, + | 'workId' + | 'profileId' + | 'sourceSessionId' + | 'workTitle' + | 'levelName' + | 'publicationStatus' + >, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: work.workId, + title: resolveTrimmedTitle( + work.workTitle, + resolveTrimmedTitle(work.levelName, '未命名拼图'), + ), + detail: resolveStatusDeleteDetail(work.publicationStatus), + noticeKeys: collectDraftNoticeKeys('puzzle', [ + work.workId, + work.profileId, + work.sourceSessionId, + buildPuzzleResultWorkId(work.sourceSessionId), + buildPuzzleResultProfileId(work.sourceSessionId), + ]), + }; +} + +function resolveMatch3DWorkDeleteConfirmationModel( + work: Pick< + Match3DWorkSummary, + | 'workId' + | 'profileId' + | 'sourceSessionId' + | 'gameName' + | 'publicationStatus' + >, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: work.workId, + title: work.gameName, + detail: resolveStatusDeleteDetail(work.publicationStatus), + noticeKeys: collectDraftNoticeKeys('match3d', [ + work.workId, + work.profileId, + work.sourceSessionId, + ]), + }; +} + +function resolveSquareHoleWorkDeleteConfirmationModel( + work: Pick< + SquareHoleWorkSummary, + | 'workId' + | 'profileId' + | 'sourceSessionId' + | 'gameName' + | 'publicationStatus' + >, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: work.workId, + title: work.gameName, + detail: resolveStatusDeleteDetail(work.publicationStatus), + noticeKeys: collectDraftNoticeKeys('square-hole', [ + work.workId, + work.profileId, + work.sourceSessionId, + ]), + }; +} + +function resolveVisualNovelWorkDeleteConfirmationModel( + work: Pick, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: work.profileId, + title: work.title || '未命名视觉小说', + detail: resolveStatusDeleteDetail(work.publishStatus), + noticeKeys: collectDraftNoticeKeys('visual-novel', [work.profileId]), + }; +} + +function resolveBabyObjectMatchDeleteConfirmationModel( + work: Pick< + BabyObjectMatchDraft, + 'profileId' | 'draftId' | 'workTitle' | 'templateName' | 'publicationStatus' + >, +): PlatformCreationWorkDeleteConfirmationModel { + return { + id: work.profileId, + title: resolveTrimmedTitle(work.workTitle, work.templateName), + detail: resolveStatusDeleteDetail( + work.publicationStatus, + EDUTAINMENT_PUBLIC_DELETE_DETAIL, + ), + noticeKeys: collectDraftNoticeKeys('baby-object-match', [ + work.profileId, + work.draftId, + ]), + }; +}