From ef6a2b720034a18a4371607acc611e90226bb3d1 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 11 Jun 2026 05:26:14 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E6=94=B6=E5=8F=A3=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=9D=A2=E6=9D=BF=E5=B7=A5=E5=85=B7=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 CreativeImageInputPanel 的预览与删除确认弹窗并回 UnifiedModal 体系 补充图片面板预览与遮罩关闭回归测试 更新 PlatformUiKit 收口计划与共享决策记录 --- .hermes/shared-memory/decision-log.md | 1 + ...】PlatformUiKit弹窗组件收口计划-2026-06-08.md | 1 + .../common/CreativeImageInputPanel.test.tsx | 95 ++++++++++ .../common/CreativeImageInputPanel.tsx | 179 +++++++----------- 4 files changed, 165 insertions(+), 111 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 48d345bc..2dc948c2 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -63,6 +63,7 @@ - 2026-06-11 追加:`PlatformDarkModalFooter` 继续从标准双按钮 footer 扩到 detail / confirm 收尾;`NpcModals.tsx` 的交易详情 footer 和 `MapModal.tsx` 的场景切换确认 footer 已改成复用同一个 dark footer frame,即使只有单个“关闭”按钮也不再手写 `flex justify-end`。这条抽象继续只覆盖 dark / pixel modal 里的底部分隔线与常规动作区排布,不向白底 profile 弹窗 footer、sticky 工作台 footer 或运行态 HUD 工具条扩张。 - 2026-06-11 追加:`PlatformFilterToolbar.tsx` 作为薄结构组件收口 RPG 首页分类工具条;组件只承接“筛选按钮 + tabs + 排序按钮”的排布与 `mobile / desktop` 两种布局差异,不持有筛选状态、空态或排序逻辑。后续只有在同构壳层真的复现时才继续往 `common` 扩覆盖面;如果只是单页内局部重复、接口会越抽越胖,就优先退回文件内 helper。 - 2026-06-11 追加:`SquareImageCropModal.tsx` 的白底弹窗壳层改为复用 `UnifiedModal.tsx`,同时给 `UnifiedModal` 薄补 `titleId` 与 `closeIcon` 透传,让裁剪弹窗继续保留自定义 close icon、无 backdrop / Escape 关闭和两列 footer,而不把 `PlatformProfileModalShell` 这类带页面语义的壳层倒灌回 `common/`。这条规则适用于 `common` 级工具弹窗:先看 `UnifiedModal` 能不能承接,再决定是否需要新的薄壳。 +- 2026-06-11 追加:`CreativeImageInputPanel.tsx` 里参考图预览、主图预览和移除图片确认都继续并回 `UnifiedModal` 体系:两个预览弹窗直接复用 `UnifiedModal`,删除确认直接复用 `UnifiedConfirmDialog`,不再在图片面板里手写三段 `platform-modal-backdrop + platform-modal-shell`。当前没有新增 `PlatformImagePreviewModal`,因为这批差异还只在尺寸与文案层,继续组合已有 modal 原语的 leverage 更高。 - 2026-06-11 追加:`PlatformProfileModalShell` 继续补齐标准 footer 插槽,直接透传 `UnifiedModal.footer` 与 `footerClassName`;`RpgEntryHomeView.tsx` 的昵称修改弹窗已改成标准 profile footer,不再把双按钮动作区手写在 body 末尾。后续个人中心里同类“表单内容 + 底部双按钮”弹窗优先走壳层 footer 接法。 - 2026-06-11 追加:`PlatformProfileModalShell` 的标准 footer 接法继续扩展到单 CTA 表单收尾;`PlatformProfileRewardCodeRedeemModal.tsx` 的兑换按钮已迁到壳层 footer,body 只保留输入和反馈消息。`PlatformAsyncStatePanel` 同日继续扩展到 `PlatformAssetPickerGrid`、`VisualNovelSavePanel.tsx` 与 `AccountModal.tsx` 的账号安全三个子区块;其中公共素材网格继续把 `error` banner 放在状态壳外层,保持错误提示可与加载态或内容并存的原语义。 - 2026-06-11 追加:按钮层继续补齐轻量漏网项。`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留透明背景和原 chip 高度;`RpgEntryCharacterSelectView.tsx` 的两处“返回”按钮统一沉到局部 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`。同日 `GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton`,`CustomWorldGenerationView.tsx` 与 `BarkBattleGeneratingView.tsx` 已开始复用这套暖色生成页返回入口骨架;后续同类轻量返回按钮与 chip 删除按钮优先继续沿共享按钮 + 薄包装的方向推进。 diff --git a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md index 103bf35d..da5dd7a5 100644 --- a/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md +++ b/docs/technical/【前端架构】PlatformUiKit弹窗组件收口计划-2026-06-08.md @@ -273,6 +273,7 @@ 19.3.47. `PlatformDarkModalFooter` 继续从标准双按钮 footer 扩展到 detail / confirm 收尾:`NpcModals.tsx` 的交易详情单按钮 footer 与 `MapModal.tsx` 的场景切换确认 footer 已接入共享 dark footer frame,分别保留“关闭”单 CTA 和“取消 / 确认前往”双 CTA 的业务语义、按钮 tone 与禁用态。后续 dark / pixel modal 里若只是标准底部分隔线 + 常规动作区排布,优先直接复用 `PlatformDarkModalFooter`,即使只有单个按钮也不再手写 `flex justify-end`;但像 `SquareImageCropModal.tsx` 这类白底弹窗 footer、sticky 工作台 footer 和运行态 HUD 工具条继续留在各自语义壳层,不强行混到 dark footer 抽象里。验证命令:`npx vitest run src/components/NpcModals.test.tsx src/components/MapModal.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 19.3.48. `RpgEntryHomeView.tsx` 里的分类筛选工具条继续从页面内重复 JSX 收口到 `src/components/common/PlatformFilterToolbar.tsx`;该 Module 只承接“筛选按钮 + 横向 tabs + 排序按钮”的结构排布,暴露 `mobile / desktop` 两种 layout 以覆盖移动端 divider + 独立排序行和桌面端同排布局差异,但不持有分类列表、筛选状态、空态或排序逻辑。当前 RPG 首页分类区已接入,后续若其它白底列表页也出现同构的筛选壳层,可直接复用这套薄结构组件;若场景只是在单页内局部重复、接口会为了兼容业务差异不断膨胀,则优先退回文件内 helper,不把 `common` 扩成假的“万能筛选条”。验证命令:`npx vitest run src/components/common/PlatformFilterToolbar.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 19.3.49. `SquareImageCropModal.tsx` 的白底 modal 壳层与 footer 已收口到 `src/components/common/UnifiedModal.tsx`;`UnifiedModal` 为此只薄补了 `titleId` 与 `closeIcon` 透传,继续由调用方决定 `closeOnBackdrop`、`closeOnEscape`、`portal`、header/footer 样式和按钮内容,不额外掺入 profile 业务语义,也不让 `common/` 反向依赖 `platform-entry/`。`SquareImageCropModal.tsx` 继续保留裁剪拖拽、pointer capture、保存禁用态与两列等宽 footer 行为,只把 header / body / footer 外壳交给共享 modal 承接。后续 `common` 级白底工具弹窗若只是标准标题栏 + 内容区 + footer 按钮排布,优先先看 `UnifiedModal` 是否够用,再决定是否需要新的薄壳;不要为了一个弹窗把 `PlatformProfileModalShell` 之类带页面语义的壳层倒灌回 `common`。验证命令:`npx vitest run src/components/common/SquareImageCropModal.test.tsx src/components/common/UnifiedModal.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 +19.3.50. `CreativeImageInputPanel.tsx` 里内嵌的 white tool modal 继续并回 `UnifiedModal` 体系:参考图预览与主图预览都改成直接复用 `src/components/common/UnifiedModal.tsx`,继续保留各自 `max-w` / `max-h` 节奏、点击遮罩关闭与紧凑 header;移除图片确认改成复用 `src/components/common/UnifiedConfirmDialog.tsx`,不再在 panel 内手写 `platform-modal-backdrop + platform-modal-shell + 两列按钮`。这次没有新增 `PlatformImagePreviewModal`,因为当前预览弹窗差异还只在尺寸和文案层,继续直接组合 `UnifiedModal` 更深、更稳。后续 `common` 级图片面板若出现同类“预览大图 + 单标题栏 + 关闭按钮”弹窗,优先先复用 `UnifiedModal` 并把尺寸/文案留在调用方;只有当至少两到三个调用点开始重复同一套 preview body/header adapter 时,再考虑补新的薄壳。验证命令:`npx vitest run src/components/common/CreativeImageInputPanel.test.tsx src/components/common/UnifiedModal.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 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/common/CreativeImageInputPanel.test.tsx b/src/components/common/CreativeImageInputPanel.test.tsx index effdd666..af811a64 100644 --- a/src/components/common/CreativeImageInputPanel.test.tsx +++ b/src/components/common/CreativeImageInputPanel.test.tsx @@ -106,6 +106,7 @@ test('creative image input panel handles reference uploads and preview', () => { expect.stringContaining('ref-1'), ); fireEvent.click(screen.getByRole('button', { name: '关闭参考图预览' })); + expect(screen.queryByRole('dialog', { name: '参考图 1' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '移除参考图 参考图 1' })); expect(onPromptReferenceRemove).toHaveBeenCalledWith('ref-1'); @@ -255,6 +256,54 @@ test('creative image input panel confirms before removing uploaded image', () => expect(onMainImageRemove).toHaveBeenCalledTimes(1); }); +test('creative image input panel closes reference preview on backdrop click', () => { + render( + {}} + onMainImageRemove={() => {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onPromptReferenceFilesSelect={() => {}} + onPromptReferenceRemove={() => {}} + onSubmit={() => {}} + />, + ); + + fireEvent.click(screen.getByRole('button', { name: '预览参考图 参考图 1' })); + const dialog = screen.getByRole('dialog', { name: '参考图 1' }); + fireEvent.click(dialog.parentElement as HTMLElement); + expect(screen.queryByRole('dialog', { name: '参考图 1' })).toBeNull(); +}); + test('creative image input panel supports a preview-only main image mode', () => { const onSubmit = vi.fn(); @@ -356,6 +405,9 @@ test('creative image input panel can preview the main image and keep upload on a 2, ); fireEvent.click(screen.getByRole('button', { name: '关闭关卡图片预览' })); + expect( + screen.queryByRole('dialog', { name: '查看关卡图片' }), + ).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '更换参考图' })); expect(inputClickSpy).toHaveBeenCalledTimes(1); @@ -376,6 +428,49 @@ test('creative image input panel can preview the main image and keep upload on a } }); +test('creative image input panel closes main image preview on backdrop click', () => { + render( + {}} + onMainImageRemove={() => {}} + onAiRedrawChange={() => {}} + onPromptChange={() => {}} + onSubmit={() => {}} + />, + ); + + fireEvent.click(screen.getByRole('button', { name: '查看关卡图片' })); + const dialog = screen.getByRole('dialog', { name: '查看关卡图片' }); + fireEvent.click(dialog.parentElement as HTMLElement); + expect(screen.queryByRole('dialog', { name: '查看关卡图片' })).toBeNull(); +}); + test('creative image input panel can hide upload and history controls independently', () => { render( ) : null} - {previewReferenceImage ? ( -
setPreviewReferenceImage(null)} - > -
event.stopPropagation()} - > -
-
- {previewReferenceImage.label} -
- setPreviewReferenceImage(null)} - className="shrink-0" - /> -
-
- -
+ setPreviewReferenceImage(null)} + closeLabel={labels.closePromptReferencePreview} + closeVariant="profileCompact" + size="lg" + zIndexClassName="z-[80]" + overlayClassName="px-4 py-6" + panelClassName="platform-remap-surface rounded-[1.35rem] p-3 shadow-[0_24px_70px_rgba(15,23,42,0.22)]" + headerClassName="mb-3 items-center border-b-0 px-1 py-0" + titleClassName="text-sm font-black" + bodyClassName="px-0 py-0" + > + {previewReferenceImage ? ( +
+
-
- ) : null} + ) : null} + - {isMainImagePreviewOpen && uploadedImageSrc ? ( -
setIsMainImagePreviewOpen(false)} - > -
event.stopPropagation()} - > -
-
- {labels.previewMainImage ?? uploadedImageAlt} -
- setIsMainImagePreviewOpen(false)} - className="shrink-0" - /> -
-
- -
+ setIsMainImagePreviewOpen(false)} + closeLabel={ + labels.closeMainImagePreview ?? labels.closePromptReferencePreview + } + closeVariant="profileCompact" + size="xl" + zIndexClassName="z-[82]" + overlayClassName="px-4 py-6" + panelClassName="platform-remap-surface rounded-[1.35rem] p-3 shadow-[0_24px_70px_rgba(15,23,42,0.22)]" + headerClassName="mb-3 items-center border-b-0 px-1 py-0" + titleClassName="text-sm font-black" + bodyClassName="px-0 py-0" + > + {uploadedImageSrc ? ( +
+
-
- ) : null} + ) : null} + - {isRemoveImageConfirmOpen ? ( -
-
-
- {labels.removeImageConfirmTitle} -
-
- {labels.removeImageConfirmBody} -
-
- setIsRemoveImageConfirmOpen(false)} - > - 取消 - - { - onMainImageRemove(); - setIsRemoveImageConfirmOpen(false); - }} - > - 移除 - -
-
-
- ) : null} + setIsRemoveImageConfirmOpen(false)} + confirmLabel="移除" + cancelLabel="取消" + showCancel + onConfirm={() => { + onMainImageRemove(); + setIsRemoveImageConfirmOpen(false); + }} + size="sm" + zIndexClassName="z-[80]" + overlayClassName="px-4 py-6" + panelClassName="platform-remap-surface max-w-xs rounded-[1.35rem] shadow-[0_24px_70px_rgba(15,23,42,0.22)]" + />
); }