Files
Genarrative/src/components/common/CreativeImageInputPanel.tsx
T
kdletters f7126f9556 接入移动端壳相机图片导入能力
新增 HostBridge file.captureImage 契约与 H5 facade

Expo 移动壳通过系统相机拍摄图片并复用图片导入校验

通用图片输入面板按宿主能力展示拍摄入口并转换为现有 File 回调

补充移动壳、HostBridge、图片面板测试和原生壳文档
2026-06-18 06:03:45 +08:00

762 lines
30 KiB
TypeScript

import {
Camera,
History,
ImagePlus,
Loader2,
Sparkles,
Trash2,
} from 'lucide-react';
import { type ReactNode, useEffect, useRef, useState } from 'react';
import {
canUseNativeHostCapability,
captureHostImageFile,
type HostFileImportImageResult,
importHostImageFile,
subscribeHostImageDrop,
} from '../../services/host-bridge/hostBridge';
import { puzzleReferenceImageDataUrlToFile } from '../../services/puzzleReferenceImage';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { PlatformActionButton } from './PlatformActionButton';
import { PlatformFieldLabel } from './PlatformFieldLabel';
import { PlatformIconBadge } from './PlatformIconBadge';
import { PlatformIconButton } from './PlatformIconButton';
import { PlatformImagePreviewModal } from './PlatformImagePreviewModal';
import { PlatformPillBadge } from './PlatformPillBadge';
import { PlatformPillSwitch } from './PlatformPillSwitch';
import { PlatformStatusMessage } from './PlatformStatusMessage';
import { PlatformTextField } from './PlatformTextField';
import { PlatformUploadPreviewCard } from './PlatformUploadPreviewCard';
import { UnifiedConfirmDialog } from './UnifiedConfirmDialog';
export type CreativeImageInputReferenceImage = {
id: string;
label: string;
imageSrc: string;
assetObjectId?: string | null;
};
export type CreativeImageInputPanelLabels = {
imageField: string;
uploadImage: string;
replaceImage: string;
emptyImageHint: string;
removeImage: string;
removeImageConfirmTitle: string;
removeImageConfirmBody: string;
promptReferenceUpload: string;
promptReferencePreviewAlt: string;
closePromptReferencePreview: string;
previewMainImage?: string;
closeMainImagePreview?: string;
history?: string;
};
export type CreativeImageInputPanelProps = {
className?: string;
fillHeight?: boolean;
disabled?: boolean;
isSubmitting?: boolean;
mainImageMode?: 'edit' | 'preview';
mainImageClickMode?: 'upload' | 'preview';
canUploadMainImage?: boolean;
canUseImageHistory?: boolean;
canRemoveMainImage?: boolean;
canToggleAiRedraw?: boolean;
canUploadPromptReferences?: boolean;
uploadedImageSrc: string;
uploadedImageAlt: string;
uploadedImageRefreshKey?: string | number | null;
mainImagePreviewZIndexClassName?: string;
mainImageMeta?: ReactNode;
mainImageInputId: string;
mainImageAccept?: string;
promptTextareaId: string;
prompt: string;
promptLabel: string;
promptAriaLabel?: string;
promptRows?: number;
aiRedraw: boolean;
promptReferenceImages: CreativeImageInputReferenceImage[];
promptReferenceLimit?: number;
imageLimitHint?: string | null;
imageModelPicker?: ReactNode;
error?: string | null;
inputError?: string | null;
showSubmitButton?: boolean;
submitLabel: string;
submitCostLabel?: string | null;
submitDisabled: boolean;
labels: CreativeImageInputPanelLabels;
onMainImageFileSelect: (file: File) => void;
onMainImageRemove: () => void;
onAiRedrawChange: (enabled: boolean) => void;
onPromptChange: (value: string) => void;
onPromptReferenceFilesSelect?: (files: File[]) => void;
onPromptReferenceRemove?: (referenceId: string) => void;
onHistoryClick?: () => void;
onSubmit: () => void;
};
const DEFAULT_IMAGE_ACCEPT = 'image/png,image/jpeg,image/webp';
const DEFAULT_PROMPT_REFERENCE_LIMIT = 5;
function hostImageImportResultToFile(result: HostFileImportImageResult) {
return puzzleReferenceImageDataUrlToFile(
`data:${result.mimeType};base64,${result.base64Data}`,
result.fileName,
);
}
export function CreativeImageInputPanel({
className = '',
fillHeight = true,
disabled = false,
isSubmitting = false,
mainImageMode = 'edit',
mainImageClickMode = 'preview',
canUploadMainImage = true,
canUseImageHistory = true,
canRemoveMainImage = true,
canToggleAiRedraw = true,
canUploadPromptReferences,
uploadedImageSrc,
uploadedImageAlt,
uploadedImageRefreshKey = null,
mainImagePreviewZIndexClassName = 'z-[82]',
mainImageMeta = null,
mainImageInputId,
mainImageAccept = DEFAULT_IMAGE_ACCEPT,
promptTextareaId,
prompt,
promptLabel,
promptAriaLabel,
promptRows = 2,
aiRedraw,
promptReferenceImages,
promptReferenceLimit = DEFAULT_PROMPT_REFERENCE_LIMIT,
imageLimitHint = null,
imageModelPicker = null,
error = null,
inputError = null,
showSubmitButton = true,
submitLabel,
submitCostLabel = null,
submitDisabled,
labels,
onMainImageFileSelect,
onMainImageRemove,
onAiRedrawChange,
onPromptChange,
onPromptReferenceFilesSelect,
onPromptReferenceRemove,
onHistoryClick,
onSubmit,
}: CreativeImageInputPanelProps) {
const mainImageCardRef = useRef<HTMLDivElement | null>(null);
const mainImageInputRef = useRef<HTMLInputElement | null>(null);
const promptReferenceInputRef = useRef<HTMLInputElement | null>(null);
const [previewReferenceImage, setPreviewReferenceImage] =
useState<CreativeImageInputReferenceImage | null>(null);
const [isMainImagePreviewOpen, setIsMainImagePreviewOpen] = useState(false);
const [isRemoveImageConfirmOpen, setIsRemoveImageConfirmOpen] =
useState(false);
const showPrompt =
mainImageMode === 'preview' || !uploadedImageSrc || aiRedraw;
const shouldShowPromptReferences =
canUploadPromptReferences ?? !uploadedImageSrc;
const promptReferenceUploadDisabled =
disabled || promptReferenceImages.length >= promptReferenceLimit;
const canEditMainImage = mainImageMode === 'edit';
const isMainImageUploadEnabled = canEditMainImage && canUploadMainImage;
const shouldShowHistoryButton =
canEditMainImage && canUseImageHistory && Boolean(onHistoryClick);
const shouldPreviewMainImage =
mainImageClickMode === 'preview' && Boolean(uploadedImageSrc);
const shouldShowMainImageUploadButton =
isMainImageUploadEnabled && shouldPreviewMainImage;
const canImportHostImage = canUseNativeHostCapability('file.importImage');
const canCaptureHostImage = canUseNativeHostCapability('file.captureImage');
const canReceiveHostImageDrop =
canUseNativeHostCapability('file.imageDropped');
const promptReferenceInputId = `${mainImageInputId}-prompt-reference`;
useEffect(() => {
if (uploadedImageSrc) {
setPreviewReferenceImage(null);
} else {
setIsMainImagePreviewOpen(false);
}
}, [uploadedImageSrc]);
useEffect(() => {
if (
previewReferenceImage &&
!promptReferenceImages.some(
(reference) => reference.id === previewReferenceImage.id,
)
) {
setPreviewReferenceImage(null);
}
}, [previewReferenceImage, promptReferenceImages]);
useEffect(() => {
if (!canReceiveHostImageDrop || disabled || !isMainImageUploadEnabled) {
return undefined;
}
return subscribeHostImageDrop((payload) => {
const card = mainImageCardRef.current;
const position = payload.position;
if (!card || !position) {
return;
}
const bounds = card.getBoundingClientRect();
const isInsideCard =
position.x >= bounds.left &&
position.x <= bounds.right &&
position.y >= bounds.top &&
position.y <= bounds.bottom;
if (!isInsideCard) {
return;
}
const topElement =
typeof document.elementFromPoint === 'function'
? document.elementFromPoint(position.x, position.y)
: null;
// 中文注释:桌面拖入是窗口级事件;只让坐标命中的最上层主图槽位消费,避免多个创作面板同时接收同一张图。
if (topElement && !card.contains(topElement)) {
return;
}
try {
onMainImageFileSelect(hostImageImportResultToFile(payload));
} catch {
// 中文注释:宿主已校验图片类型和体积;这里仅兜住浏览器 File 构造异常,保持当前表单状态。
}
});
}, [
canReceiveHostImageDrop,
disabled,
isMainImageUploadEnabled,
onMainImageFileSelect,
]);
const bodyClassName = fillHeight
? 'creative-image-input-panel__body puzzle-creation-form-body flex min-h-0 flex-1 flex-col overflow-hidden pr-0 lg:overflow-y-auto lg:pr-1'
: 'creative-image-input-panel__body puzzle-creation-form-body flex flex-none flex-col overflow-visible pr-0 lg:pr-1';
const sectionClassName = fillHeight
? 'creative-image-input-panel__section puzzle-creation-form-section flex min-h-0 flex-1 flex-col overflow-hidden lg:overflow-visible'
: 'creative-image-input-panel__section puzzle-creation-form-section flex flex-none flex-col overflow-visible';
const gridSizeClassName = fillHeight ? 'min-h-0 flex-1' : 'flex-none';
const imageFieldClassName = fillHeight
? 'creative-image-input-panel__image-field puzzle-image-field flex min-h-0 min-w-0 flex-1 flex-col'
: 'creative-image-input-panel__image-field puzzle-image-field flex min-w-0 flex-none flex-col';
const imageFrameClassName = fillHeight
? 'creative-image-input-panel__image-frame puzzle-image-card-frame flex min-h-0 flex-1 items-center justify-center'
: 'creative-image-input-panel__image-frame puzzle-image-card-frame flex flex-none items-center justify-center';
const imageCardClassName = fillHeight
? 'creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square h-full min-h-[14rem] max-h-full max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition sm:min-h-[18rem] lg:h-auto lg:w-full'
: 'creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square w-full min-h-[14rem] max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition sm:min-h-[18rem]';
const importHostImageAsFile = async () => {
const result = await importHostImageFile();
if (!result) {
return null;
}
return hostImageImportResultToFile(result);
};
const captureHostImageAsFile = async () => {
const result = await captureHostImageFile();
if (!result) {
return null;
}
return hostImageImportResultToFile(result);
};
const handleMainImageUploadClick = () => {
if (disabled || !isMainImageUploadEnabled) {
return;
}
if (!canImportHostImage) {
mainImageInputRef.current?.click();
return;
}
void (async () => {
try {
const file = await importHostImageAsFile();
if (file) {
onMainImageFileSelect(file);
}
} catch {
// 中文注释:宿主导入失败时不再弹浏览器文件框,避免权限失败后重复打扰用户。
}
})();
};
const handleMainImageCaptureClick = () => {
if (disabled || !isMainImageUploadEnabled || !canCaptureHostImage) {
return;
}
void (async () => {
try {
const file = await captureHostImageAsFile();
if (file) {
onMainImageFileSelect(file);
}
} catch {
// 中文注释:相机拍摄失败或用户取消时保持当前表单状态,不回落到相册或文件框。
}
})();
};
const handlePromptReferenceUploadClick = () => {
if (
promptReferenceUploadDisabled ||
!shouldShowPromptReferences ||
!onPromptReferenceFilesSelect
) {
return;
}
if (!canImportHostImage) {
promptReferenceInputRef.current?.click();
return;
}
void (async () => {
try {
const file = await importHostImageAsFile();
if (file) {
onPromptReferenceFilesSelect([file]);
}
} catch {
// 中文注释:宿主导入失败时保持当前表单状态,由外层错误通道继续承接后续重试。
}
})();
};
const handlePromptReferenceCaptureClick = () => {
if (
promptReferenceUploadDisabled ||
!shouldShowPromptReferences ||
!onPromptReferenceFilesSelect ||
!canCaptureHostImage
) {
return;
}
void (async () => {
try {
const file = await captureHostImageAsFile();
if (file) {
onPromptReferenceFilesSelect([file]);
}
} catch {
// 中文注释:相机拍摄失败不打断当前创作输入,用户可继续选择上传或重试拍摄。
}
})();
};
return (
<div
className={`creative-image-input-panel flex min-h-0 flex-col ${
fillHeight ? 'flex-1' : 'flex-none'
} ${className}`}
>
<div className={bodyClassName}>
<section className={sectionClassName}>
<div
className={`creative-image-input-panel__grid puzzle-creation-form-grid ${gridSizeClassName} gap-2.5 sm:gap-4 ${
showPrompt
? 'flex flex-col lg:grid lg:grid-cols-[minmax(15rem,0.9fr)_minmax(0,1.15fr)]'
: 'flex flex-col lg:grid lg:grid-cols-1'
}`}
>
<div
className={`${imageFieldClassName} ${
disabled ? 'opacity-55' : ''
}`}
>
<PlatformFieldLabel variant="form" className="shrink-0">
{labels.imageField}
</PlatformFieldLabel>
<div className={imageFrameClassName}>
<div ref={mainImageCardRef} className={imageCardClassName}>
{isMainImageUploadEnabled ? (
<input
ref={mainImageInputRef}
id={mainImageInputId}
type="file"
accept={mainImageAccept}
disabled={disabled}
aria-label={labels.uploadImage}
onChange={(event) => {
const file = event.currentTarget.files?.[0] ?? null;
event.currentTarget.value = '';
if (file) {
onMainImageFileSelect(file);
}
}}
className="sr-only"
/>
) : null}
{shouldPreviewMainImage ? (
<button
type="button"
className="absolute inset-0 z-[2] cursor-zoom-in"
aria-label={labels.previewMainImage ?? uploadedImageAlt}
title={labels.previewMainImage ?? uploadedImageAlt}
onClick={() => setIsMainImagePreviewOpen(true)}
/>
) : isMainImageUploadEnabled ? (
<button
type="button"
className={`absolute inset-0 z-0 border-0 bg-transparent p-0 ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}
disabled={disabled}
aria-label={
uploadedImageSrc
? labels.replaceImage
: labels.uploadImage
}
title={
uploadedImageSrc
? labels.replaceImage
: labels.uploadImage
}
onClick={handleMainImageUploadClick}
>
<span className="sr-only">
{uploadedImageSrc
? labels.replaceImage
: labels.uploadImage}
</span>
</button>
) : null}
{uploadedImageSrc ? (
<ResolvedAssetImage
src={uploadedImageSrc}
refreshKey={uploadedImageRefreshKey}
alt={uploadedImageAlt}
className="pointer-events-none absolute inset-0 h-full w-full object-cover"
/>
) : (
<span className="pointer-events-none flex h-full items-center justify-center bg-[radial-gradient(circle_at_50%_28%,rgba(255,255,255,0.9),transparent_38%),linear-gradient(135deg,rgba(255,255,255,0.96),rgba(255,241,229,0.86))]">
<PlatformIconBadge
icon={<ImagePlus className="h-6 w-6 sm:h-8 sm:w-8" />}
size="xl"
tone="soft"
className="border border-[var(--platform-subpanel-border)] bg-white/92 sm:h-20 sm:w-20"
/>
</span>
)}
<div className="pointer-events-none absolute inset-0 z-[1] bg-[linear-gradient(180deg,rgba(255,255,255,0.12)_0%,rgba(255,255,255,0.04)_42%,rgba(255,255,255,0.18)_100%)]" />
{shouldShowMainImageUploadButton ? (
<PlatformIconButton
variant="surfaceFloating"
label={labels.replaceImage}
title={labels.replaceImage}
disabled={disabled}
onClick={handleMainImageUploadClick}
icon={<ImagePlus className="h-4 w-4" />}
className="absolute bottom-3 right-3 z-10 h-10 w-10"
/>
) : null}
{shouldShowHistoryButton ? (
<PlatformIconButton
variant="surfaceFloating"
label={labels.history ?? '选择历史图片'}
title={labels.history ?? '选择历史图片'}
disabled={disabled}
onClick={onHistoryClick}
icon={<History className="h-3.5 w-3.5" />}
className="absolute right-3 top-3 z-10 gap-1.5 px-3 py-2 text-[11px] font-black"
>
<span>历史</span>
</PlatformIconButton>
) : null}
{isMainImageUploadEnabled && canCaptureHostImage ? (
<PlatformIconButton
variant="surfaceFloating"
label="拍摄图片"
title="拍摄图片"
disabled={disabled}
onClick={handleMainImageCaptureClick}
icon={<Camera className="h-3.5 w-3.5" />}
className={`absolute top-3 z-10 h-10 w-10 ${
shouldShowHistoryButton ? 'right-[4.75rem]' : 'right-3'
}`}
/>
) : null}
{canEditMainImage && uploadedImageSrc && canToggleAiRedraw ? (
<PlatformPillSwitch
label="AI重绘"
aria-label="AI重绘"
checked={aiRedraw}
disabled={disabled}
onChange={(event) =>
onAiRedrawChange(event.target.checked)
}
className="absolute bottom-3 left-3 z-10"
/>
) : null}
{canEditMainImage &&
uploadedImageSrc &&
canRemoveMainImage ? (
<PlatformIconButton
variant="surfaceFloating"
label={labels.removeImage}
title={labels.removeImage}
disabled={disabled}
onClick={() => setIsRemoveImageConfirmOpen(true)}
icon={<Trash2 className="h-4 w-4" />}
className="absolute left-3 top-3 z-10 h-10 w-10"
/>
) : isMainImageUploadEnabled && !uploadedImageSrc ? (
<button
type="button"
disabled={disabled}
onClick={handleMainImageUploadClick}
className={`absolute bottom-9 left-1/2 z-10 -translate-x-1/2 whitespace-nowrap border-0 bg-transparent p-0 text-center text-sm font-black text-[var(--platform-text-strong)] drop-shadow-[0_1px_0_rgba(255,255,255,0.82)] transition hover:text-[var(--platform-accent)] sm:bottom-10 ${
disabled
? 'cursor-not-allowed opacity-55'
: 'cursor-pointer'
}`}
>
{labels.emptyImageHint}
</button>
) : null}
</div>
</div>
{mainImageMeta ? (
<div className="mt-3 shrink-0">{mainImageMeta}</div>
) : null}
{imageLimitHint ? (
<div className="mt-2 shrink-0 text-center text-[11px] font-semibold text-[var(--platform-text-soft)]">
{imageLimitHint}
</div>
) : null}
</div>
{showPrompt ? (
<div className="block shrink-0 lg:min-h-0">
<label htmlFor={promptTextareaId} className="mb-2 block">
<PlatformFieldLabel variant="form" className="mb-0">
{promptLabel}
</PlatformFieldLabel>
</label>
<div className="relative">
<PlatformTextField
variant="textarea"
id={promptTextareaId}
value={prompt}
disabled={disabled}
rows={promptRows}
placeholder=""
onChange={(event) => onPromptChange(event.target.value)}
size="lg"
density="roomy"
className="h-[6rem] min-h-[6rem] rounded-[1.15rem] pb-14 font-normal placeholder:text-zinc-400 sm:h-[7.5rem] sm:min-h-[7.5rem] lg:h-[9.25rem] lg:min-h-[9.25rem]"
aria-label={promptAriaLabel ?? promptLabel}
/>
{imageModelPicker}
{shouldShowPromptReferences &&
onPromptReferenceFilesSelect ? (
<>
<input
ref={promptReferenceInputRef}
id={promptReferenceInputId}
type="file"
accept={mainImageAccept}
multiple
aria-label={labels.promptReferenceUpload}
disabled={promptReferenceUploadDisabled}
onChange={(event) => {
const files = Array.from(
event.currentTarget.files ?? [],
);
event.currentTarget.value = '';
if (files.length > 0) {
onPromptReferenceFilesSelect(files);
}
}}
className="sr-only"
/>
<div className="absolute bottom-3 right-3 z-10 flex gap-2">
{canCaptureHostImage ? (
<PlatformIconButton
variant="surfaceFloating"
label="拍摄图片"
title="拍摄图片"
disabled={promptReferenceUploadDisabled}
onClick={handlePromptReferenceCaptureClick}
icon={<Camera className="h-3.5 w-3.5" />}
className="h-8 w-8 border-[var(--platform-subpanel-border)] bg-white/96 hover:bg-[var(--platform-subpanel-fill)]"
/>
) : null}
{canImportHostImage ? (
<PlatformIconButton
variant="surfaceFloating"
label={labels.promptReferenceUpload}
title={labels.promptReferenceUpload}
disabled={promptReferenceUploadDisabled}
onClick={handlePromptReferenceUploadClick}
icon={<ImagePlus className="h-4 w-4" />}
className="h-8 w-8 border-[var(--platform-subpanel-border)] bg-white/96 hover:bg-[var(--platform-subpanel-fill)]"
/>
) : (
<PlatformIconButton
asChild="label"
htmlFor={promptReferenceInputId}
variant="surfaceFloating"
label={labels.promptReferenceUpload}
title={labels.promptReferenceUpload}
icon={<ImagePlus className="h-4 w-4" />}
className={`h-8 w-8 border-[var(--platform-subpanel-border)] bg-white/96 hover:bg-[var(--platform-subpanel-fill)] ${
promptReferenceUploadDisabled
? 'cursor-not-allowed opacity-55'
: 'cursor-pointer'
}`}
/>
)}
</div>
</>
) : null}
</div>
{shouldShowPromptReferences &&
promptReferenceImages.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-2">
{promptReferenceImages.map((reference) => (
<PlatformUploadPreviewCard
key={reference.id}
imageSrc={reference.imageSrc}
imageAlt=""
previewLabel={`预览参考图 ${reference.label}`}
removeLabel={`移除参考图 ${reference.label}`}
onPreview={() => setPreviewReferenceImage(reference)}
onRemove={
onPromptReferenceRemove
? () => onPromptReferenceRemove(reference.id)
: undefined
}
disabled={disabled}
resolveAsset
className="h-12 w-12 rounded-[0.75rem] bg-white/90 shadow-sm"
previewButtonProps={{ title: reference.label }}
removeButtonProps={{
title: '移除参考图',
className:
'right-0.5 top-0.5 bg-white/94 text-[var(--platform-text-strong)] shadow-sm hover:bg-white hover:text-[var(--platform-accent)]',
}}
/>
))}
</div>
) : null}
</div>
) : null}
</div>
<div className="mt-2 shrink-0 space-y-3">
{inputError ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="md"
className="rounded-2xl"
>
{inputError}
</PlatformStatusMessage>
) : null}
{error ? (
<PlatformStatusMessage
tone="error"
surface="profile"
size="md"
className="rounded-2xl"
>
{error}
</PlatformStatusMessage>
) : null}
</div>
</section>
</div>
{showSubmitButton ? (
<div className="mt-2 flex shrink-0 justify-center pb-[max(0.25rem,env(safe-area-inset-bottom))] sm:mt-3">
<PlatformActionButton
disabled={disabled || submitDisabled}
onClick={onSubmit}
className={`min-h-10 px-4 sm:min-h-11 sm:px-5 ${
submitDisabled ? 'cursor-not-allowed opacity-55' : ''
}`}
>
<span className="inline-flex flex-wrap items-center justify-center gap-1.5 sm:gap-2">
{isSubmitting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
<Sparkles className="h-4 w-4" />
<span>{submitLabel}</span>
{submitCostLabel ? (
<PlatformPillBadge
tone="lightOverlay"
size="xs"
className="px-2 font-bold"
>
{submitCostLabel}
</PlatformPillBadge>
) : null}
</span>
</PlatformActionButton>
</div>
) : null}
<PlatformImagePreviewModal
open={Boolean(previewReferenceImage)}
title={previewReferenceImage?.label ?? labels.promptReferencePreviewAlt}
imageSrc={previewReferenceImage?.imageSrc ?? null}
imageAlt={labels.promptReferencePreviewAlt}
closeLabel={labels.closePromptReferencePreview}
zIndexClassName="z-[80]"
onClose={() => setPreviewReferenceImage(null)}
/>
<PlatformImagePreviewModal
open={isMainImagePreviewOpen && Boolean(uploadedImageSrc)}
title={labels.previewMainImage ?? uploadedImageAlt}
imageSrc={uploadedImageSrc}
imageAlt={uploadedImageAlt}
refreshKey={uploadedImageRefreshKey}
closeLabel={
labels.closeMainImagePreview ?? labels.closePromptReferencePreview
}
zIndexClassName={mainImagePreviewZIndexClassName}
onClose={() => setIsMainImagePreviewOpen(false)}
/>
<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>
);
}
export default CreativeImageInputPanel;