diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 6092eeb5..4ca4e204 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -42,7 +42,7 @@ 1. 草稿页作品卡对齐发现页列表卡风格:左侧信息,右侧封面图,移动端单列,桌面两到三列。 2. 草稿页顶部 `全部 / 草稿 / 已发布` 筛选与发现页 `推荐 / 今日 / 分类 / 排行` 频道标签复用同一选中 / 未选中视觉,即 `platform-mobile-home-channel` 与 `platform-mobile-home-channel--active`,不再使用旧 `platform-tab` 胶囊样式。 -3. 草稿页与底部导航的未读提示点统一使用平台暖棕色点和暖棕光晕,不再使用红点或红色 glow;草稿 Tab 作品架卡片无论草稿 / 已发布都不外露作者信息;已发布作品卡右上角直接显示无边框分享 icon。删除等破坏性动作继续收口到左滑或长按操作层。 +3. 草稿页与底部导航的未读提示点统一使用平台暖棕色点和暖棕光晕,不再使用红点或红色 glow;草稿 Tab 作品架卡片无论草稿 / 已发布都不外露作者信息;已发布作品卡右上角直接显示无边框分享 icon。删除等破坏性动作在作品卡上也要直接开放独立删除入口,左滑或长按仅作为辅助操作层。 4. 生成中作品在整卡上加等待遮罩,但不移除作品基础信息。 5. 生成中状态不能只存在前端内存 notice。后端作品摘要必须下发可恢复的 `generationStatus`;前端刷新或退出产品后,作品架优先用摘要状态恢复等待遮罩,本轮内存 notice 只作为即时反馈。 6. 点击 `generationStatus=generating` 的草稿卡必须恢复对应玩法的生成进度页,不能进入空白结果页或普通工作区;恢复生成页的 `startedAtMs` 使用进入生成页的当前时间,作品摘要 `updatedAt` 只用于排序和摘要展示,不参与假进度起算。 diff --git a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx index ff231fa7..1d0d9680 100644 --- a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx +++ b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx @@ -560,7 +560,7 @@ test('creation hub shows RPG public work code from published library entry', () expect(screen.queryByText('CW-00000001')).toBeNull(); }); -test('creation hub hides persisted draft delete action behind swipe underlay', () => { +test('creation hub exposes persisted draft delete action directly on the card', () => { const { container } = render( { @@ -607,7 +607,9 @@ test('creation hub reveals persisted draft delete action from left swipe', () => }); fireEvent.touchEnd(card); - expect(screen.getByRole('button', { name: '删除' })).toBeTruthy(); + expect( + container.querySelector('.creation-work-card__swipe-button--danger'), + ).toBeTruthy(); expect( container.querySelector('.creation-work-card-shell--actions-visible'), ).toBeTruthy(); @@ -615,7 +617,7 @@ test('creation hub reveals persisted draft delete action from left swipe', () => test('creation hub reveals persisted draft delete action from keyboard', async () => { const user = userEvent.setup(); - render( + const { container } = render( { +test('creation hub published work delete action is directly visible', async () => { const user = userEvent.setup(); const onDeletePuzzle = vi.fn(); const onOpenPuzzleDetail = vi.fn(); @@ -751,12 +759,6 @@ test('creation hub published work delete action is revealed without opening card />, ); - expect(screen.queryByRole('button', { name: '删除' })).toBeNull(); - expect(screen.getByRole('button', { name: '分享' })).toBeTruthy(); - - screen.getByRole('button', { name: /查看详情《待删拼图》/u }).focus(); - await user.keyboard('{ArrowLeft}'); - expect(screen.getByRole('button', { name: '删除' })).toBeTruthy(); expect(screen.getByRole('button', { name: '分享' })).toBeTruthy(); @@ -768,6 +770,115 @@ test('creation hub published work delete action is revealed without opening card expect(onOpenPuzzleDetail).not.toHaveBeenCalled(); }); +test('creation hub exposes work delete action directly on card', async () => { + const user = userEvent.setup(); + const onDeletePuzzle = vi.fn(); + const onOpenPuzzleDetail = vi.fn(); + + render( + {}} + onCreateType={noopCreateType} + onOpenDraft={() => {}} + onEnterPublished={() => {}} + onOpenPuzzleDetail={onOpenPuzzleDetail} + onDeletePuzzle={onDeletePuzzle} + entryConfig={testEntryConfig} + creationTypes={testCreationTypes} + />, + ); + + await user.click(screen.getByRole('button', { name: '删除' })); + + expect(onDeletePuzzle).toHaveBeenCalledWith( + expect.objectContaining({ profileId: 'puzzle-profile-direct-delete' }), + ); + expect(onOpenPuzzleDetail).not.toHaveBeenCalled(); +}); + +test('creation hub keeps swipe delete action available', async () => { + const user = userEvent.setup(); + const onDeletePuzzle = vi.fn(); + const onOpenPuzzleDetail = vi.fn(); + + const { container } = render( + {}} + onCreateType={noopCreateType} + onOpenDraft={() => {}} + onEnterPublished={() => {}} + onOpenPuzzleDetail={onOpenPuzzleDetail} + onDeletePuzzle={onDeletePuzzle} + entryConfig={testEntryConfig} + creationTypes={testCreationTypes} + />, + ); + + const card = screen.getByRole('button', { name: /查看详情《左滑删除拼图》/u }); + fireEvent.touchStart(card, { + touches: [{ clientX: 180, clientY: 20 }], + }); + fireEvent.touchMove(card, { + touches: [{ clientX: 80, clientY: 22 }], + }); + fireEvent.touchEnd(card); + + const swipeDeleteButton = container.querySelector( + '.creation-work-card__swipe-button--danger', + ) as HTMLButtonElement | null; + expect(swipeDeleteButton).toBeTruthy(); + await user.click(swipeDeleteButton!); + + expect(onDeletePuzzle).toHaveBeenCalledWith( + expect.objectContaining({ profileId: 'puzzle-profile-swipe-delete' }), + ); + expect(onOpenPuzzleDetail).not.toHaveBeenCalled(); +}); + test('creation hub opens persisted rpg drafts by card click', async () => { const user = userEvent.setup(); const openedItems: CustomWorldWorkSummary[] = []; @@ -942,7 +1053,7 @@ test('creation hub left swipe draft reveals delete without opening card', () => const onDeletePublished = vi.fn(); const onOpenDraft = vi.fn(); - render( + const { container } = render( }); fireEvent.touchEnd(card); - expect(screen.getByRole('button', { name: '删除' })).toBeTruthy(); + expect( + container.querySelector('.creation-work-card__swipe-button--danger'), + ).toBeTruthy(); expect(onOpenDraft).not.toHaveBeenCalled(); }); diff --git a/src/components/custom-world-home/CustomWorldWorkCard.tsx b/src/components/custom-world-home/CustomWorldWorkCard.tsx index 392282c4..00f38323 100644 --- a/src/components/custom-world-home/CustomWorldWorkCard.tsx +++ b/src/components/custom-world-home/CustomWorldWorkCard.tsx @@ -676,43 +676,75 @@ export function CustomWorldWorkCard({ {displayTitle} - {canUseShareAction ? ( - - ) : null} +
+ {canUseShareAction ? ( + + ) : null} + {onDelete ? ( + + ) : null} +
diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 48a1278c..301342ea 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -2081,7 +2081,7 @@ function pickDraftCompletionDialogSourceId( function buildDraftCompletionDialogSource( kind: CreationWorkShelfKind, ids: Array, -) { +): string { const sourceId = pickDraftCompletionDialogSourceId(ids); switch (kind) { case 'rpg': @@ -2103,6 +2103,7 @@ function buildDraftCompletionDialogSource( case 'baby-object-match': return formatPlatformTaskCompletionSource('宝贝识物草稿', sourceId); } + return formatPlatformTaskCompletionSource('创作草稿', sourceId); } function createMiniGameDraftGenerationStateForRestoredDraft( diff --git a/src/index.css b/src/index.css index d1807475..5d82171f 100644 --- a/src/index.css +++ b/src/index.css @@ -2044,7 +2044,14 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { white-space: normal; } -.creation-work-card__share-button { +.creation-work-card__quick-actions { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + gap: 0.12rem; +} + +.creation-work-card__quick-action-button { display: inline-flex; width: 2rem; height: 2rem; @@ -2061,17 +2068,32 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock { transform 160ms ease; } -.creation-work-card__share-button:hover { +.creation-work-card__quick-action-button:hover { transform: translateY(-1px); background: color-mix(in srgb, var(--platform-cool-bg) 24%, transparent); color: var(--platform-cool-text); } -.creation-work-card__share-button:focus-visible { +.creation-work-card__quick-action-button:focus-visible { outline: 2px solid var(--platform-cool-border); outline-offset: 2px; } +.creation-work-card__quick-action-button--danger { + color: color-mix(in srgb, #c7653d 78%, var(--platform-text-soft)); +} + +.creation-work-card__quick-action-button--danger:hover { + background: color-mix(in srgb, #c7653d 18%, transparent); + color: #a9472c; +} + +.creation-work-card__quick-action-button:disabled { + cursor: not-allowed; + opacity: 0.62; + transform: none; +} + .creation-work-card__meta { display: flex; min-width: 0;