refactor: unify ai wash upload preview

This commit is contained in:
Your Name 2026-06-01 10:32:59 +08:00
parent afc41c5a42
commit b1dc367bcf

View File

@ -988,69 +988,6 @@ export function AiWashWorkspace({
</div>
</div>
<Button
className={cn(
"h-12 w-full rounded-[20px] px-4 text-[15px] font-semibold",
PRIMARY_ACTION_BUTTON_CLASS
)}
disabled={
isSubmitting ||
!compareSourceUrl ||
enabledPresets.length === 0 ||
(shouldShowUserModelSelector && !selectedUserModel)
}
onClick={handleSubmit}
type="button"
>
{isSubmitting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
)}
{isSubmitting
? "正在洗图"
: totalEstimatedCost > 0
? `开始 AI洗图 · ${totalEstimatedCost} 积分`
: "开始 AI洗图"}
</Button>
</div>
<div className={cn(SUBTLE_PANEL_CLASS, "space-y-3 p-3.5")}>
<div className="flex items-center justify-between">
<div className="text-[15px] font-semibold text-[#111111]">
上传底图
</div>
<Button
className={cn(
"h-9 rounded-full px-3 text-xs font-medium",
SECONDARY_ACTION_BUTTON_CLASS
)}
onClick={() => onRequestAddImage?.()}
size="sm"
type="button"
variant="outline"
>
<Upload className="mr-1.5 h-3.5 w-3.5" />
{compareSourceUrl ? "更换图片" : "选择图片"}
</Button>
</div>
{compareSourceUrl ? (
<div className="overflow-hidden rounded-[18px] border border-[#ececec] bg-white">
<img
alt="AI洗图底图"
className="aspect-[16/10] w-full object-cover"
src={getDisplayImageUrl(compareSourceUrl, 720)}
/>
</div>
) : (
<button
className="flex aspect-[16/10] w-full items-center justify-center rounded-[18px] border border-dashed border-[#d9d9d9] bg-white text-center text-sm text-[#8a8a8a] transition hover:border-[#111111] hover:text-[#111111]"
onClick={() => onRequestAddImage?.()}
type="button"
>
先上传一张底图
</button>
)}
</div>
<div className={cn(SUBTLE_PANEL_CLASS, "space-y-2 p-3.5")}>
@ -1247,49 +1184,27 @@ export function AiWashWorkspace({
</div>
</aside>
<main className="flex min-w-0 flex-1 flex-col overflow-hidden px-6 pb-6 pt-6">
<main className="flex min-w-0 flex-1 flex-col gap-3 overflow-hidden px-6 pb-6 pt-6">
<section
className={cn(
PANEL_CLASS,
"flex min-h-0 flex-1 flex-col overflow-hidden p-4"
"flex min-h-0 flex-1 flex-col overflow-hidden p-3"
)}
>
<div className="mb-4 flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<div className="text-[24px] font-semibold leading-tight text-[#111111]">
{selectedPreviewResult?.title || "AI洗图结果预览"}
</div>
<span className="rounded-full border border-[#ebebeb] bg-[#fafafa] px-3 py-1 text-xs text-[#666666]">
{getAiWashStatusLabel(selectedPreviewResult?.status)}
</span>
</div>
<div className="mt-1 max-w-3xl text-sm leading-6 text-[#666666]">
{selectedPreviewResult?.description ||
"左侧选择渲染目标后,所有洗图结果都会共用这里的大尺寸预览区。"}
</div>
</div>
<div className="flex flex-wrap items-center gap-2 text-xs text-[#666666]">
{selectedPreviewResult?.modelId ? (
<span className="rounded-full border border-[#ebebeb] bg-[#fafafa] px-3 py-1.5">
{selectedPreviewResult.modelId}
</span>
) : null}
{selectedPreviewResult?.createdAt ? (
<span className="rounded-full border border-[#ebebeb] bg-[#fafafa] px-3 py-1.5">
{new Date(selectedPreviewResult.createdAt).toLocaleTimeString(
"zh-CN",
{
hour: "2-digit",
minute: "2-digit",
}
)}
</span>
) : null}
</div>
</div>
<div className="relative min-h-[560px] flex-1 overflow-hidden rounded-[28px] border border-[#e9e9e9] bg-[radial-gradient(circle_at_50%_0%,#ffffff_0%,#f8f8f8_52%,#f2f2f2_100%)]">
<div className="relative min-h-0 flex-1 overflow-hidden rounded-[28px] border border-[#e9e9e9] bg-[radial-gradient(circle_at_50%_0%,#ffffff_0%,#f8f8f8_52%,#f2f2f2_100%)]">
{compareSourceUrl ? (
<button
className={cn(
"absolute right-4 top-4 z-20 inline-flex h-10 items-center rounded-full px-4 text-sm font-medium",
SECONDARY_ACTION_BUTTON_CLASS
)}
onClick={() => onRequestAddImage?.()}
type="button"
>
<Upload className="mr-2 h-4 w-4" />
更换图片
</button>
) : null}
{selectedPreviewResult ? (
previewHasImage ? (
<>
@ -1359,54 +1274,96 @@ export function AiWashWorkspace({
</>
) : null}
</>
) : compareSourceUrl ? (
<>
<img
alt="AI洗图底图"
className="absolute inset-0 h-full w-full object-contain"
src={getDisplayImageUrl(compareSourceUrl, 1800)}
/>
<div className="pointer-events-none absolute left-4 top-4 rounded-full bg-black/65 px-3 py-1.5 text-[11px] text-white">
原图
</div>
{(selectedPreviewResult.status === "running" ||
selectedPreviewResult.status === "submitting" ||
selectedPreviewResult.status === "failed") ? (
<div className="absolute inset-0 flex items-center justify-center bg-white/58 px-6 text-center backdrop-blur-[1px]">
<div className="max-w-sm rounded-[24px] border border-[#ececec] bg-white/92 px-6 py-5 shadow-[0_18px_40px_rgba(0,0,0,0.08)]">
{selectedPreviewResult.status === "running" ||
selectedPreviewResult.status === "submitting" ? (
<>
<Loader2 className="mx-auto mb-3 h-8 w-8 animate-spin text-[#666666]" />
<div className="text-base font-medium text-[#111111]">
{selectedPreviewResult.status === "submitting"
? "正在提交模型任务"
: "模型正在生成结果"}
</div>
<div className="mt-2 text-sm leading-6 text-[#777777]">
结果完成后会在同一区域切换为可拖动对比图。
</div>
</>
) : (
<>
<div className="text-base font-medium text-[#9b2c2c]">
该目标生成失败
</div>
<div className="mt-2 text-sm leading-6 text-[#9b2c2c]">
{selectedPreviewResult.error || "请调整配置后重新生成。"}
</div>
</>
)}
</div>
</div>
) : (
<div className="pointer-events-none absolute bottom-5 left-1/2 -translate-x-1/2 rounded-full border border-[#e5e5e5] bg-white/92 px-4 py-2 text-xs text-[#666666] shadow-[0_12px_24px_rgba(0,0,0,0.06)]">
原图已就绪,点击下方开始 AI洗图
</div>
)}
</>
) : (
<div className="absolute inset-0 flex items-center justify-center px-6 text-center">
<div className="max-w-sm">
{selectedPreviewResult.status === "running" ||
selectedPreviewResult.status === "submitting" ? (
<>
<Loader2 className="mx-auto mb-4 h-9 w-9 animate-spin text-[#666666]" />
<div className="text-base font-medium text-[#111111]">
{selectedPreviewResult.status === "submitting"
? "正在提交模型任务"
: "模型正在生成结果"}
</div>
<div className="mt-2 text-sm leading-6 text-[#777777]">
生成完成后,这里会自动切换为大图对比预览。
</div>
</>
) : selectedPreviewResult.status === "failed" ? (
<>
<div className="text-base font-medium text-[#9b2c2c]">
该目标生成失败
</div>
<div className="mt-2 text-sm leading-6 text-[#9b2c2c]">
{selectedPreviewResult.error || "请调整配置后重新生成。"}
</div>
</>
) : (
<>
<ImageIcon className="mx-auto mb-4 h-10 w-10 text-[#b2b2b2]" />
<div className="text-base font-medium text-[#111111]">
固定单图预览区
</div>
<div className="mt-2 text-sm leading-6 text-[#777777]">
先上传底图并点击开始 AI洗图,结果会在这里完整展示。
</div>
</>
)}
<div>
<button
className="inline-flex h-16 items-center rounded-[26px] bg-[#111111] px-10 text-[18px] font-semibold text-white shadow-[0_18px_40px_rgba(0,0,0,0.16)] transition hover:bg-black"
onClick={() => onRequestAddImage?.()}
type="button"
>
<ImageIcon className="mr-3 h-5 w-5" />
添加图片
</button>
<div className="mt-4 text-sm text-[#777777]">
选择本地图后即可开始 AI洗图
</div>
</div>
</div>
)
) : compareSourceUrl ? (
<>
<img
alt="AI洗图底图"
className="absolute inset-0 h-full w-full object-contain"
src={getDisplayImageUrl(compareSourceUrl, 1800)}
/>
<div className="pointer-events-none absolute left-4 top-4 rounded-full bg-black/65 px-3 py-1.5 text-[11px] text-white">
原图
</div>
<div className="pointer-events-none absolute bottom-5 left-1/2 -translate-x-1/2 rounded-full border border-[#e5e5e5] bg-white/92 px-4 py-2 text-xs text-[#666666] shadow-[0_12px_24px_rgba(0,0,0,0.06)]">
原图已就绪,点击下方开始 AI洗图
</div>
</>
) : (
<div className="absolute inset-0 flex items-center justify-center px-6 text-center">
<div>
<Sparkles className="mx-auto mb-4 h-10 w-10 text-[#9a9a9a]" />
<div className="text-base font-medium text-[#111111]">
AI洗图准备就绪
</div>
<div className="mt-2 text-sm text-[#777777]">
管理员配置预设后,这里会展示共享结果预览。
<button
className="inline-flex h-16 items-center rounded-[26px] bg-[#111111] px-10 text-[18px] font-semibold text-white shadow-[0_18px_40px_rgba(0,0,0,0.16)] transition hover:bg-black"
onClick={() => onRequestAddImage?.()}
type="button"
>
<ImageIcon className="mr-3 h-5 w-5" />
添加图片
</button>
<div className="mt-4 text-sm text-[#777777]">
选择本地图后即可开始 AI洗图
</div>
</div>
</div>
@ -1414,30 +1371,61 @@ export function AiWashWorkspace({
</div>
</section>
<div className="mt-5 flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="rounded-full border border-[#e5e5e5] bg-white px-4 py-2 text-xs text-[#666666] shadow-[0_0_0_1px_rgba(0,0,0,0.02),0_10px_20px_rgba(0,0,0,0.03)]">
{selectedResult
? `已选中 ${selectedResult.title},可直接发送回画布`
: selectedPreviewResult
? `当前预览 ${selectedPreviewResult.title},生成完成后可发送回画布`
: "请选择或生成一张洗图结果"}
? compareSourceUrl
? `当前目标 ${selectedPreviewResult.title},生成完成后可发送回画布`
: "请先添加一张底图"
: compareSourceUrl
? "原图已上传,点击开始 AI洗图生成结果"
: "请先添加一张底图"}
</div>
<div className="flex flex-wrap items-center gap-2">
<Button
className={cn(
"h-11 rounded-[18px] px-5 text-sm font-semibold",
PRIMARY_ACTION_BUTTON_CLASS
)}
disabled={
isSubmitting ||
!compareSourceUrl ||
enabledPresets.length === 0 ||
(shouldShowUserModelSelector && !selectedUserModel)
}
onClick={handleSubmit}
type="button"
>
{isSubmitting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Sparkles className="mr-2 h-4 w-4" />
)}
{isSubmitting
? "正在洗图"
: totalEstimatedCost > 0
? `开始 AI洗图 · ${totalEstimatedCost} 积分`
: "开始 AI洗图"}
</Button>
<Button
className={cn(
"h-11 rounded-[18px] px-5 text-sm font-medium",
SECONDARY_ACTION_BUTTON_CLASS
)}
disabled={!selectedResult?.imageUrl}
onClick={handleApply}
type="button"
variant="outline"
>
<Send className="mr-2 h-4 w-4" />
发送到画布
</Button>
</div>
<Button
className={cn(
"h-11 rounded-[18px] px-5 text-sm font-medium",
SECONDARY_ACTION_BUTTON_CLASS
)}
disabled={!selectedResult?.imageUrl}
onClick={handleApply}
type="button"
variant="outline"
>
<Send className="mr-2 h-4 w-4" />
发送到画布
</Button>
</div>
<div className={cn(PANEL_CLASS, "mt-4 overflow-hidden p-3")}>
<div className={cn(PANEL_CLASS, "overflow-hidden p-3")}>
<button
className="flex w-full items-center justify-between gap-3 rounded-[22px] px-1 py-1 text-left transition hover:bg-[#fafafa]"
onClick={() => setIsHistoryExpanded((current) => !current)}