继续收口图片面板工具弹窗

将 CreativeImageInputPanel 的预览与删除确认弹窗并回 UnifiedModal 体系
补充图片面板预览与遮罩关闭回归测试
更新 PlatformUiKit 收口计划与共享决策记录
This commit is contained in:
2026-06-11 05:26:14 +08:00
parent ebd958b5a0
commit ef6a2b7200
4 changed files with 165 additions and 111 deletions

View File

@@ -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(
<CreativeImageInputPanel
uploadedImageSrc=""
uploadedImageAlt="拼图图片"
mainImageInputId="image-upload-input"
promptTextareaId="image-prompt-input"
prompt="旧街灯牌下的猫。"
promptLabel="画面描述"
aiRedraw
promptReferenceImages={[
{
id: 'ref-1',
label: '参考图 1',
imageSrc: 'data:image/png;base64,ref-1',
},
]}
imageModelPicker={null}
submitLabel="生成"
submitDisabled={false}
labels={{
imageField: '拼图画面',
uploadImage: '上传拼图图片',
replaceImage: '更换拼图图片',
emptyImageHint: '上传图片/填写画面描述',
removeImage: '移除拼图图片',
removeImageConfirmTitle: '移除拼图图片?',
removeImageConfirmBody: '移除后需要重新上传图片。',
promptReferenceUpload: '上传参考图',
promptReferencePreviewAlt: '参考图预览',
closePromptReferencePreview: '关闭参考图预览',
}}
onMainImageFileSelect={() => {}}
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(
<CreativeImageInputPanel
mainImageClickMode="preview"
uploadedImageSrc="/generated-puzzle-assets/session/level/image.png"
uploadedImageAlt="拼图关卡图"
mainImageInputId="level-image-upload-input"
promptTextareaId="level-prompt-input"
prompt="旧街灯牌下的猫。"
promptLabel="画面描述"
aiRedraw
promptReferenceImages={[]}
imageModelPicker={null}
submitLabel="重新生成画面"
submitDisabled={false}
labels={{
imageField: '画面图',
uploadImage: '上传参考图',
replaceImage: '更换参考图',
emptyImageHint: '上传图片/填写画面描述',
removeImage: '移除参考图',
removeImageConfirmTitle: '移除参考图?',
removeImageConfirmBody: '移除后可重新上传或选择历史图片。',
promptReferenceUpload: '上传参考图',
promptReferencePreviewAlt: '参考图预览',
closePromptReferencePreview: '关闭参考图预览',
previewMainImage: '查看关卡图片',
closeMainImagePreview: '关闭关卡图片预览',
}}
onMainImageFileSelect={() => {}}
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(
<CreativeImageInputPanel

View File

@@ -6,12 +6,13 @@ import { PlatformActionButton } from './PlatformActionButton';
import { PlatformFieldLabel } from './PlatformFieldLabel';
import { PlatformIconBadge } from './PlatformIconBadge';
import { PlatformIconButton } from './PlatformIconButton';
import { PlatformModalCloseButton } from './PlatformModalCloseButton';
import { PlatformPillBadge } from './PlatformPillBadge';
import { PlatformPillSwitch } from './PlatformPillSwitch';
import { PlatformStatusMessage } from './PlatformStatusMessage';
import { PlatformTextField } from './PlatformTextField';
import { PlatformUploadPreviewCard } from './PlatformUploadPreviewCard';
import { UnifiedConfirmDialog } from './UnifiedConfirmDialog';
import { UnifiedModal } from './UnifiedModal';
export type CreativeImageInputReferenceImage = {
id: string;
@@ -489,120 +490,76 @@ export function CreativeImageInputPanel({
</div>
) : null}
{previewReferenceImage ? (
<div
className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6"
onClick={() => setPreviewReferenceImage(null)}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="creative-image-reference-preview-title"
className="platform-modal-shell platform-remap-surface w-full max-w-2xl rounded-[1.35rem] p-3 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center justify-between gap-3 px-1">
<div
id="creative-image-reference-preview-title"
className="min-w-0 truncate text-sm font-black text-[var(--platform-text-strong)]"
>
{previewReferenceImage.label}
</div>
<PlatformModalCloseButton
label={labels.closePromptReferencePreview}
variant="profileCompact"
onClick={() => setPreviewReferenceImage(null)}
className="shrink-0"
/>
</div>
<div className="max-h-[72vh] overflow-hidden rounded-[1rem] bg-black/5">
<ResolvedAssetImage
src={previewReferenceImage.imageSrc}
alt={labels.promptReferencePreviewAlt}
className="h-full max-h-[72vh] w-full object-contain"
/>
</div>
<UnifiedModal
open={Boolean(previewReferenceImage)}
title={previewReferenceImage?.label ?? labels.promptReferencePreviewAlt}
onClose={() => 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 ? (
<div className="max-h-[72vh] overflow-hidden rounded-[1rem] bg-black/5">
<ResolvedAssetImage
src={previewReferenceImage.imageSrc}
alt={labels.promptReferencePreviewAlt}
className="h-full max-h-[72vh] w-full object-contain"
/>
</div>
</div>
) : null}
) : null}
</UnifiedModal>
{isMainImagePreviewOpen && uploadedImageSrc ? (
<div
className="platform-modal-backdrop fixed inset-0 z-[82] flex items-center justify-center px-4 py-6"
onClick={() => setIsMainImagePreviewOpen(false)}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby="creative-image-main-preview-title"
className="platform-modal-shell platform-remap-surface w-full max-w-4xl rounded-[1.35rem] p-3 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
onClick={(event) => event.stopPropagation()}
>
<div className="mb-3 flex items-center justify-between gap-3 px-1">
<div
id="creative-image-main-preview-title"
className="min-w-0 truncate text-sm font-black text-[var(--platform-text-strong)]"
>
{labels.previewMainImage ?? uploadedImageAlt}
</div>
<PlatformModalCloseButton
label={
labels.closeMainImagePreview ??
labels.closePromptReferencePreview
}
variant="profileCompact"
onClick={() => setIsMainImagePreviewOpen(false)}
className="shrink-0"
/>
</div>
<div className="max-h-[82vh] overflow-hidden rounded-[1rem] bg-black/5">
<ResolvedAssetImage
src={uploadedImageSrc}
refreshKey={uploadedImageRefreshKey}
alt={uploadedImageAlt}
className="h-full max-h-[82vh] w-full object-contain"
/>
</div>
<UnifiedModal
open={isMainImagePreviewOpen && Boolean(uploadedImageSrc)}
title={labels.previewMainImage ?? uploadedImageAlt}
onClose={() => 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 ? (
<div className="max-h-[82vh] overflow-hidden rounded-[1rem] bg-black/5">
<ResolvedAssetImage
src={uploadedImageSrc}
refreshKey={uploadedImageRefreshKey}
alt={uploadedImageAlt}
className="h-full max-h-[82vh] w-full object-contain"
/>
</div>
</div>
) : null}
) : null}
</UnifiedModal>
{isRemoveImageConfirmOpen ? (
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
<div
role="dialog"
aria-modal="true"
aria-labelledby="creative-image-remove-confirm-title"
className="platform-modal-shell platform-remap-surface w-full max-w-xs rounded-[1.35rem] p-5 shadow-[0_24px_70px_rgba(15,23,42,0.22)]"
>
<div
id="creative-image-remove-confirm-title"
className="text-base font-black text-[var(--platform-text-strong)]"
>
{labels.removeImageConfirmTitle}
</div>
<div className="mt-2 text-sm leading-6 text-[var(--platform-text-base)]">
{labels.removeImageConfirmBody}
</div>
<div className="mt-5 grid grid-cols-2 gap-3">
<PlatformActionButton
tone="secondary"
onClick={() => setIsRemoveImageConfirmOpen(false)}
>
</PlatformActionButton>
<PlatformActionButton
onClick={() => {
onMainImageRemove();
setIsRemoveImageConfirmOpen(false);
}}
>
</PlatformActionButton>
</div>
</div>
</div>
) : null}
<UnifiedConfirmDialog
open={isRemoveImageConfirmOpen}
title={labels.removeImageConfirmTitle}
description={labels.removeImageConfirmBody}
onClose={() => 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)]"
/>
</div>
);
}