fix: polish ai wash layout and sidebar overflow
This commit is contained in:
parent
ea970888f8
commit
37f6ef7e8e
@ -24,7 +24,6 @@ import {
|
||||
} from "react";
|
||||
import useSWR from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import {
|
||||
@ -53,8 +52,6 @@ const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
const PANEL_CLASS =
|
||||
"rounded-[28px] border border-[#e5e5e5] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_20px_40px_rgba(0,0,0,0.06)]";
|
||||
const SUBTLE_PANEL_CLASS =
|
||||
"rounded-[22px] border border-[#ececec] bg-[#fbfbfb] shadow-[inset_0_1px_0_rgba(255,255,255,0.95)]";
|
||||
const PRIMARY_ACTION_BUTTON_CLASS =
|
||||
"border border-[#111111] bg-[#111111] text-white shadow-[0_16px_30px_rgba(0,0,0,0.18)] transition hover:bg-black hover:border-black disabled:border-[#e5e5e5] disabled:bg-[#f1f1f1] disabled:text-[#9a9a9a] disabled:shadow-none";
|
||||
const SECONDARY_ACTION_BUTTON_CLASS =
|
||||
@ -933,6 +930,13 @@ export function AiWashWorkspace({
|
||||
? comparePositionById[selectedPreviewResult.presetId] ?? 50
|
||||
: 50;
|
||||
const previewHasImage = Boolean(selectedPreviewResult?.imageUrl);
|
||||
const selectedExecutionState =
|
||||
presetExecutionStates.find(
|
||||
(item) => item.presetId === selectedPreviewResult?.presetId
|
||||
) ||
|
||||
presetExecutionStates.find((item) => item.preset.isDefault) ||
|
||||
presetExecutionStates[0] ||
|
||||
null;
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full bg-[#ffffff] text-[#111111]">
|
||||
@ -940,8 +944,13 @@ export function AiWashWorkspace({
|
||||
<div className="pattern-grid-lg pointer-events-none absolute inset-0 opacity-[0.025]" />
|
||||
|
||||
<div className="relative z-10 flex h-full w-full pt-24">
|
||||
<aside className="relative z-20 order-2 h-full w-[420px] max-w-[42vw] shrink-0 py-6 pr-6">
|
||||
<div className={cn("h-full space-y-3 overflow-y-auto pb-3", SIDEBAR_SCROLL_CLASS)}>
|
||||
<aside className="relative z-20 order-2 -mt-16 h-[calc(100%+4rem)] w-[430px] max-w-[42vw] shrink-0 pb-6 pr-6">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full space-y-4 overflow-y-auto rounded-[36px] border border-[#e7e7e7] bg-white/72 p-4 shadow-[0_24px_70px_rgba(0,0,0,0.08)] backdrop-blur-xl",
|
||||
SIDEBAR_SCROLL_CLASS
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={cn(
|
||||
@ -966,42 +975,36 @@ export function AiWashWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className={cn(PANEL_CLASS, "space-y-3 p-3.5")}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-[30px] border border-[#e8e8e8] bg-[#fbfbfb]/90 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.9)]">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[16px] border border-[#ececec] bg-white text-[#111111] shadow-[0_10px_22px_rgba(0,0,0,0.05)]">
|
||||
<CanvasToolbarIcon
|
||||
className="h-5 w-5"
|
||||
fallback={Sparkles}
|
||||
iconUrl={config.modeIconUrl}
|
||||
/>
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-[22px] border border-[#ececec] bg-white text-[#111111] shadow-[0_12px_24px_rgba(0,0,0,0.06)]">
|
||||
<SlidersHorizontal className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-semibold text-[22px] text-[#111111]">
|
||||
{config.modeLabel || "AI洗图"}
|
||||
<div className="inline-flex rounded-full border border-[#e6e6e6] bg-white px-3 py-1 font-semibold text-[11px] text-[#777777] tracking-[0.18em]">
|
||||
STUDIO SETUP
|
||||
</div>
|
||||
<div className="mt-2 font-semibold text-[20px] text-[#111111]">
|
||||
设置
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-[#ebebeb] bg-white px-2.5 py-1 text-[11px] text-[#666666]">
|
||||
{enabledPresets.length} 个预设
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span className="rounded-full border border-[#e6e6e6] bg-white px-3 py-1.5 text-xs text-[#666666]">
|
||||
Redefine
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 rotate-180 text-[#666666]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={cn(SUBTLE_PANEL_CLASS, "space-y-2 p-3.5")}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[14px] border border-[#ececec] bg-white text-[#111111] shadow-[0_8px_18px_rgba(0,0,0,0.04)]">
|
||||
<SlidersHorizontal className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold text-[15px] text-[#111111]">
|
||||
渲染目标
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[26px] border border-[#ececec] bg-[#fbfbfb]/92 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.95)]">
|
||||
<div className="mb-3 font-semibold text-[17px] text-[#111111]">
|
||||
增强预设
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2.5">
|
||||
{presetExecutionStates.map((item) => {
|
||||
const targetResult = results.find(
|
||||
(result) => result.presetId === item.preset.id
|
||||
@ -1014,72 +1017,46 @@ export function AiWashWorkspace({
|
||||
<button
|
||||
aria-pressed={isActive}
|
||||
className={cn(
|
||||
"w-full rounded-[16px] border px-3 py-2.5 text-left transition",
|
||||
"w-full rounded-[18px] border px-4 py-3 text-left transition",
|
||||
isActive
|
||||
? "border-[#111111] bg-[#111111] text-white shadow-[0_12px_24px_rgba(0,0,0,0.12)]"
|
||||
: "border-[#ececec] bg-white text-[#111111] hover:border-[#111111]"
|
||||
? "border-[#111111] bg-white text-[#111111] shadow-[0_10px_22px_rgba(0,0,0,0.08)]"
|
||||
: "border-[#e8e8e8] bg-white text-[#666666] hover:border-[#111111] hover:text-[#111111]"
|
||||
)}
|
||||
key={item.preset.id}
|
||||
onClick={() => setSelectedResultId(item.preset.id)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="min-w-0 truncate font-medium text-sm">
|
||||
<div className="min-w-0 truncate font-medium text-[15px]">
|
||||
{item.preset.title}
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-full border px-2 py-1 text-[11px]",
|
||||
isActive
|
||||
? "border-white/20 bg-white/10 text-white"
|
||||
? "border-[#111111] bg-[#111111] text-white"
|
||||
: "border-[#ebebeb] bg-[#fafafa] text-[#666666]"
|
||||
)}
|
||||
>
|
||||
{getAiWashStatusLabel(targetResult?.status)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex min-w-0 items-center gap-2 text-[11px]">
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 truncate rounded-full border px-2 py-1",
|
||||
isActive
|
||||
? "border-white/15 bg-white/10 text-white/80"
|
||||
: "border-[#ebebeb] bg-[#fafafa] text-[#666666]"
|
||||
)}
|
||||
>
|
||||
{item.resolvedModel?.displayName ||
|
||||
item.resolvedModel?.modelId ||
|
||||
(item.usesUserSelectableModel
|
||||
? "前台自选模型"
|
||||
: "待配置模型")}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-full border px-2 py-1",
|
||||
isActive
|
||||
? "border-white/15 bg-white/10 text-white/80"
|
||||
: "border-[#ebebeb] bg-[#fafafa] text-[#666666]"
|
||||
)}
|
||||
>
|
||||
{item.unitCost > 0 ? `${item.unitCost} 积分/张` : "未计费"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-3 max-h-24 overflow-hidden rounded-[18px] border border-[#e9e9e9] bg-white px-4 py-3 text-sm leading-6 text-[#666666]">
|
||||
{selectedExecutionState?.resolvedPrompt ||
|
||||
"选择预设后会使用管理员配置的提示词进行 AI洗图。"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn(SUBTLE_PANEL_CLASS, "space-y-3 p-3.5")}>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[14px] border border-[#ececec] bg-white text-[#111111] shadow-[0_8px_18px_rgba(0,0,0,0.04)]">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="font-semibold text-[15px] text-[#111111]">
|
||||
参数设置
|
||||
</div>
|
||||
<div className="rounded-[26px] border border-[#ececec] bg-[#fbfbfb]/92 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.95)]">
|
||||
<div className="mb-3 flex min-w-0 items-center justify-between gap-3">
|
||||
<div className="font-semibold text-[17px] text-[#111111]">
|
||||
参数设置
|
||||
</div>
|
||||
<Settings2 className="h-4 w-4 text-[#777777]" />
|
||||
</div>
|
||||
|
||||
{shouldShowUserModelSelector ? (
|
||||
@ -1148,7 +1125,7 @@ export function AiWashWorkspace({
|
||||
null;
|
||||
return (
|
||||
<div
|
||||
className="rounded-[16px] border border-[#ececec] bg-white px-3.5 py-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.94)]"
|
||||
className="rounded-[20px] border border-[#ececec] bg-white px-4 py-3.5 shadow-[0_8px_18px_rgba(0,0,0,0.03)]"
|
||||
key={control.id}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
@ -1184,361 +1161,288 @@ export function AiWashWorkspace({
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="order-1 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-3"
|
||||
)}
|
||||
>
|
||||
<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 ? (
|
||||
<>
|
||||
<img
|
||||
alt={selectedPreviewResult.title}
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
src={getDisplayImageUrl(
|
||||
selectedPreviewResult.imageUrl || "",
|
||||
1800
|
||||
)}
|
||||
/>
|
||||
{compareSourceUrl ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
style={{
|
||||
clipPath: `inset(0 ${100 - previewComparePosition}% 0 0)`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={`${selectedPreviewResult.title} 原图对比`}
|
||||
className="h-full w-full object-contain"
|
||||
src={getDisplayImageUrl(compareSourceUrl, 1800)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 w-px bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.22),0_0_20px_rgba(0,0,0,0.18)]"
|
||||
style={{ left: `${previewComparePosition}%` }}
|
||||
<main className="order-1 relative min-w-0 flex-1 overflow-hidden px-8 pb-7 pt-6">
|
||||
<div className="relative h-full w-full">
|
||||
{compareSourceUrl ? (
|
||||
<button
|
||||
className={cn(
|
||||
"absolute right-4 top-2 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}
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center px-8 pb-44 pt-8">
|
||||
{selectedPreviewResult && previewHasImage ? (
|
||||
<div className="relative max-h-full max-w-full overflow-hidden rounded-[24px] shadow-[0_18px_52px_rgba(0,0,0,0.08)]">
|
||||
<img
|
||||
alt={selectedPreviewResult.title}
|
||||
className="block max-h-[calc(100vh-320px)] max-w-[min(64vw,1120px)] object-contain"
|
||||
src={getDisplayImageUrl(selectedPreviewResult.imageUrl || "", 1800)}
|
||||
/>
|
||||
{compareSourceUrl ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
style={{
|
||||
clipPath: `inset(0 ${100 - previewComparePosition}% 0 0)`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={`${selectedPreviewResult.title} 原图对比`}
|
||||
className="h-full w-full object-contain"
|
||||
src={getDisplayImageUrl(compareSourceUrl, 1800)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 w-px bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.22),0_0_20px_rgba(0,0,0,0.18)]"
|
||||
style={{ left: `${previewComparePosition}%` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 cursor-col-resize touch-none"
|
||||
onPointerCancel={(event) =>
|
||||
finishCompareDrag(selectedPreviewResult.presetId, event)
|
||||
}
|
||||
onPointerDown={(event) =>
|
||||
handleComparePointerDown(
|
||||
selectedPreviewResult.presetId,
|
||||
event
|
||||
)
|
||||
}
|
||||
onPointerMove={(event) =>
|
||||
handleComparePointerMove(
|
||||
selectedPreviewResult.presetId,
|
||||
event
|
||||
)
|
||||
}
|
||||
onPointerUp={(event) =>
|
||||
finishCompareDrag(selectedPreviewResult.presetId, event)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 cursor-col-resize touch-none"
|
||||
onPointerCancel={(event) =>
|
||||
finishCompareDrag(selectedPreviewResult.presetId, event)
|
||||
}
|
||||
onPointerDown={(event) =>
|
||||
handleComparePointerDown(
|
||||
selectedPreviewResult.presetId,
|
||||
event
|
||||
)
|
||||
}
|
||||
onPointerMove={(event) =>
|
||||
handleComparePointerMove(
|
||||
selectedPreviewResult.presetId,
|
||||
event
|
||||
)
|
||||
}
|
||||
onPointerUp={(event) =>
|
||||
finishCompareDrag(selectedPreviewResult.presetId, event)
|
||||
}
|
||||
className="pointer-events-none absolute inset-y-0 w-12 -translate-x-1/2"
|
||||
style={{ left: `${previewComparePosition}%` }}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 w-12 -translate-x-1/2"
|
||||
style={{ left: `${previewComparePosition}%` }}
|
||||
>
|
||||
<div className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-white" />
|
||||
<div className="absolute left-1/2 top-1/2 flex h-12 w-12 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white bg-black/72 text-white shadow-[0_14px_28px_rgba(0,0,0,0.24)]">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-white" />
|
||||
<div className="absolute left-1/2 top-1/2 flex h-12 w-12 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white bg-black/72 text-white shadow-[0_14px_28px_rgba(0,0,0,0.24)]">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<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 right-4 top-4 rounded-full bg-black/65 px-3 py-1.5 text-[11px] text-white">
|
||||
洗后
|
||||
</div>
|
||||
</>
|
||||
) : 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 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="absolute inset-0 flex items-center justify-center px-6 text-center">
|
||||
<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 className="pointer-events-none absolute right-4 top-4 rounded-full bg-black/65 px-3 py-1.5 text-[11px] text-white">
|
||||
洗后
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : compareSourceUrl ? (
|
||||
<>
|
||||
<div className="relative max-h-full max-w-full">
|
||||
<img
|
||||
alt="AI洗图底图"
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
className="block max-h-[calc(100vh-320px)] max-w-[min(64vw,1120px)] rounded-[24px] object-contain shadow-[0_18px_52px_rgba(0,0,0,0.08)]"
|
||||
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>
|
||||
<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洗图
|
||||
{selectedPreviewResult?.status === "running" ||
|
||||
selectedPreviewResult?.status === "submitting" ||
|
||||
selectedPreviewResult?.status === "failed" ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-[24px] 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>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center">
|
||||
<button
|
||||
className="inline-flex h-[92px] items-center rounded-[28px] bg-[#111111] px-12 text-[22px] font-semibold text-white shadow-[0_22px_56px_rgba(0,0,0,0.16)] transition hover:bg-black"
|
||||
onClick={() => onRequestAddImage?.()}
|
||||
type="button"
|
||||
>
|
||||
<ImageIcon className="mr-4 h-6 w-6" />
|
||||
添加图片
|
||||
</button>
|
||||
<div className="mt-5 text-[15px] text-[#777777]">
|
||||
选择本地图后即可开始 {config.modeLabel || "AI洗图"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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
|
||||
? compareSourceUrl
|
||||
? `当前目标 ${selectedPreviewResult.title},生成完成后可发送回画布`
|
||||
: "请先添加一张底图"
|
||||
: compareSourceUrl
|
||||
? "原图已上传,点击开始 AI洗图生成结果"
|
||||
: "请先添加一张底图"}
|
||||
<div className="absolute bottom-0 left-1/2 z-30 flex -translate-x-1/2 flex-col items-center gap-2">
|
||||
<div className="rounded-full border border-[#e5e5e5] bg-white/92 px-5 py-2 text-xs text-[#666666] shadow-[0_10px_24px_rgba(0,0,0,0.06)] backdrop-blur">
|
||||
生成后可直接发送回画布
|
||||
</div>
|
||||
<div className="rounded-full border border-[#e5e5e5] bg-white/92 px-5 py-2 text-xs text-[#333333] shadow-[0_10px_24px_rgba(0,0,0,0.06)] backdrop-blur">
|
||||
{totalEstimatedCost > 0
|
||||
? `预计消耗 ${totalEstimatedCost} 积分`
|
||||
: "预计消耗按模型配置计算"}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-[34px] border border-[#e6e6e6] bg-white/88 p-3 shadow-[0_18px_46px_rgba(0,0,0,0.08)] backdrop-blur-xl">
|
||||
<Button
|
||||
className={cn(
|
||||
"h-14 min-w-[220px] rounded-[26px] px-8 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 ? "正在洗图" : "开始 AI洗图"}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(
|
||||
"h-14 min-w-[220px] rounded-[26px] px-8 text-[15px] 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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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]"
|
||||
className="absolute bottom-2 left-2 z-30 inline-flex h-10 items-center gap-2 rounded-full border border-[#e5e5e5] bg-white/90 px-4 text-xs text-[#666666] shadow-[0_10px_24px_rgba(0,0,0,0.06)] backdrop-blur transition hover:border-[#111111] hover:text-[#111111]"
|
||||
onClick={() => setIsHistoryExpanded((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-[16px] border border-[#ececec] bg-white shadow-[0_10px_22px_rgba(0,0,0,0.05)]">
|
||||
<HistoryIcon className="h-5 w-5 text-[#111111]" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold text-[#111111]">
|
||||
历史洗图记录
|
||||
</div>
|
||||
<div className="text-sm text-[#666666]">
|
||||
保留最近的 AI洗图批次,支持一键回看并继续发送结果到画布。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<div className="rounded-full border border-[#ebebeb] bg-[#fafafa] px-3 py-1 text-xs text-[#666666]">
|
||||
最近 {visibleHistoryBatches.length} 条
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-1 rounded-full border border-[#ebebeb] bg-white px-3 py-1 text-xs text-[#666666]">
|
||||
{isHistoryExpanded ? "收起" : "展开"}
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 transition-transform",
|
||||
isHistoryExpanded && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HistoryIcon className="h-4 w-4" />
|
||||
历史 {visibleHistoryBatches.length}
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 transition-transform",
|
||||
isHistoryExpanded && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isHistoryExpanded ? (
|
||||
visibleHistoryBatches.length > 0 ? (
|
||||
<div className="mt-3 flex gap-3 overflow-x-auto border-t border-[#eeeeee] pt-3 pb-1">
|
||||
{visibleHistoryBatches.map((entry) => {
|
||||
const cover = entry.items[0];
|
||||
return (
|
||||
<div
|
||||
className="min-w-[260px] max-w-[260px] rounded-[22px] border border-[#ececec] bg-[#fbfbfb] p-3"
|
||||
key={entry.id}
|
||||
>
|
||||
<button
|
||||
className="w-full text-left"
|
||||
onClick={() => handleLoadHistoryBatch(entry)}
|
||||
type="button"
|
||||
>
|
||||
<div className="overflow-hidden rounded-[18px] border border-[#ececec] bg-white">
|
||||
<img
|
||||
alt={cover?.title || "历史洗图结果"}
|
||||
className="aspect-[4/3] w-full object-cover"
|
||||
src={getDisplayImageUrl(
|
||||
cover?.thumbnailUrl || cover?.imageUrl || "",
|
||||
520
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-medium text-sm text-[#111111]">
|
||||
{cover?.title || "AI洗图结果"}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[11px] text-[#666666]">
|
||||
<Clock3 className="h-3.5 w-3.5" />
|
||||
{new Date(entry.createdAt).toLocaleString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-[#ebebeb] bg-white px-2 py-1 text-[11px] text-[#666666]">
|
||||
{entry.items.length} 张
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{cover?.modelId ? (
|
||||
<span className="rounded-full border border-[#ebebeb] bg-white px-2 py-1 text-[11px] text-[#666666]">
|
||||
{cover.modelId}
|
||||
</span>
|
||||
) : null}
|
||||
{entry.sourceImageUrl ? (
|
||||
<span className="rounded-full border border-[#dcebdd] bg-[#f4fbf5] px-2 py-1 text-[11px] text-[#2f6f3b]">
|
||||
可对比
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white text-[#666666] transition hover:border-[#111111] hover:text-[#111111]"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteHistoryBatch(entry.id);
|
||||
}}
|
||||
title="删除历史记录"
|
||||
type="button"
|
||||
<div className="absolute bottom-16 left-2 z-40 w-[380px] rounded-[28px] border border-[#e5e5e5] bg-white/94 p-3 shadow-[0_24px_60px_rgba(0,0,0,0.12)] backdrop-blur-xl">
|
||||
{visibleHistoryBatches.length > 0 ? (
|
||||
<div className="flex max-h-[360px] flex-col gap-3 overflow-y-auto pr-1">
|
||||
{visibleHistoryBatches.map((entry) => {
|
||||
const cover = entry.items[0];
|
||||
return (
|
||||
<div
|
||||
className="rounded-[20px] border border-[#ececec] bg-[#fbfbfb] p-3"
|
||||
key={entry.id}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-[22px] border border-dashed border-[#dcdcdc] bg-[#fafafa] px-5 py-8 text-center text-sm text-[#8a8a8a]">
|
||||
还没有历史洗图记录,生成一次后会自动出现在这里。
|
||||
</div>
|
||||
)
|
||||
<button
|
||||
className="flex w-full items-center gap-3 text-left"
|
||||
onClick={() => handleLoadHistoryBatch(entry)}
|
||||
type="button"
|
||||
>
|
||||
<div className="h-16 w-20 shrink-0 overflow-hidden rounded-[14px] border border-[#ececec] bg-white">
|
||||
<img
|
||||
alt={cover?.title || "历史洗图结果"}
|
||||
className="h-full w-full object-cover"
|
||||
src={getDisplayImageUrl(
|
||||
cover?.thumbnailUrl || cover?.imageUrl || "",
|
||||
360
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-medium text-sm text-[#111111]">
|
||||
{cover?.title || "AI洗图结果"}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[11px] text-[#666666]">
|
||||
<Clock3 className="h-3.5 w-3.5" />
|
||||
{new Date(entry.createdAt).toLocaleString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-[#ebebeb] bg-white px-2 py-1 text-[11px] text-[#666666]">
|
||||
{entry.items.length} 张
|
||||
</div>
|
||||
</button>
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
{cover?.modelId ? (
|
||||
<span className="max-w-[240px] truncate rounded-full border border-[#ebebeb] bg-white px-2 py-1 text-[11px] text-[#666666]">
|
||||
{cover.modelId}
|
||||
</span>
|
||||
) : null}
|
||||
{entry.sourceImageUrl ? (
|
||||
<span className="rounded-full border border-[#dcebdd] bg-[#f4fbf5] px-2 py-1 text-[11px] text-[#2f6f3b]">
|
||||
可对比
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<button
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white text-[#666666] transition hover:border-[#111111] hover:text-[#111111]"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleDeleteHistoryBatch(entry.id);
|
||||
}}
|
||||
title="删除历史记录"
|
||||
type="button"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[20px] border border-dashed border-[#dcdcdc] bg-[#fafafa] px-5 py-8 text-center text-sm text-[#8a8a8a]">
|
||||
还没有历史洗图记录,生成一次后会自动出现在这里。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@ -122,7 +122,7 @@ export function FloatingSidebarRail({
|
||||
: "border border-white/8 bg-[#17181d]/94 text-white shadow-[0_22px_60px_rgba(6,8,14,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]",
|
||||
isHorizontal
|
||||
? "h-[56px] w-auto flex-row overflow-hidden rounded-[22px] px-3 py-2"
|
||||
: "w-[56px] flex-col overflow-y-auto rounded-[24px] px-2 py-2",
|
||||
: "w-[56px] flex-col overflow-x-hidden overflow-y-auto overscroll-contain rounded-[24px] px-2 py-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user