refactor: align ai wash workspace styling
This commit is contained in:
parent
1c7d005088
commit
40190a6034
@ -1,13 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
History as HistoryIcon,
|
||||
ImageIcon,
|
||||
Loader2,
|
||||
RefreshCcw,
|
||||
Send,
|
||||
Settings2,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
Upload,
|
||||
@ -50,9 +51,23 @@ import { cn } from "@/lib/utils";
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
|
||||
const PANEL_CLASS =
|
||||
"rounded-[28px] border border-white/10 bg-white/[0.04] shadow-[0_20px_40px_rgba(0,0,0,0.22)] backdrop-blur-xl";
|
||||
"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-[24px] border border-white/10 bg-white/[0.03] shadow-[inset_0_1px_0_rgba(255,255,255,0.05)]";
|
||||
"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 =
|
||||
"border border-[#d9d9d9] bg-white text-[#111111] shadow-[0_0_0_1px_rgba(0,0,0,0.02),0_10px_22px_rgba(0,0,0,0.03)] transition hover:border-[#111111] hover:bg-[#fafafa] hover:text-black disabled:border-[#e5e5e5] disabled:bg-[#f7f7f7] disabled:text-[#a5a5a5] disabled:shadow-none";
|
||||
const FIELD_CLASS =
|
||||
"h-10 rounded-[14px] border-[#e5e5e5] bg-white text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95),0_1px_2px_rgba(0,0,0,0.04)]";
|
||||
const SELECT_CONTENT_CLASS =
|
||||
"border-[#e5e5e5] bg-white text-[#111111] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_18px_38px_rgba(0,0,0,0.08)]";
|
||||
const SIDEBAR_SCROLL_CLASS =
|
||||
"pr-1.5 [scrollbar-width:thin] [scrollbar-color:#c8c8c8_transparent] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-transparent [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-[#c8c8c8] hover:[&::-webkit-scrollbar-thumb]:bg-[#a8a8a8]";
|
||||
const SLIDER_TRACK_CLASS = "h-2.5 bg-[#ededed] shadow-inner";
|
||||
const SLIDER_RANGE_CLASS = "bg-[#111111]";
|
||||
const SLIDER_THUMB_CLASS =
|
||||
"h-4 w-4 border border-[#d4d4d4] bg-white shadow-[0_0_0_4px_rgba(0,0,0,0.08)]";
|
||||
const AI_WASH_HISTORY_STORAGE_KEY = "ai-chatbot:ai-wash-history:v1";
|
||||
const AI_WASH_HISTORY_TOTAL_LIMIT = 24;
|
||||
const AI_WASH_TASK_POLL_INTERVAL_MS = 2500;
|
||||
@ -891,432 +906,459 @@ export function AiWashWorkspace({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full bg-[#0a0c12] text-white">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(95,99,255,0.16),transparent_28%),radial-gradient(circle_at_bottom_right,rgba(16,185,129,0.14),transparent_22%),linear-gradient(180deg,#0b0d13_0%,#07090e_100%)]" />
|
||||
<div className="relative flex h-full w-full bg-[#ffffff] text-[#111111]">
|
||||
<div className="pointer-events-none absolute inset-0 bg-[#ffffff]" />
|
||||
<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="flex h-full w-[320px] shrink-0 border-white/8 border-r px-5 pb-6">
|
||||
<div className="flex h-full w-full flex-col gap-4 overflow-y-auto pr-1">
|
||||
<div className={cn(PANEL_CLASS, "p-4")}>
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-[18px] border border-white/10 bg-white/[0.05]">
|
||||
<aside className="relative z-20 h-full w-[420px] max-w-[42vw] shrink-0 py-6 pl-6">
|
||||
<div className={cn("h-full space-y-3 overflow-y-auto pb-3", SIDEBAR_SCROLL_CLASS)}>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={cn(
|
||||
PANEL_CLASS,
|
||||
"flex items-center gap-2 px-4 py-3 text-[#666666] text-sm"
|
||||
)}
|
||||
>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
正在加载 AI洗图 配置...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!config.enabled && !isLoading ? (
|
||||
<div className="rounded-[24px] border border-[#e5e5e5] bg-white px-4 py-3 text-[#333333] text-sm leading-6 shadow-[0_10px_24px_rgba(0,0,0,0.04)]">
|
||||
当前未启用 AI洗图。请先在系统设置的“本地AI设置”里开启并配置预设。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{lastErrorMessage ? (
|
||||
<div className="rounded-[24px] border border-[#f0d4d4] bg-[#fff7f7] px-4 py-3 text-[#9b2c2c] text-sm leading-6 shadow-[0_10px_24px_rgba(0,0,0,0.04)]">
|
||||
{lastErrorMessage}
|
||||
</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="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 text-white"
|
||||
className="h-5 w-5"
|
||||
fallback={Sparkles}
|
||||
iconUrl={config.modeIconUrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 text-[30px] font-semibold tracking-[-0.05em]">
|
||||
{config.modeLabel || "AI洗图"}
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-semibold text-[22px] text-[#111111]">
|
||||
{config.modeLabel || "AI洗图"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-white/58">
|
||||
使用管理员预设好的模型与提示词,生成多路洗图结果并拖动对比。
|
||||
<div className="rounded-full border border-[#ebebeb] bg-white px-2.5 py-1 text-[11px] text-[#666666]">
|
||||
{enabledPresets.length} 个预设
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-xs text-white/70">
|
||||
{enabledPresets.length} 个预设
|
||||
|
||||
<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")}>
|
||||
<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>
|
||||
<div className="space-y-2">
|
||||
{presetExecutionStates.map((item) => (
|
||||
<div
|
||||
className="rounded-[16px] border border-[#ececec] bg-white px-3 py-2.5"
|
||||
key={item.preset.id}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="min-w-0 truncate font-medium text-sm text-[#111111]">
|
||||
{item.preset.title}
|
||||
</div>
|
||||
<span className="shrink-0 rounded-full border border-[#ebebeb] bg-[#fafafa] px-2 py-1 text-[11px] text-[#666666]">
|
||||
{item.unitCost > 0 ? `${item.unitCost} 积分/张` : "未计费"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex min-w-0 items-center gap-2 text-[11px] text-[#666666]">
|
||||
<span className="min-w-0 truncate rounded-full border border-[#ebebeb] bg-[#fafafa] px-2 py-1">
|
||||
{item.resolvedModel?.displayName ||
|
||||
item.resolvedModel?.modelId ||
|
||||
(item.usesUserSelectableModel
|
||||
? "前台自选模型"
|
||||
: "待配置模型")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="h-12 w-full rounded-[18px] border-0 bg-white text-[#111111] hover:bg-white/90"
|
||||
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 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>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className={cn(SUBTLE_PANEL_CLASS, "p-4")}>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-[18px] border border-white/10 bg-white/[0.05]">
|
||||
<CanvasToolbarIcon
|
||||
className="h-5 w-5 text-white/80"
|
||||
fallback={Sparkles}
|
||||
iconUrl={config.modeIconUrl}
|
||||
{shouldShowUserModelSelector ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-[12px] font-medium text-[#666666]">
|
||||
生图模型
|
||||
</div>
|
||||
{selectedUserModel?.creditDiscountLabel ? (
|
||||
<span className="rounded-full bg-emerald-50 px-2 py-1 text-[11px] text-emerald-700">
|
||||
{selectedUserModel.creditDiscountLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={setSelectedUserModelId}
|
||||
value={selectedUserModel?.id || selectedUserModelId}
|
||||
>
|
||||
<SelectTrigger className={FIELD_CLASS}>
|
||||
<SelectValue placeholder="选择生图模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={SELECT_CONTENT_CLASS}>
|
||||
{selectableModelOptions.map((model) => (
|
||||
<SelectItem
|
||||
disabled={model.selectable === false}
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
>
|
||||
<div className="flex w-full min-w-0 items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{model.displayName}
|
||||
</span>
|
||||
<span className="shrink-0 text-[#777777] text-xs">
|
||||
{model.unitCost > 0
|
||||
? `${model.unitCost} 积分/张`
|
||||
: "未计费"}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-[12px] leading-5 text-[#666666]">
|
||||
{selectedUserModel
|
||||
? `当前模型:${selectedUserModel.displayName} · ${
|
||||
selectedUserModel.unitCost > 0
|
||||
? `${selectedUserModel.unitCost} 积分/张`
|
||||
: "未设置积分"
|
||||
}`
|
||||
: modelOptionsLoading
|
||||
? "正在加载可选模型..."
|
||||
: "没有可选模型"}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{uiControls.map((control) => {
|
||||
const selectedIndex =
|
||||
selectedLevelIndexByControl[control.id] ?? control.defaultLevel;
|
||||
const activeLevel =
|
||||
getControlLevelByIndex(control, selectedIndex) ||
|
||||
control.levels[0] ||
|
||||
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)]"
|
||||
key={control.id}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-[#222222]">
|
||||
{control.label}
|
||||
</div>
|
||||
<div className="inline-flex min-w-[54px] justify-center rounded-full border border-[#e7e7e7] bg-white px-2.5 py-1 font-medium text-[11px] text-[#666666] shadow-[0_4px_12px_rgba(0,0,0,0.04)]">
|
||||
{activeLevel?.label || selectedIndex + 1}
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
className="w-full"
|
||||
max={Math.max(0, control.levels.length - 1)}
|
||||
min={0}
|
||||
onValueChange={([next]) =>
|
||||
setSelectedLevelIndexByControl((current) => ({
|
||||
...current,
|
||||
[control.id]: Math.round(next ?? 0),
|
||||
}))
|
||||
}
|
||||
rangeClassName={SLIDER_RANGE_CLASS}
|
||||
step={1}
|
||||
thumbClassName={SLIDER_THUMB_CLASS}
|
||||
trackClassName={SLIDER_TRACK_CLASS}
|
||||
value={[selectedIndex]}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-[11px] font-semibold tracking-[0.22em] text-white/45 uppercase">
|
||||
Fine Tune
|
||||
</div>
|
||||
<div className="truncate text-lg font-semibold tracking-[-0.03em]">
|
||||
{config.modeLabel || "AI洗图"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.06] px-3 py-1 text-sm font-semibold text-white/88">
|
||||
{totalEstimatedCost > 0 ? `${totalEstimatedCost} 积分` : "待计费"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{shouldShowUserModelSelector ? (
|
||||
<div className="mb-4 space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium">生图模型</div>
|
||||
{selectedUserModel?.creditDiscountLabel ? (
|
||||
<span className="rounded-full bg-emerald-400/10 px-2 py-1 text-[11px] text-emerald-100">
|
||||
{selectedUserModel.creditDiscountLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Select
|
||||
onValueChange={setSelectedUserModelId}
|
||||
value={selectedUserModel?.id || selectedUserModelId}
|
||||
>
|
||||
<SelectTrigger className="h-11 rounded-[16px] border-white/10 bg-white/[0.04] text-white">
|
||||
<SelectValue placeholder="选择生图模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="border-white/10 bg-[#11141b] text-white">
|
||||
{selectableModelOptions.map((model) => (
|
||||
<SelectItem
|
||||
disabled={model.selectable === false}
|
||||
key={model.id}
|
||||
value={model.id}
|
||||
>
|
||||
<div className="flex w-full min-w-0 items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{model.displayName}
|
||||
</span>
|
||||
<span className="shrink-0 text-white/52 text-xs">
|
||||
{model.unitCost > 0
|
||||
? `${model.unitCost} 积分/张`
|
||||
: "未计费"}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-[12px] leading-5 text-white/52">
|
||||
{selectedUserModel
|
||||
? `当前模型:${selectedUserModel.displayName} · ${
|
||||
selectedUserModel.unitCost > 0
|
||||
? `${selectedUserModel.unitCost} 积分/张`
|
||||
: "未设置积分"
|
||||
}`
|
||||
: modelOptionsLoading
|
||||
? "正在加载可选模型..."
|
||||
: "没有可选模型"}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 rounded-[18px] border border-white/10 bg-white/[0.04] px-3.5 py-3 text-[12px] leading-5 text-white/62">
|
||||
管理员已经为当前洗图流程指定了模型,前台不再展示模型选择。
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{uiControls.map((control) => {
|
||||
const selectedIndex =
|
||||
selectedLevelIndexByControl[control.id] ?? control.defaultLevel;
|
||||
const activeLevel =
|
||||
getControlLevelByIndex(control, selectedIndex) ||
|
||||
control.levels[0] ||
|
||||
null;
|
||||
return (
|
||||
<div
|
||||
className="rounded-[20px] border border-white/10 bg-white/[0.03] px-3.5 py-3"
|
||||
key={control.id}
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-white/88">
|
||||
{control.label}
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.06] px-2.5 py-1 text-xs font-semibold text-white/78">
|
||||
{activeLevel?.label || selectedIndex + 1}
|
||||
</div>
|
||||
</div>
|
||||
<Slider
|
||||
className="w-full"
|
||||
max={Math.max(0, control.levels.length - 1)}
|
||||
min={0}
|
||||
onValueChange={([next]) =>
|
||||
setSelectedLevelIndexByControl((current) => ({
|
||||
...current,
|
||||
[control.id]: Math.round(next ?? 0),
|
||||
}))
|
||||
}
|
||||
step={1}
|
||||
value={[selectedIndex]}
|
||||
/>
|
||||
<div className="mt-2 text-[11px] leading-5 text-white/46">
|
||||
{control.helperText}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={cn(SUBTLE_PANEL_CLASS, "p-4")}>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-sm font-medium">上传底图</div>
|
||||
<Button
|
||||
className="h-8 rounded-full border-white/10 bg-white/[0.05] px-3 text-white hover:bg-white/[0.1]"
|
||||
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-white/10 bg-black/20">
|
||||
<img
|
||||
alt="AI洗图底图"
|
||||
className="aspect-[4/5] w-full object-cover"
|
||||
src={getDisplayImageUrl(compareSourceUrl, 720)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex aspect-[4/5] items-center justify-center rounded-[18px] border border-dashed border-white/12 bg-white/[0.02] text-center text-sm text-white/45">
|
||||
先上传一张底图
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={cn(SUBTLE_PANEL_CLASS, "p-4")}>
|
||||
<div className="mb-3 text-sm font-medium">渲染目标</div>
|
||||
<div className="space-y-2">
|
||||
{presetExecutionStates.map((item) => (
|
||||
<div
|
||||
className="rounded-[18px] border border-white/10 bg-white/[0.04] px-3 py-3"
|
||||
key={item.preset.id}
|
||||
>
|
||||
<div className="font-medium text-sm text-white">
|
||||
{item.preset.title}
|
||||
</div>
|
||||
<div className="mt-1 text-[12px] leading-5 text-white/55">
|
||||
{item.preset.description}
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-[11px] text-white/55">
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.05] px-2 py-1">
|
||||
{item.resolvedModel?.displayName ||
|
||||
item.resolvedModel?.modelId ||
|
||||
(item.usesUserSelectableModel
|
||||
? "前台自选模型"
|
||||
: "待配置模型")}
|
||||
</span>
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.05] px-2 py-1">
|
||||
{item.unitCost > 0 ? `${item.unitCost} 积分/张` : "未计费"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{lastErrorMessage ? (
|
||||
<div className="rounded-[18px] border border-rose-400/18 bg-rose-400/10 px-4 py-3 text-sm text-rose-100">
|
||||
{lastErrorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex min-w-0 flex-1 flex-col overflow-hidden px-6 pb-6">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="grid gap-5 xl:grid-cols-3">
|
||||
{results.length > 0 ? (
|
||||
results.map((result) => {
|
||||
const hasImage = Boolean(result.imageUrl);
|
||||
const comparePosition = comparePositionById[result.presetId] ?? 50;
|
||||
const isSelected = selectedResultId === result.presetId && hasImage;
|
||||
<main className="flex min-w-0 flex-1 flex-col overflow-hidden px-6 pb-6 pt-6">
|
||||
<div className="grid min-h-0 flex-1 gap-5 xl:grid-cols-3">
|
||||
{results.length > 0 ? (
|
||||
results.map((result) => {
|
||||
const hasImage = Boolean(result.imageUrl);
|
||||
const comparePosition = comparePositionById[result.presetId] ?? 50;
|
||||
const isSelected = selectedResultId === result.presetId && hasImage;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
PANEL_CLASS,
|
||||
"flex min-h-[560px] flex-col overflow-hidden p-4 text-left transition",
|
||||
isSelected && "ring-2 ring-white/70"
|
||||
)}
|
||||
key={result.presetId}
|
||||
onClick={() => hasImage && setSelectedResultId(result.presetId)}
|
||||
type="button"
|
||||
>
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[22px] font-semibold tracking-[-0.04em]">
|
||||
{result.title}
|
||||
</div>
|
||||
<div className="mt-1 text-sm leading-6 text-white/58">
|
||||
{result.description}
|
||||
</div>
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
PANEL_CLASS,
|
||||
"flex min-h-0 flex-col overflow-hidden p-4 text-left transition",
|
||||
isSelected &&
|
||||
"border-[#111111] shadow-[0_0_0_1px_rgba(0,0,0,0.08),0_18px_36px_rgba(0,0,0,0.08)]"
|
||||
)}
|
||||
key={result.presetId}
|
||||
onClick={() => hasImage && setSelectedResultId(result.presetId)}
|
||||
type="button"
|
||||
>
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-[22px] font-semibold text-[#111111]">
|
||||
{result.title}
|
||||
</div>
|
||||
<div className="shrink-0 rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs text-white/72">
|
||||
{result.status === "idle"
|
||||
? "待生成"
|
||||
: result.status === "submitting"
|
||||
? "提交中"
|
||||
: result.status === "running"
|
||||
? "生成中"
|
||||
: result.status === "failed"
|
||||
? "失败"
|
||||
: "已完成"}
|
||||
<div className="mt-1 text-sm leading-6 text-[#666666]">
|
||||
{result.description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 rounded-full border border-[#ebebeb] bg-[#fafafa] px-3 py-1 text-xs text-[#666666]">
|
||||
{result.status === "idle"
|
||||
? "待生成"
|
||||
: result.status === "submitting"
|
||||
? "提交中"
|
||||
: result.status === "running"
|
||||
? "生成中"
|
||||
: result.status === "failed"
|
||||
? "失败"
|
||||
: "已完成"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex-1 overflow-hidden rounded-[22px] border border-white/10 bg-black/25">
|
||||
{hasImage ? (
|
||||
<>
|
||||
<img
|
||||
alt={result.title}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={getDisplayImageUrl(result.imageUrl || "", 1400)}
|
||||
/>
|
||||
{compareSourceUrl ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
style={{
|
||||
clipPath: `inset(0 ${100 - comparePosition}% 0 0)`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={`${result.title} 原图对比`}
|
||||
className="h-full w-full object-cover"
|
||||
src={getDisplayImageUrl(compareSourceUrl, 1400)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 w-px bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.3),0_0_16px_rgba(0,0,0,0.3)]"
|
||||
style={{ left: `${comparePosition}%` }}
|
||||
<div className="relative min-h-[360px] flex-1 overflow-hidden rounded-[22px] border border-[#ececec] bg-[#f7f7f7]">
|
||||
{hasImage ? (
|
||||
<>
|
||||
<img
|
||||
alt={result.title}
|
||||
className="absolute inset-0 h-full w-full object-contain"
|
||||
src={getDisplayImageUrl(result.imageUrl || "", 1400)}
|
||||
/>
|
||||
{compareSourceUrl ? (
|
||||
<>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 overflow-hidden"
|
||||
style={{
|
||||
clipPath: `inset(0 ${100 - comparePosition}% 0 0)`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={`${result.title} 原图对比`}
|
||||
className="h-full w-full object-contain"
|
||||
src={getDisplayImageUrl(compareSourceUrl, 1400)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 w-px bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.2),0_0_16px_rgba(0,0,0,0.18)]"
|
||||
style={{ left: `${comparePosition}%` }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 cursor-col-resize touch-none"
|
||||
onPointerCancel={(event) =>
|
||||
finishCompareDrag(result.presetId, event)
|
||||
}
|
||||
onPointerDown={(event) =>
|
||||
handleComparePointerDown(result.presetId, event)
|
||||
}
|
||||
onPointerMove={(event) =>
|
||||
handleComparePointerMove(result.presetId, event)
|
||||
}
|
||||
onPointerUp={(event) =>
|
||||
finishCompareDrag(result.presetId, event)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 cursor-col-resize touch-none"
|
||||
onPointerCancel={(event) =>
|
||||
finishCompareDrag(result.presetId, event)
|
||||
}
|
||||
onPointerDown={(event) =>
|
||||
handleComparePointerDown(result.presetId, event)
|
||||
}
|
||||
onPointerMove={(event) =>
|
||||
handleComparePointerMove(result.presetId, event)
|
||||
}
|
||||
onPointerUp={(event) =>
|
||||
finishCompareDrag(result.presetId, event)
|
||||
}
|
||||
className="pointer-events-none absolute inset-y-0 w-12 -translate-x-1/2"
|
||||
style={{ left: `${comparePosition}%` }}
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-y-0 w-12 -translate-x-1/2"
|
||||
style={{ left: `${comparePosition}%` }}
|
||||
>
|
||||
<div className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-white/95" />
|
||||
<div className="absolute left-1/2 top-1/2 flex h-11 w-11 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full bg-[#5b67ff] text-white shadow-[0_12px_28px_rgba(91,103,255,0.45)]">
|
||||
<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-11 w-11 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-white bg-black/70 text-white shadow-[0_12px_24px_rgba(0,0,0,0.2)]">
|
||||
<RefreshCcw className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute left-3 top-3 rounded-full bg-black/55 px-2.5 py-1 text-[11px] text-white/92">
|
||||
原图
|
||||
</div>
|
||||
<div className="pointer-events-none absolute right-3 top-3 rounded-full bg-black/55 px-2.5 py-1 text-[11px] text-white/92">
|
||||
洗后
|
||||
</div>
|
||||
<div className="pointer-events-none absolute left-3 top-3 rounded-full bg-black/65 px-2.5 py-1 text-[11px] text-white">
|
||||
原图
|
||||
</div>
|
||||
<div className="pointer-events-none absolute right-3 top-3 rounded-full bg-black/65 px-2.5 py-1 text-[11px] text-white">
|
||||
洗后
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[360px] items-center justify-center bg-[linear-gradient(180deg,#ffffff,#f7f7f7)]">
|
||||
<div className="px-6 text-center">
|
||||
{result.status === "running" || result.status === "submitting" ? (
|
||||
<>
|
||||
<Loader2 className="mx-auto mb-3 h-8 w-8 animate-spin text-[#666666]" />
|
||||
<div className="text-sm text-[#666666]">
|
||||
{result.status === "submitting"
|
||||
? "正在提交模型任务"
|
||||
: "模型正在生成结果"}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[440px] items-center justify-center bg-[linear-gradient(180deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]">
|
||||
<div className="px-6 text-center">
|
||||
{result.status === "running" || result.status === "submitting" ? (
|
||||
<>
|
||||
<Loader2 className="mx-auto mb-3 h-8 w-8 animate-spin text-white/70" />
|
||||
<div className="text-sm text-white/72">
|
||||
{result.status === "submitting"
|
||||
? "正在提交模型任务"
|
||||
: "模型正在生成结果"}
|
||||
</div>
|
||||
</>
|
||||
) : result.status === "failed" ? (
|
||||
<div className="text-sm leading-6 text-rose-100/90">
|
||||
{result.error || "该预设生成失败"}
|
||||
) : result.status === "failed" ? (
|
||||
<div className="text-sm leading-6 text-[#9b2c2c]">
|
||||
{result.error || "该预设生成失败"}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ImageIcon className="mx-auto mb-3 h-8 w-8 text-[#b2b2b2]" />
|
||||
<div className="text-sm text-[#888888]">
|
||||
生成后会在这里完整展示可拖动对比图
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ImageIcon className="mx-auto mb-3 h-8 w-8 text-white/45" />
|
||||
<div className="text-sm text-white/58">
|
||||
生成后会在这里展示可拖动对比图
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between gap-3 text-xs text-white/58">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{result.modelId ? (
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-1">
|
||||
{result.modelId}
|
||||
</span>
|
||||
) : null}
|
||||
{isSelected ? (
|
||||
<span className="rounded-full border border-emerald-400/20 bg-emerald-400/10 px-2.5 py-1 text-emerald-100">
|
||||
当前发送目标
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{result.createdAt ? (
|
||||
<span>
|
||||
{new Date(result.createdAt).toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between gap-3 text-xs text-[#666666]">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{result.modelId ? (
|
||||
<span className="rounded-full border border-[#ebebeb] bg-[#fafafa] px-2.5 py-1">
|
||||
{result.modelId}
|
||||
</span>
|
||||
) : null}
|
||||
{isSelected ? (
|
||||
<span className="rounded-full border border-[#111111] bg-white px-2.5 py-1 text-[#111111]">
|
||||
当前发送目标
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={cn(PANEL_CLASS, "col-span-full p-10 text-center")}>
|
||||
<Sparkles className="mx-auto mb-4 h-8 w-8 text-white/55" />
|
||||
<div className="text-lg font-medium">AI洗图准备就绪</div>
|
||||
<div className="mt-2 text-sm text-white/58">
|
||||
上传底图后,系统会按管理员配置好的多个模型预设同时生成洗图结果。
|
||||
</div>
|
||||
{result.createdAt ? (
|
||||
<span>
|
||||
{new Date(result.createdAt).toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className={cn(PANEL_CLASS, "col-span-full p-10 text-center")}>
|
||||
<Sparkles className="mx-auto mb-4 h-8 w-8 text-[#8a8a8a]" />
|
||||
<div className="text-lg font-medium text-[#111111]">AI洗图准备就绪</div>
|
||||
<div className="mt-2 text-sm text-[#666666]">
|
||||
上传底图后,系统会按管理员配置好的多个模式同时生成洗图结果。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.05] px-4 py-2 text-xs text-white/62">
|
||||
<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},可直接发送回画布`
|
||||
: "请选择或生成一张洗图结果"}
|
||||
</div>
|
||||
<Button
|
||||
className="h-11 rounded-[18px] border border-white/12 bg-white text-[#111111] px-5 hover:bg-white/90"
|
||||
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" />
|
||||
发送到画布
|
||||
@ -1326,19 +1368,19 @@ export function AiWashWorkspace({
|
||||
<div className={cn(PANEL_CLASS, "mt-5 p-4")}>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-[16px] border border-white/10 bg-white/[0.05]">
|
||||
<HistoryIcon className="h-5 w-5 text-white/80" />
|
||||
<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 tracking-[-0.03em]">
|
||||
<div className="text-lg font-semibold text-[#111111]">
|
||||
历史洗图记录
|
||||
</div>
|
||||
<div className="text-sm text-white/55">
|
||||
<div className="text-sm text-[#666666]">
|
||||
保留最近的 AI洗图批次,支持一键回看并继续发送结果到画布。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.05] px-3 py-1 text-xs text-white/65">
|
||||
<div className="rounded-full border border-[#ebebeb] bg-[#fafafa] px-3 py-1 text-xs text-[#666666]">
|
||||
最近 {visibleHistoryBatches.length} 条
|
||||
</div>
|
||||
</div>
|
||||
@ -1349,7 +1391,7 @@ export function AiWashWorkspace({
|
||||
const cover = entry.items[0];
|
||||
return (
|
||||
<div
|
||||
className="min-w-[260px] max-w-[260px] rounded-[22px] border border-white/10 bg-white/[0.03] p-3"
|
||||
className="min-w-[260px] max-w-[260px] rounded-[22px] border border-[#ececec] bg-[#fbfbfb] p-3"
|
||||
key={entry.id}
|
||||
>
|
||||
<button
|
||||
@ -1357,7 +1399,7 @@ export function AiWashWorkspace({
|
||||
onClick={() => handleLoadHistoryBatch(entry)}
|
||||
type="button"
|
||||
>
|
||||
<div className="overflow-hidden rounded-[18px] border border-white/10 bg-black/20">
|
||||
<div className="overflow-hidden rounded-[18px] border border-[#ececec] bg-white">
|
||||
<img
|
||||
alt={cover?.title || "历史洗图结果"}
|
||||
className="aspect-[4/3] w-full object-cover"
|
||||
@ -1369,10 +1411,10 @@ export function AiWashWorkspace({
|
||||
</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-white">
|
||||
<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-white/52">
|
||||
<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",
|
||||
@ -1382,7 +1424,7 @@ export function AiWashWorkspace({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.05] px-2 py-1 text-[11px] text-white/72">
|
||||
<div className="rounded-full border border-[#ebebeb] bg-white px-2 py-1 text-[11px] text-[#666666]">
|
||||
{entry.items.length} 张
|
||||
</div>
|
||||
</div>
|
||||
@ -1390,18 +1432,18 @@ export function AiWashWorkspace({
|
||||
<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-white/10 bg-white/[0.04] px-2 py-1 text-[11px] text-white/60">
|
||||
<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-emerald-400/20 bg-emerald-400/10 px-2 py-1 text-[11px] text-emerald-100">
|
||||
<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-white/10 bg-white/[0.04] text-white/70 transition hover:bg-white/[0.1] hover:text-white"
|
||||
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);
|
||||
@ -1417,7 +1459,7 @@ export function AiWashWorkspace({
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-[22px] border border-dashed border-white/10 bg-white/[0.02] px-5 py-8 text-center text-sm text-white/48">
|
||||
<div className="rounded-[22px] border border-dashed border-[#dcdcdc] bg-[#fafafa] px-5 py-8 text-center text-sm text-[#8a8a8a]">
|
||||
还没有历史洗图记录,生成一次后会自动出现在这里。
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user