From 06bf03a28c4c3f4e3d2b2d8eeb1cb4b326f0bdaf Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 11 Jun 2026 01:41:15 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E6=94=B6=E5=8F=A3=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E7=A9=BA=E6=80=81=E4=B8=8E=E5=8A=A8=E4=BD=9C=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 作品架异步状态切换复用 PlatformAsyncStatePanel 复制反馈动作外观改为组合 PlatformActionButton 结果页与调试面板空态继续收口到 PlatformEmptyState 暗色私聊与工坊按钮改为复用 PlatformActionButton 更新 PlatformUiKit 收口文档与团队决策记录 --- .hermes/shared-memory/decision-log.md | 5 +- ...】PlatformUiKit弹窗组件收口计划-2026-06-08.md | 3 + src/components/AdventureEntityModal.test.tsx | 7 ++ src/components/AdventureEntityModal.tsx | 14 +-- src/components/InventoryPanel.test.tsx | 5 + src/components/InventoryPanel.tsx | 15 ++- .../common/CopyFeedbackButton.test.tsx | 6 +- src/components/common/CopyFeedbackButton.tsx | 57 +++++---- ...ustomWorldCreationHub.interaction.test.tsx | 25 ++++ .../CustomWorldCreationHub.tsx | 108 ++++++++++-------- .../match3d-result/Match3DResultView.tsx | 46 ++++---- .../puzzle-result/PuzzleResultView.test.tsx | 9 +- .../puzzle-result/PuzzleResultView.tsx | 12 +- .../RpgCreationAssetDebugPanel.test.tsx | 9 +- .../RpgCreationAssetDebugPanel.tsx | 11 +- 15 files changed, 202 insertions(+), 130 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 3914583b..0a4ebd06 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -2075,7 +2075,10 @@ - 决策:简单泥点确认流的开关状态机统一收口到 `src/components/common/useMudPointConfirmController.ts`,只暴露 `open / requestOpen / close / confirm`,不持有点数、标题、描述或禁用态等业务字段;`PuzzleCreationWorkspace.tsx`、`Match3DCreationWorkspace.tsx` 与 `Match3DResultView.tsx` 的两个批量素材面板已接入。`PuzzleResultView.tsx` 和 `RpgCreationRoleAssetStudioModalImpl.tsx` 这类节奏不同或携带 pending payload 的场景继续保留本地状态机,避免把简单 hook 扩成泛型动作路由器。 - 决策:标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态 overlay 不跟这条线混收,继续保留局部 close 语义。 - 决策:平台入口的创作前置泥点阻断提示只在 `platform-entry` 局部抽成 `src/components/platform-entry/PlatformDraftGenerationPointNoticeDialog.tsx`,并使用 `DraftGenerationPointNotice` union(`insufficient-points` / `balance-load-failed`)承接业务真相;不要在 `common/` 再抽一个泛化 `BlockingNoticeDialog`,否则会把 `PlatformAcknowledgeStatusDialog` 的样式透传再包装一层而不缩小调用面。 -- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +- 决策:`PlatformAsyncStatePanel` 从 profile modal 扩展到作品架类白底 panel;`CustomWorldCreationHub.tsx` 的作品架主体现在也统一走 `loadingState / emptyState / children` 三段 slot,但 error + 重试继续留在业务层外侧,不把共享组件扩成“banner + retry + content”全能状态机。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用这套骨架。 +- 决策:`CopyFeedbackButton.tsx` 的 `actionSurface` 分支继续收口到 `PlatformActionButton`,`pill` 分支继续保留 `PlatformPillBadge` 风格;复制反馈按钮不再直接调用 `getPlatformActionButtonClassName` 手拼平台按钮基础 chrome。后续同类“复制状态机 + 平台动作按钮”组合优先直接复用 `CopyFeedbackButton`,不要在业务页重新混写图标、文案、aria 和动作按钮 class。 +- 决策:白底 / 暗色面板里的轻量空态和普通 CTA 继续向共享组件收口。`PuzzleResultView.tsx` 的缺草稿提示、`RpgCreationAssetDebugPanel.tsx` 的空诊断提示改为 `PlatformEmptyState`,`Match3DResultView.tsx` 的引用素材列表直接复用 `PlatformAssetPickerGrid` 自己的空态;`AdventureEntityModal.tsx` 的私聊按钮和 `InventoryPanel.tsx` 的锻造 / 合成按钮改为 `PlatformActionButton surface="editorDark"`,业务页只贴局部 sky / emerald 皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel" size="inline"`,暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`。 +- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx`、`npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx`、`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 ## 2026-05-26 敲木鱼发布后作品架与推荐流刷新口径 diff --git a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md index 82c3f6bf..3b1cac92 100644 --- a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md +++ b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md @@ -254,6 +254,9 @@ 19.3.30. `PlatformSegmentedTabs` 继续承接首页 / 结果页里剩余的横向 rail 与二选一切换:`RpgEntryHomeView.tsx` 的 discover channel bar、移动端 / 桌面端分类 chip rail,`CustomWorldEntityCatalog.tsx` 的 `RESULT_TABS` sticky rail,以及 `PlatformProfileRechargeModal.tsx` 的“泥点充值 / 会员卡”切换条已迁移。`CustomWorldEntityCatalog` 通过 `ReactNode label` 保留“标题 + count”两行内容,`RpgEntryHomeView` 和个人中心切换条继续通过 `itemClassName` 贴回本地皮肤;同类 rail 优先直接复用 `PlatformSegmentedTabs`,测试也应按 `role="tablist" / "tab"` 查询,不再把这些切换项当普通 button。 19.3.31. 简单泥点确认流的开关状态机收口到 `src/components/common/useMudPointConfirmController.ts`;该 hook 只承接 `open / requestOpen / close / confirm` 四个动作,`confirm` 固定先关弹窗再执行回调,不持有 `points / title / description / confirmDisabled` 之类业务字段。`PuzzleCreationWorkspace.tsx`、`Match3DCreationWorkspace.tsx` 和 `Match3DResultView.tsx` 的两个批量素材面板已接入;`PuzzleCreationWorkspace` 仍在业务页判断“只有 `aiRedraw` 才弹确认”。`PuzzleResultView.tsx` 与 `RpgCreationRoleAssetStudioModalImpl.tsx` 这类要么节奏不同、要么携带 pending payload 的场景先保留本地状态机,不把 hook 扩成泛型动作路由器。 19.3.32. 标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"`;`PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态专属 overlay 继续保留本地 close 语义,不把 `PlatformModalCloseButton` 硬塞进非平台 modal header 场景。 +19.3.33. `PlatformAsyncStatePanel` 从 profile modal 扩展到作品架:`CustomWorldCreationHub.tsx` 的作品架主体现在也统一通过 `loadingState / emptyState / children` 三个 slot 切换,保留外层 error + 重试提示不并入共享状态骨架。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用 `PlatformAsyncStatePanel`,不要再在业务 JSX 中重复拼 skeleton 和“当前筛选下没有内容”的分支。验证命令:`npx vitest run src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx`、`npm run check:encoding`。 +19.3.34. `CopyFeedbackButton.tsx` 的 `actionSurface` 分支继续向共享按钮收口:带平台动作外观的复制按钮现在直接组合 `PlatformActionButton`,仅保留 `pill` 分支继续复用 `PlatformPillBadge` 风格。复制反馈按钮不再手动调用 `getPlatformActionButtonClassName` 拼平台按钮基础 chrome;后续同类“复制状态机 + 平台动作按钮”组合也优先走 `CopyFeedbackButton + PlatformActionButton`,不要在业务页或按钮组件里重新混写图标、文案、aria 和 class。验证命令:`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx`。 +19.3.35. 白底 / 暗色面板里的轻量空态和普通 CTA 继续按共享组件收口:`PuzzleResultView.tsx` 的“还没有可编辑的拼图草稿”和 `RpgCreationAssetDebugPanel.tsx` 的“没有可诊断项”改为 `PlatformEmptyState`,`Match3DResultView.tsx` 的引用素材列表直接交给 `PlatformAssetPickerGrid` 自己处理空态;`AdventureEntityModal.tsx` 的私聊按钮与 `InventoryPanel.tsx` 的锻造 / 合成按钮改为 `PlatformActionButton surface="editorDark"`,业务页只贴回 sky / emerald 局部皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel" size="inline"`,暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`,不要再手写基础按钮壳或空态面板 chrome。验证命令:`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx`、`npm run check:encoding`。 19.3. creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作、首页激励 CTA 和 prompt suggestion 按钮迁移到 `PlatformIconButton` / `PlatformActionButton`;首页继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏、抽屉和品牌化胶囊视觉,不把视觉回收和语义收口绑成一次大改。`Beta` 徽标和历史记录纯文本行暂保留本地实现,等出现更多同构轻量列表行后再评估是否抽新的共享 row primitive。 19.4. 大鱼吃小鱼结果页 hero 的返回入口迁移到 `PlatformIconButton variant="darkMini"`,测试 / 发布动作迁移到 `PlatformActionButton surface="editorDark"`;结果页只保留测试运行、发布提交和文案状态语义,不再手写 hero 顶栏按钮壳。 19.4.1. 大鱼吃小鱼结果页的发布失败弹层迁移到 `src/components/common/PlatformStatusDialog.tsx`;`PlatformStatusDialog` 补充自定义图标、可访问标签和动作按钮样式透传后,`BigFishResultView` 不再保留 `BigFishResultErrorModal` 内联的 `UnifiedConfirmDialog + PlatformIconBadge` 组合。结果页只保留失败文案和关闭回调,发布失败的状态图标、遮罩、白底面板和“知道了”主动作统一由共享状态弹层承接。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx`、`npm run typecheck`。 diff --git a/src/components/AdventureEntityModal.test.tsx b/src/components/AdventureEntityModal.test.tsx index 1945c883..3fdb2641 100644 --- a/src/components/AdventureEntityModal.test.tsx +++ b/src/components/AdventureEntityModal.test.tsx @@ -397,6 +397,7 @@ test('私聊和队友收束复用暗色 tint PlatformSubpanel chrome', () => { const companionResolutionEcho = screen.getByTestId( 'companion-resolution-echo', ); + const privateChatButton = screen.getByRole('button', { name: '聊天' }); expect(privateChatPanel.className).toContain('border-sky-400/18'); expect(privateChatPanel.className).toContain('bg-sky-500/8'); @@ -404,6 +405,12 @@ test('私聊和队友收束复用暗色 tint PlatformSubpanel chrome', () => { expect(companionResolutionEcho.className).toContain('border-emerald-400/18'); expect(companionResolutionEcho.className).toContain('bg-emerald-500/8'); expect(companionResolutionEcho.className).toContain('rounded-xl'); + expect(privateChatButton.className).toContain( + 'platform-action-button--editor-dark', + ); + expect(privateChatButton.className).toContain('rounded-xl'); + expect(privateChatButton.className).toContain('bg-sky-400/15'); + expect(privateChatButton.className).toContain('disabled:bg-black/20'); }); test('技能详情静态标签复用暗色 PlatformPillBadge chrome', () => { diff --git a/src/components/AdventureEntityModal.tsx b/src/components/AdventureEntityModal.tsx index fcd63550..39dc7970 100644 --- a/src/components/AdventureEntityModal.tsx +++ b/src/components/AdventureEntityModal.tsx @@ -78,6 +78,7 @@ import { StatusRow, } from './CharacterInfoShared'; import { PlatformEmptyState } from './common/PlatformEmptyState'; +import { PlatformActionButton } from './common/PlatformActionButton'; import { PlatformPillBadge } from './common/PlatformPillBadge'; import { PlatformSubpanel } from './common/PlatformSubpanel'; import { GENERIC_NPC_SCENE_SCALE } from './game-canvas/GameCanvasShared'; @@ -1106,8 +1107,9 @@ export function AdventureEntityModal({ : `好感达到 ${privateChatUnlockAffinity ?? DEFAULT_PRIVATE_CHAT_UNLOCK_AFFINITY} 后解锁,当前 ${companionNpcState?.affinity ?? 0}。`} - + diff --git a/src/components/InventoryPanel.test.tsx b/src/components/InventoryPanel.test.tsx index ce8ab093..92d433f3 100644 --- a/src/components/InventoryPanel.test.tsx +++ b/src/components/InventoryPanel.test.tsx @@ -83,6 +83,7 @@ test('背包工坊材料需求状态复用暗色平台胶囊标签', () => { const missingRequirement = screen.getByText('木材 0/1'); const forgePanel = screen.getByText('工坊').closest('section'); const recipePanel = screen.getByText('潮汐护符').closest('section'); + const forgeButton = screen.getByRole('button', { name: '锻造' }); expect(metRequirement.className).toContain('rounded-full'); expect(metRequirement.className).toContain('font-black'); @@ -95,6 +96,10 @@ test('背包工坊材料需求状态复用暗色平台胶囊标签', () => { expect(forgePanel?.className).toContain('bg-black/25'); expect(recipePanel?.className).toContain('border-white/10'); expect(recipePanel?.className).toContain('bg-black/25'); + expect(forgeButton.className).toContain('platform-action-button--editor-dark'); + expect(forgeButton.className).toContain('rounded-lg'); + expect(forgeButton.className).toContain('bg-emerald-500/10'); + expect(forgeButton.className).toContain('disabled:bg-black/20'); }); test('背包文书和故事档案区块复用暗色 PlatformSubpanel chrome', () => { diff --git a/src/components/InventoryPanel.tsx b/src/components/InventoryPanel.tsx index 203fd897..2b2516bc 100644 --- a/src/components/InventoryPanel.tsx +++ b/src/components/InventoryPanel.tsx @@ -15,6 +15,7 @@ import { WorldType, } from '../types'; import { PlatformPillBadge } from './common/PlatformPillBadge'; +import { PlatformActionButton } from './common/PlatformActionButton'; import { PlatformStatusMessage } from './common/PlatformStatusMessage'; import { PlatformSubpanel } from './common/PlatformSubpanel'; import { @@ -216,8 +217,10 @@ export function InventoryPanel({ 花费:{recipe.currencyText} - +
{recipe.requirements.map((requirement) => { diff --git a/src/components/common/CopyFeedbackButton.test.tsx b/src/components/common/CopyFeedbackButton.test.tsx index c2707fe7..bbc85901 100644 --- a/src/components/common/CopyFeedbackButton.test.tsx +++ b/src/components/common/CopyFeedbackButton.test.tsx @@ -105,15 +105,19 @@ test('can opt into platform action button chrome', () => { actionSurface="platform" actionShape="pill" actionFullWidth + aria-label="复制错误详情" + title="复制错误详情" />, ); - const button = screen.getByRole('button', { name: '复制报错' }); + const button = screen.getByRole('button', { name: '复制错误详情' }); expect(button.className).toContain('platform-button--primary'); expect(button.className).toContain('w-full'); expect(button.className).toContain('rounded-full'); expect(button.className).toContain('disabled:cursor-not-allowed'); + expect(button.getAttribute('title')).toBe('复制错误详情'); + expect(button.textContent).toContain('复制报错'); }); test('can opt into shared pill action chrome', () => { diff --git a/src/components/common/CopyFeedbackButton.tsx b/src/components/common/CopyFeedbackButton.tsx index 7a37e563..d88a5750 100644 --- a/src/components/common/CopyFeedbackButton.tsx +++ b/src/components/common/CopyFeedbackButton.tsx @@ -2,12 +2,12 @@ import { Check, Copy } from 'lucide-react'; import type { ButtonHTMLAttributes, ReactNode } from 'react'; import { - getPlatformActionButtonClassName, type PlatformActionButtonSize, type PlatformActionButtonShape, type PlatformActionButtonSurface, type PlatformActionButtonTone, } from './platformActionButtonModel'; +import { PlatformActionButton } from './PlatformActionButton'; import { getPlatformPillBadgeClassName, type PlatformPillBadgeSize, @@ -105,38 +105,53 @@ export function CopyFeedbackButton({ : typeof idleLabel === 'string' ? idleLabel : undefined); + const resolvedAriaLabel = ariaLabel ?? accessibleLabel; + const resolvedTitle = + title ?? (typeof accessibleLabel === 'string' ? accessibleLabel : undefined); + const content = ( + <> + {showIcon ? icon : null} + {showLabel ? {label} : null} + + ); + + if (actionSurface) { + return ( + + {content} + + ); + } return ( ); } diff --git a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx index b7a628d6..d9ff1b79 100644 --- a/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx +++ b/src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx @@ -847,6 +847,31 @@ test('creation hub works-only tab filters bark battle draft and published works' expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(barkBattlePublishedItem); }); +test('creation hub keeps filtered empty copy when selected tab has no works', async () => { + const user = userEvent.setup(); + + render( + {}} + onCreateType={noopCreateType} + onOpenDraft={() => {}} + onEnterPublished={() => {}} + entryConfig={testEntryConfig} + creationTypes={testCreationTypes} + />, + ); + + await user.click(screen.getByRole('tab', { name: '草稿 0' })); + + expect(screen.getByText('当前筛选下没有内容')).toBeTruthy(); + expect(screen.queryByText('还没有作品')).toBeNull(); +}); + test('creation hub published work delete action stays in revealed side actions', async () => { const user = userEvent.setup(); const onDeletePuzzle = vi.fn(); diff --git a/src/components/custom-world-home/CustomWorldCreationHub.tsx b/src/components/custom-world-home/CustomWorldCreationHub.tsx index 4c73d87a..8bd05eec 100644 --- a/src/components/custom-world-home/CustomWorldCreationHub.tsx +++ b/src/components/custom-world-home/CustomWorldCreationHub.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useState } from 'react'; import { resolveSelectionStageFromPath } from '../../routing/appPageRoutes'; import type { CreationEntryConfig } from '../../services/creationEntryConfigService'; import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformAsyncStatePanel } from '../common/PlatformAsyncStatePanel'; import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; import type { PublishShareModalPayload } from '../common/publishShareModalModel'; @@ -206,6 +207,7 @@ export function CustomWorldCreationHub({ const recentCreationTypeIds = [ ...new Set(recentWorkItems.map((item) => item.kind)), ]; + const isWorkShelfEmpty = !loading && filteredItems.length === 0; function handleOpenShelfItem(item: CreationWorkShelfItem) { onOpenShelfItem?.(item); @@ -238,6 +240,55 @@ export function CustomWorldCreationHub({ const showStartCard = mode !== 'works-only'; const showWorkShelf = mode !== 'start-only'; + const workShelfLoadingState = ( +
+ {Array.from({ length: 3 }).map((_, index) => ( + +
+
+
+
+
+
+
+
+ + ))} +
+ ); + const workShelfEmptyState = ( + + ); + const workShelfContent = ( +
+ {filteredItems.map((item) => ( + { + handleOpenShelfItem(item); + }} + onDelete={buildDeleteAction(item)} + deleteBusy={deletingWorkId === item.id} + onShare={buildShareAction(item)} + onClaimPointIncentive={buildPointIncentiveAction(item)} + pointIncentiveBusy={ + item.source.kind === 'puzzle' && + claimingPuzzleProfileId === item.source.item.profileId + } + /> + ))} +
+ ); return (
@@ -276,55 +327,14 @@ export function CustomWorldCreationHub({ ) : null} {showWorkShelf ? ( - loading ? ( -
- {Array.from({ length: 3 }).map((_, index) => ( - -
-
-
-
-
-
-
-
- - ))} -
- ) : filteredItems.length > 0 ? ( -
- {filteredItems.map((item) => ( - { - handleOpenShelfItem(item); - }} - onDelete={buildDeleteAction(item)} - deleteBusy={deletingWorkId === item.id} - onShare={buildShareAction(item)} - onClaimPointIncentive={buildPointIncentiveAction(item)} - pointIncentiveBusy={ - item.source.kind === 'puzzle' && - claimingPuzzleProfileId === item.source.item.profileId - } - /> - ))} -
- ) : shelfItems.length === 0 ? ( - - ) : ( - - ) + + {workShelfContent} + ) : null}
diff --git a/src/components/match3d-result/Match3DResultView.tsx b/src/components/match3d-result/Match3DResultView.tsx index 8cdbde0a..a14e06ec 100644 --- a/src/components/match3d-result/Match3DResultView.tsx +++ b/src/components/match3d-result/Match3DResultView.tsx @@ -1673,30 +1673,28 @@ function Match3DCoverImageEditor({ ))}
) : null} - {sourceAssets.length > 0 ? ( - asset.id} - getImageSrc={(asset) => asset.imageSrc} - getImageAlt={() => ''} - getTitle={(asset) => asset.label} - getAriaLabel={(asset) => `引用${asset.label}`} - isSelected={(asset) => - referenceImages.some( - (reference) => reference.imageSrc === asset.imageSrc, - ) - } - onSelect={(asset) => onReferenceSelect(asset.imageSrc)} - gridClassName="grid grid-cols-3 gap-2 sm:grid-cols-4" - cardClassName="bg-white/74" - cardRadiusClassName="rounded-[1rem]" - imageShellClassName="aspect-square" - bodyClassName="truncate px-2 py-2 text-[11px] font-semibold text-[var(--platform-text-base)]" - /> - ) : null} + asset.id} + getImageSrc={(asset) => asset.imageSrc} + getImageAlt={() => ''} + getTitle={(asset) => asset.label} + getAriaLabel={(asset) => `引用${asset.label}`} + isSelected={(asset) => + referenceImages.some( + (reference) => reference.imageSrc === asset.imageSrc, + ) + } + onSelect={(asset) => onReferenceSelect(asset.imageSrc)} + gridClassName="grid grid-cols-3 gap-2 sm:grid-cols-4" + cardClassName="bg-white/74" + cardRadiusClassName="rounded-[1rem]" + imageShellClassName="aspect-square" + bodyClassName="truncate px-2 py-2 text-[11px] font-semibold text-[var(--platform-text-base)]" + />
) : null} diff --git a/src/components/puzzle-result/PuzzleResultView.test.tsx b/src/components/puzzle-result/PuzzleResultView.test.tsx index b15cbcef..f8068b7e 100644 --- a/src/components/puzzle-result/PuzzleResultView.test.tsx +++ b/src/components/puzzle-result/PuzzleResultView.test.tsx @@ -81,7 +81,7 @@ function stubReferenceImageUpload(dataUrl: string) { vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader); } -test('renders missing draft notice with shared PlatformSubpanel chrome', () => { +test('renders missing draft notice with shared PlatformEmptyState chrome', () => { render( { const noticePanel = screen .getByText('还没有可编辑的拼图草稿') - .closest('.platform-subpanel'); + .closest('.platform-empty-state'); + expect(noticePanel?.className).toContain('platform-empty-state'); expect(noticePanel?.className).toContain('rounded-[1rem]'); - expect(noticePanel?.className).toContain('sm:p-5'); - expect(noticePanel?.className).toContain('text-[var(--platform-text-base)]'); + expect(noticePanel?.className).toContain('py-5'); + expect(noticePanel?.className).toContain('text-[var(--platform-text-soft)]'); }); function createSession( diff --git a/src/components/puzzle-result/PuzzleResultView.tsx b/src/components/puzzle-result/PuzzleResultView.tsx index 4f4e249e..59dd77c9 100644 --- a/src/components/puzzle-result/PuzzleResultView.tsx +++ b/src/components/puzzle-result/PuzzleResultView.tsx @@ -23,6 +23,7 @@ import { readPuzzleReferenceImageAsDataUrl } from '../../services/puzzleReferenc import { useAuthUi } from '../auth/AuthUiContext'; import { CreativeImageInputPanel } from '../common/CreativeImageInputPanel'; import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; import { PlatformIconBadge } from '../common/PlatformIconBadge'; import { PlatformIconButton } from '../common/PlatformIconButton'; @@ -1556,14 +1557,13 @@ export function PuzzleResultView({ if (!draft || !editState || !syncedDraft) { return (
- 还没有可编辑的拼图草稿 - +
); } diff --git a/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx b/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx index 49e97437..54b3afb3 100644 --- a/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx +++ b/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx @@ -173,7 +173,7 @@ test('RPG asset debug panel uses PlatformSubpanel shells for summary and entries } }); -test('RPG asset debug panel uses PlatformSubpanel shell for empty state', () => { +test('RPG asset debug panel uses PlatformEmptyState shell for empty state', () => { const emptyProfile = { ...createProfileWithAssets(), playableNpcs: [], @@ -187,14 +187,15 @@ test('RPG asset debug panel uses PlatformSubpanel shell for empty state', () => const emptyPanel = screen .getByText('当前结果页 profile 里没有拿到任何可诊断的图片地址。') - .closest('section'); + .closest('.platform-empty-state'); expect(screen.getByText('0项')).toBeTruthy(); - expect(emptyPanel?.className).toContain('platform-subpanel'); + expect(emptyPanel?.className).toContain('platform-empty-state'); expect(emptyPanel?.className).toContain('rounded-2xl'); + expect(emptyPanel?.className).toContain('bg-black/20'); expect( container.querySelectorAll( - 'section.platform-subpanel, div.platform-subpanel', + 'section.platform-subpanel, div.platform-subpanel, div.platform-empty-state', ), ).toHaveLength(5); }); diff --git a/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.tsx b/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.tsx index e90c9835..645cd172 100644 --- a/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.tsx +++ b/src/components/rpg-creation-result/RpgCreationAssetDebugPanel.tsx @@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { resolveAssetReadUrl } from '../../services/assetReadUrlService'; import type { CustomWorldProfile } from '../../types'; +import { PlatformEmptyState } from '../common/PlatformEmptyState'; import { PlatformPillBadge } from '../common/PlatformPillBadge'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; @@ -303,12 +304,14 @@ export function RpgCreationAssetDebugPanel({ ); }) ) : ( - 当前结果页 profile 里没有拿到任何可诊断的图片地址。 - + )}