diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index f8734426..b7193bf9 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -46,6 +46,7 @@ - 2026-06-10 追加:RPG 首页个人中心的泥点账单、每日任务和兑换码弹层统一抽到 `src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx`、`src/components/platform-entry/PlatformProfileTaskCenterModal.tsx` 与 `src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx`;`RpgEntryHomeView` 只保留打开条件和数据流,标准 profile 弹层内容以后优先沉到 `platform-entry` 独立组件,不在首页继续堆叠。 - 2026-06-10 追加:个人中心支付结果提示与支付确认遮罩统一抽到 `src/components/common/PlatformStatusDialog.tsx`,扫码面板统一抽到 `src/components/platform-entry/PlatformProfileQrScannerModal.tsx`;`RpgEntryHomeView` 只保留支付结果 kind 到 `success / loading / cancel / error` 的映射、确认遮罩开关和扫码结果写回,不再内联 profile 状态弹层壳层、二维码摄像头启动或 `BarcodeDetector` 轮询。后续 profile 侧同类“状态图标 + 标题正文 + 可选主动作”弹层优先复用 `PlatformStatusDialog`,扫码类弹层优先复用 `PlatformProfileQrScannerModal`。 - 2026-06-10 追加:`PlatformStatusDialog` 支持自定义图标、图标可访问标签以及动作按钮 surface / size / className 透传,用来承接玩法结果页里保留品牌视觉但语义仍是“状态结果弹层”的场景;大鱼吃小鱼结果页的发布失败弹层已迁移到这套组件,业务页不再保留 `UnifiedConfirmDialog + PlatformIconBadge` 的专用组合。 +- 2026-06-10 追加:`PlatformStatusDialog` 继续支持 header notice 布局、body content、close button、backdrop / Escape 关闭路径,用来承接“提示 / 规则阻断 / 作品不可用 / 泥点不足”这类带标题栏的状态 notice;平台入口的 `draftGenerationPointNotice`、`workNotFoundRecoveryDialog` 和 RPG 大编辑器里的 `EditorNoticeDialog` 已迁移到这套共享组件,不再各自维护 `UnifiedConfirmDialog` 壳层和关闭策略。 - 2026-06-09 追加:通用输入 Composer 的上传参考图、发送和移除参考图已迁移到 `PlatformIconButton`;图标上传仍使用 `asChild="label"` 保留 label + file input 语义,公共组件会自动写入隐藏文本,确保内嵌 file input 继承可访问名称。 - 2026-06-10 追加:creation-agent composer 的上传文档 / 上传参考图入口使用 `PlatformIconButton` 默认 `platformIcon`;工作台只保留动态 label、title、busy 状态和 picker 回调,发送按钮继续保留主题色动作布局。验证命令:`npm run test -- src/components/creation-agent/CreationAgentWorkspace.test.tsx src/components/common/PlatformIconButton.test.tsx`。 - 2026-06-10 追加:作品详情顶部返回 / 分享和封面轮播上一张 / 下一张入口使用 `PlatformIconButton variant="platformIcon"`;详情页保留原 `platform-work-detail__*` 局部 class 控制位置和尺寸,点赞、复制三态等专用动作暂不迁移。验证命令:`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformIconButton.test.tsx`。 diff --git a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md index 0b444064..8f561e00 100644 --- a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md +++ b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md @@ -241,6 +241,7 @@ 19.3.17. RPG 首页个人中心的账户充值弹层抽到 `src/components/platform-entry/PlatformProfileRechargeModal.tsx`;组件承接 Native 二维码生成、点数 / 会员 tab、套餐卡片、空态和错误重试,继续复用 `PlatformProfileModalShell` 与平台白底卡片 token,`RpgEntryHomeView` 不再内联 `useWechatNativeQrCode`、`RechargeProductCard` 和 `ProfileRechargeModal`。组件级验证新增 `src/components/platform-entry/PlatformProfileRechargeModal.test.tsx`,首页继续复用 `RpgEntryHomeView.recharge.test.tsx` 的充值入口与 Native 二维码断言。验证命令:`npm run test -- src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "renders point products and forwards buy action|shows empty state when the selected tab has no products|profile recharge modal shows native qr code on desktop web by default|create tab wallet chip opens recharge when recharge entry is enabled"`、`npm run typecheck`。 19.3.18. RPG 首页个人中心的泥点账单、每日任务和兑换码三类标准 profile 弹层分别抽到 `src/components/platform-entry/PlatformProfileWalletLedgerModal.tsx`、`src/components/platform-entry/PlatformProfileTaskCenterModal.tsx` 与 `src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx`;账单继续复用 `PlatformProfileSecondaryModalShell`,任务和兑换码继续复用 `PlatformProfileModalShell`,页面不再内联账单余额 badge、任务领取列表和兑换码输入提交实现。三者均新增组件级测试,并继续复用 `RpgEntryHomeView.recharge.test.tsx` 的真实入口断言。验证命令:`npm run test -- src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "renders ledger entries with shared balance presentation|retries from the shared error state|renders claimable tasks and forwards claim action|keeps incomplete tasks disabled|submits on button click and enter key|disables submit when the code is blank|opens wallet ledger modal from narrative coin card|profile daily task shortcut reflects task progress and claim updates|wallet ledger modal shows empty and error states|opens reward code modal from profile action on mobile|create tab wallet chip opens reward code when recharge entry is hidden"`、`npm run typecheck`。 19.3.19. RPG 首页个人中心的支付结果提示、支付确认遮罩与扫码面板继续向共享组件收口:支付结果 / 确认中弹层统一抽到 `src/components/common/PlatformStatusDialog.tsx`,扫码面板统一抽到 `src/components/platform-entry/PlatformProfileQrScannerModal.tsx`;`RpgEntryHomeView` 仅保留支付状态映射、扫码打开关闭和结果写回,不再内联 `RechargePaymentResultModal`、`RechargePaymentConfirmationMask`、`ProfileQrScannerModal`、`BarcodeDetector` 启动逻辑和 profile 弹层壳层参数。组件级验证新增 `src/components/common/PlatformStatusDialog.test.tsx` 与 `src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx`,首页继续复用 `RpgEntryHomeView.recharge.test.tsx` 的支付 / 扫码入口断言。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx`、`npm run test -- src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx`、`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx -t "profile recharge modal jumps to h5 payment on mobile web by default|profile recharge modal posts mini program payment request and reacts to success hash result|profile recharge modal releases submitting state and shows virtual payment failure detail|profile recharge modal eventually shows error text even when hashchange is not dispatched|profile recharge modal resumes virtual payment confirmation when pageshow returns with paid order|profile recharge modal blocks tab navigation while virtual payment confirmation is pending|profile scan action opens camera scanner instead of recharge panel"`、`npm run typecheck`。 +19.3.20. `PlatformStatusDialog` 继续扩展到 notice 场景:组件新增 header notice 布局、body content、close button、backdrop / Escape 关闭路径以及动作按钮样式透传;`PlatformEntryFlowShellImpl` 里的 `draftGenerationPointNotice` / `workNotFoundRecoveryDialog` 和 `RpgCreationEntityEditorShared.tsx` 里的 `EditorNoticeDialog` 已接入。创作入口泥点不足、作品不可用恢复和 RPG 大编辑器规则阻断提示不再各自维护 `UnifiedConfirmDialog` 壳层,只保留标题、正文、辅助提示和关闭回调。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx`、`npm run test -- src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx -t "bark battle form checks mud points before creating a draft|puzzle form checks mud points before creating a draft|match3d form checks mud points before creating a draft|direct missing public work detail shows unified dialog before returning home"`、`npm run test -- src/components/CustomWorldEntityEditorModal.test.tsx -t "可扮演角色至少保留一个背景章节时使用统一提示弹窗|场景连接缺少可连接目标时使用统一提示弹窗|场景保存缺少主角色时使用统一提示弹窗"`、`npm run typecheck`。 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/CustomWorldEntityEditorModal.test.tsx b/src/components/CustomWorldEntityEditorModal.test.tsx index be92b7ee..5a010883 100644 --- a/src/components/CustomWorldEntityEditorModal.test.tsx +++ b/src/components/CustomWorldEntityEditorModal.test.tsx @@ -2,6 +2,7 @@ import { cleanup, + fireEvent, render, screen, waitFor, @@ -857,11 +858,15 @@ test('可扮演角色至少保留一个背景章节时使用统一提示弹窗', await user.click(deleteChapterButtons()[0]!); const dialog = await screen.findByRole('dialog', { name: '提示' }); + const actionButton = within(dialog).getByRole('button', { name: '知道了' }); + expect(within(dialog).getByText('至少保留一个背景章节。')).toBeTruthy(); + expect(dialog.querySelector('.platform-icon-badge')).toBeTruthy(); + expect(actionButton.className).toContain('platform-button'); expect(alertSpy).not.toHaveBeenCalled(); expect(screen.getByText('编辑角色:沈砺')).toBeTruthy(); - await user.click(within(dialog).getByRole('button', { name: '知道了' })); + await user.click(actionButton); await waitFor(() => { expect(screen.queryByRole('dialog', { name: '提示' })).toBeNull(); }); @@ -1798,12 +1803,15 @@ test('场景连接缺少可连接目标时使用统一提示弹窗', async () => await user.click(screen.getByText('北')); const dialog = await screen.findByRole('dialog', { name: '提示' }); + const overlay = dialog.parentElement as HTMLElement; + expect( within(dialog).getByText('请先保留至少一个其他场景,才能配置连接关系。'), ).toBeTruthy(); + expect(overlay.className).toContain('rpg-editor-notice-dialog-overlay'); expect(alertSpy).not.toHaveBeenCalled(); - await user.click(within(dialog).getByRole('button', { name: '知道了' })); + fireEvent.click(overlay); await waitFor(() => { expect(screen.queryByRole('dialog', { name: '提示' })).toBeNull(); }); @@ -2000,6 +2008,11 @@ test('场景保存缺少主角色时使用统一提示弹窗', async () => { expect(alertSpy).not.toHaveBeenCalled(); expect(screen.getByText('编辑场景:空港栈桥')).toBeTruthy(); + fireEvent.keyDown(window, { key: 'Escape' }); + await waitFor(() => { + expect(screen.queryByRole('dialog', { name: '提示' })).toBeNull(); + }); + alertSpy.mockRestore(); }); diff --git a/src/components/common/PlatformStatusDialog.test.tsx b/src/components/common/PlatformStatusDialog.test.tsx index 2c380864..0533b494 100644 --- a/src/components/common/PlatformStatusDialog.test.tsx +++ b/src/components/common/PlatformStatusDialog.test.tsx @@ -96,3 +96,37 @@ test('supports custom badge icon label and action button styling', () => { expect(action.className).toContain('border-slate-950'); expect(action.className).toContain('bg-slate-950'); }); + +test('supports header notice layout with body content and close button', () => { + const onClose = vi.fn(); + + render( + + 本次需要 6 泥点,当前 5 泥点。 + , + ); + + const dialog = screen.getByRole('dialog', { name: '泥点不足' }); + + expect( + dialog.querySelector('.mt-4.text-xl.font-black.text-\\[var\\(--platform-text-strong\\)\\]'), + ).toBeNull(); + expect(screen.getByText('本次需要 6 泥点,当前 5 泥点。')).toBeTruthy(); + expect( + screen.getByText( + '当前表单不会丢失,关闭后可继续编辑或补足泥点再继续。', + ), + ).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: '关闭' })); + expect(onClose).toHaveBeenCalledTimes(1); +}); diff --git a/src/components/common/PlatformStatusDialog.tsx b/src/components/common/PlatformStatusDialog.tsx index 144b0072..af74622d 100644 --- a/src/components/common/PlatformStatusDialog.tsx +++ b/src/components/common/PlatformStatusDialog.tsx @@ -39,8 +39,15 @@ type PlatformStatusDialogProps = { status: PlatformStatusDialogStatus; title: string; description?: ReactNode; + children?: ReactNode; onClose: () => void; action?: PlatformStatusDialogAction; + showHeader?: boolean; + showBodyTitle?: boolean; + showCloseButton?: boolean; + closeOnBackdrop?: boolean; + closeOnEscape?: boolean; + closeLabel?: string; closeDisabled?: boolean; zIndexClassName?: string; overlayClassName?: string; @@ -101,8 +108,15 @@ export function PlatformStatusDialog({ status, title, description, + children, onClose, action, + showHeader = false, + showBodyTitle, + showCloseButton = false, + closeOnBackdrop = false, + closeOnEscape = false, + closeLabel, closeDisabled = false, zIndexClassName = 'z-[90]', overlayClassName = DEFAULT_OVERLAY_CLASS, @@ -114,24 +128,26 @@ export function PlatformStatusDialog({ }: PlatformStatusDialogProps) { const visualConfig = getStatusVisualConfig(status); const badgeIcon = icon ?? visualConfig.icon; + const shouldRenderBodyTitle = showBodyTitle ?? !showHeader; return ( + > -
- {title} -
+ {shouldRenderBodyTitle ? ( +
+ {title} +
+ ) : null} + {children ? ( +
+ {children} +
+ ) : null} {description ? (
{description} diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index e934ad04..443b3a8b 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -365,6 +365,7 @@ import type { CustomWorldProfile } from '../../types'; import { useAuthUi } from '../auth/AuthUiContext'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; +import { PlatformStatusDialog } from '../common/PlatformStatusDialog'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformSubpanel } from '../common/PlatformSubpanel'; import { PublishShareModal } from '../common/PublishShareModal'; @@ -16996,19 +16997,30 @@ export function PlatformEntryFlowShellImpl({ }} /> ) : null} - setDraftGenerationPointNotice(null)} - confirmLabel="知道了" + showHeader + showCloseButton closeOnBackdrop - size="sm" overlayClassName={`platform-theme ${platformThemeClass} !items-center`} panelClassName="platform-remap-surface rounded-[1.75rem]" + iconClassName={ + draftGenerationPointNotice?.title === '读取泥点余额失败' + ? undefined + : 'bg-amber-100/80 text-amber-600' + } + action={{ + label: '知道了', + onClick: () => setDraftGenerationPointNotice(null), + surface: 'platform', + }} > {draftGenerationPointNotice?.message} - + - {workNotFoundRecoveryDialog?.message} - + void; }) { return ( - - {message} - + /> ); }