feat(intake): 重写 AI 白底图工作区界面
This commit is contained in:
parent
33c2e2899f
commit
562cf5bba3
@ -1,15 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { AlertCircle, ImageIcon, Sparkles, UploadCloud, X } from "lucide-react";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle2,
|
||||
ImageIcon,
|
||||
Maximize2,
|
||||
RefreshCcw,
|
||||
Sparkles,
|
||||
UploadCloud,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import Select from "@/components/ui/Select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
AI_WHITE_BACKGROUND_VIEW_KEYS,
|
||||
AI_WHITE_BACKGROUND_VIEW_LABELS,
|
||||
type AiWhiteBackgroundTargetFinishOption,
|
||||
type AiWhiteBackgroundSessionStatus,
|
||||
type AiWhiteBackgroundTargetFinishOption,
|
||||
type AiWhiteBackgroundUploadManifest,
|
||||
type AiWhiteBackgroundViewKey,
|
||||
} from "./nonIntegratedAiWhiteBackground";
|
||||
@ -74,31 +83,36 @@ function getSessionStatusMeta(status: AiWhiteBackgroundSessionStatus | null) {
|
||||
if (status === "uploaded") {
|
||||
return {
|
||||
label: "已上传",
|
||||
className: "bg-[#eef7f2] text-[#2f6b51]",
|
||||
className: "bg-[#e6f3fb] text-[#39565d]",
|
||||
dotClassName: "bg-[#47646b]",
|
||||
};
|
||||
}
|
||||
if (status === "generated") {
|
||||
return {
|
||||
label: "已生成",
|
||||
className: "bg-black/[0.035] text-apple-label-primary",
|
||||
className: "bg-[#eef7f2] text-[#2f6b51]",
|
||||
dotClassName: "bg-[#2f6b51]",
|
||||
};
|
||||
}
|
||||
if (status === "upload_failed") {
|
||||
return {
|
||||
label: "上传失败",
|
||||
className: "bg-[#fff3f5] text-[#a5485a]",
|
||||
className: "bg-[#fff1ef] text-[#9a403d]",
|
||||
dotClassName: "bg-[#a83836]",
|
||||
};
|
||||
}
|
||||
if (status === "draft") {
|
||||
return {
|
||||
label: "待生成",
|
||||
className: "bg-black/[0.035] text-apple-label-secondary",
|
||||
className: "bg-[#f1f4f5] text-[#5a6062]",
|
||||
dotClassName: "bg-[#8a9496]",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: "未开始",
|
||||
className: "bg-black/[0.035] text-apple-label-secondary",
|
||||
className: "bg-[#f1f4f5] text-[#5a6062]",
|
||||
dotClassName: "bg-[#adb3b5]",
|
||||
};
|
||||
}
|
||||
|
||||
@ -106,7 +120,8 @@ function getManifestStatusMeta(manifest: AiWhiteBackgroundUploadManifest | null)
|
||||
if (!manifest) {
|
||||
return {
|
||||
label: "未上传素材库",
|
||||
className: "bg-black/[0.035] text-apple-label-secondary",
|
||||
className: "bg-[#f1f4f5] text-[#5a6062]",
|
||||
dotClassName: "bg-[#adb3b5]",
|
||||
};
|
||||
}
|
||||
|
||||
@ -114,30 +129,35 @@ function getManifestStatusMeta(manifest: AiWhiteBackgroundUploadManifest | null)
|
||||
return {
|
||||
label: "素材库生效中",
|
||||
className: "bg-[#eef7f2] text-[#2f6b51]",
|
||||
dotClassName: "bg-[#2f6b51]",
|
||||
};
|
||||
}
|
||||
if (manifest.status === "cleanup_pending") {
|
||||
return {
|
||||
label: "清理中",
|
||||
className: "bg-[#fff7e8] text-[#8a6538]",
|
||||
className: "bg-[#fff7e8] text-[#725737]",
|
||||
dotClassName: "bg-[#b6905d]",
|
||||
};
|
||||
}
|
||||
if (manifest.status === "cleaned") {
|
||||
return {
|
||||
label: "已清理",
|
||||
className: "bg-black/[0.035] text-apple-label-secondary",
|
||||
className: "bg-[#f1f4f5] text-[#5a6062]",
|
||||
dotClassName: "bg-[#8a9496]",
|
||||
};
|
||||
}
|
||||
if (manifest.status === "cleanup_failed") {
|
||||
return {
|
||||
label: "清理失败",
|
||||
className: "bg-[#fff3f5] text-[#a5485a]",
|
||||
className: "bg-[#fff1ef] text-[#9a403d]",
|
||||
dotClassName: "bg-[#a83836]",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: "待人工复核",
|
||||
className: "bg-[#fff7e8] text-[#8a6538]",
|
||||
className: "bg-[#fff7e8] text-[#725737]",
|
||||
dotClassName: "bg-[#b6905d]",
|
||||
};
|
||||
}
|
||||
|
||||
@ -156,6 +176,62 @@ function getTargetFinishLabel(finish: AiWhiteBackgroundTargetFinishOption) {
|
||||
.join(" · ");
|
||||
}
|
||||
|
||||
function StatChip({
|
||||
label,
|
||||
value,
|
||||
tone = "neutral",
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
tone?: "neutral" | "success" | "warning";
|
||||
}) {
|
||||
return (
|
||||
<div className="grid min-w-[86px] gap-0.5 rounded-[6px] bg-white/72 px-3 py-2 shadow-[0_1px_0_rgba(173,179,181,0.18)] ring-1 ring-[#adb3b5]/15">
|
||||
<span className="sr-only">
|
||||
{label} {value}
|
||||
</span>
|
||||
<span className="text-[10px] font-black uppercase tracking-[0.16em] text-[#5a6062]/64">
|
||||
{label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-mono text-[15px] font-black leading-5",
|
||||
tone === "success" && "text-[#2f6b51]",
|
||||
tone === "warning" && "text-[#725737]",
|
||||
tone === "neutral" && "text-[#2d3335]",
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusPill({
|
||||
label,
|
||||
value,
|
||||
className,
|
||||
dotClassName,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
className: string;
|
||||
dotClassName: string;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-7 items-center gap-2 rounded-[6px] px-2.5 text-[11px] font-bold",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className={cn("h-1.5 w-1.5 rounded-full", dotClassName)} />
|
||||
<span className="text-[#5a6062]/72">{label}</span>
|
||||
<span className="text-current">{value}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AiWhiteBackgroundImagePreviewDialog({
|
||||
src,
|
||||
alt,
|
||||
@ -167,29 +243,34 @@ function AiWhiteBackgroundImagePreviewDialog({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[70] flex items-center justify-center bg-black/65 p-4"
|
||||
className="fixed inset-0 z-[70] flex items-center justify-center bg-[#0c0f10]/72 p-4 backdrop-blur-[10px]"
|
||||
role="dialog"
|
||||
aria-label={alt}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="relative flex max-h-[88vh] w-full max-w-5xl flex-col rounded-[18px] bg-white p-4 shadow-[0_28px_80px_rgba(15,23,42,0.32)]"
|
||||
className="relative flex max-h-[88vh] w-full max-w-5xl flex-col overflow-hidden rounded-[8px] bg-[#f8f9fa] shadow-[0_24px_72px_rgba(12,15,16,0.35)] ring-1 ring-white/18"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<p className="min-w-0 truncate text-sm font-apple-medium text-[#2d3335]">
|
||||
{alt}
|
||||
</p>
|
||||
<div className="flex h-12 items-center justify-between gap-3 bg-white/82 px-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-[#5a6062]/62">
|
||||
Source Preview
|
||||
</p>
|
||||
<p className="truncate text-[13px] font-black text-[#2d3335]">
|
||||
{alt}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="关闭预览"
|
||||
onClick={onClose}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-black/[0.04] text-[#5a6062] transition hover:bg-black/[0.08] hover:text-[#2d3335]"
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[6px] bg-[#ebeef0] text-[#5a6062] transition hover:bg-[#dee3e6] hover:text-[#2d3335]"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-[14px] bg-[#f5f6f7]">
|
||||
<div className="m-3 overflow-hidden rounded-[7px] bg-[#ebeef0]">
|
||||
{src.trim() ? (
|
||||
<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
@ -200,7 +281,7 @@ function AiWhiteBackgroundImagePreviewDialog({
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex min-h-[320px] items-center justify-center px-6 text-sm text-[#5a6062]">
|
||||
<div className="flex min-h-[320px] items-center justify-center px-6 text-sm font-semibold text-[#5a6062]">
|
||||
原图加载失败
|
||||
</div>
|
||||
)}
|
||||
@ -221,7 +302,7 @@ function AiWhiteBackgroundCardImage({
|
||||
}) {
|
||||
if (!file.dataUrl.trim()) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center px-4 text-[11px] text-[#5a6062]/72">
|
||||
<div className="flex h-full w-full items-center justify-center px-4 text-[11px] font-semibold text-[#5a6062]/72">
|
||||
源图加载失败
|
||||
</div>
|
||||
);
|
||||
@ -304,153 +385,148 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-full bg-black/[0.035] px-2.5 py-1 text-[11px] font-medium text-[#2d3335]">
|
||||
{styleLabel}
|
||||
</span>
|
||||
<span className="rounded-full bg-black/[0.035] px-2.5 py-1 font-mono text-[11px] font-semibold text-[#5a6062]">
|
||||
{styleKey}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2.5 py-1 text-[11px] font-medium",
|
||||
sessionMeta.className,
|
||||
)}
|
||||
>
|
||||
会话:{sessionMeta.label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2.5 py-1 text-[11px] font-medium",
|
||||
manifestMeta.className,
|
||||
)}
|
||||
>
|
||||
素材库:{manifestMeta.label}
|
||||
</span>
|
||||
<span className="rounded-full bg-black/[0.035] px-2.5 py-1 text-[11px] text-[#5a6062]">
|
||||
可生成 {availableSourceViewCount}/8
|
||||
</span>
|
||||
<span className="rounded-full bg-black/[0.035] px-2.5 py-1 text-[11px] text-[#5a6062]">
|
||||
已生成 {generatedCount}/8
|
||||
</span>
|
||||
<span className="rounded-full bg-black/[0.035] px-2.5 py-1 text-[11px] text-[#5a6062]">
|
||||
已上传 {uploadedCount}/8
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden rounded-[8px] bg-[#f8f9fa] text-[#2d3335]">
|
||||
<header className="shrink-0 bg-[#f1f4f5] px-3 py-3 sm:px-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-[220px] flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-[5px] bg-[#47646b] px-2 py-1 text-[10px] font-black uppercase tracking-[0.16em] text-[#eefbff]">
|
||||
AI White Studio
|
||||
</span>
|
||||
<StatusPill
|
||||
label="会话"
|
||||
value={sessionMeta.label}
|
||||
className={sessionMeta.className}
|
||||
dotClassName={sessionMeta.dotClassName}
|
||||
/>
|
||||
<StatusPill
|
||||
label="素材库"
|
||||
value={manifestMeta.label}
|
||||
className={manifestMeta.className}
|
||||
dotClassName={manifestMeta.dotClassName}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<h2 className="text-[18px] font-black leading-6 tracking-[-0.01em] text-[#2d3335]">
|
||||
AI 白底图工坊
|
||||
</h2>
|
||||
<span className="text-[12px] font-bold text-[#5a6062]">
|
||||
{styleLabel}
|
||||
</span>
|
||||
<span className="rounded-[4px] bg-white/72 px-2 py-0.5 font-mono text-[11px] font-black text-[#5a6062] ring-1 ring-[#adb3b5]/15">
|
||||
{styleKey}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="ai-white-background-grid"
|
||||
className="grid min-h-[220px] flex-1 grid-cols-3 grid-rows-3 gap-2"
|
||||
>
|
||||
{AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => {
|
||||
const generatedFile = generatedFiles[viewKey];
|
||||
const sourceFile = sourceFilesByView[viewKey];
|
||||
const sourceReady = sourceReadyByView[viewKey];
|
||||
const displayedImageFile = isImageUploadFile(generatedFile)
|
||||
? generatedFile
|
||||
: isImageUploadFile(sourceFile)
|
||||
? sourceFile
|
||||
: null;
|
||||
const isShowingGeneratedFile =
|
||||
generatedFile != null && displayedImageFile?.id === generatedFile.id;
|
||||
const previewLabel = isShowingGeneratedFile
|
||||
? `${AI_WHITE_BACKGROUND_VIEW_LABELS[viewKey]}原图`
|
||||
: `${AI_WHITE_BACKGROUND_VIEW_LABELS[viewKey]}源图`;
|
||||
const hoverPreviewLabel = isShowingGeneratedFile
|
||||
? "点击查看原图"
|
||||
: "点击查看源图";
|
||||
const cardStatusLabel = generatedFile
|
||||
? "已生成"
|
||||
: sourceReady
|
||||
? "可生成"
|
||||
: "缺源图";
|
||||
const isViewGenerating = generatingViewKeySet.has(viewKey);
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<StatChip
|
||||
label="可生成"
|
||||
value={`${availableSourceViewCount}/8`}
|
||||
tone={availableSourceViewCount > 0 ? "success" : "warning"}
|
||||
/>
|
||||
<StatChip
|
||||
label="已生成"
|
||||
value={`${generatedCount}/8`}
|
||||
tone={generatedCount > 0 ? "success" : "neutral"}
|
||||
/>
|
||||
<StatChip
|
||||
label="已上传"
|
||||
value={`${uploadedCount}/8`}
|
||||
tone={uploadedCount > 0 ? "success" : "neutral"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
return (
|
||||
<article
|
||||
key={viewKey}
|
||||
className={cn(
|
||||
"group flex min-h-0 h-full flex-col rounded-[16px] p-2.5 transition shadow-[0_8px_18px_rgba(20,24,28,0.03)]",
|
||||
generatedFile
|
||||
? "bg-white"
|
||||
: sourceReady
|
||||
? "bg-white/82"
|
||||
: "bg-black/[0.02]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="truncate text-[12px] font-apple-semibold text-[#2d3335]">
|
||||
{AI_WHITE_BACKGROUND_VIEW_LABELS[viewKey]}
|
||||
</p>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||
generatedFile
|
||||
? "bg-[#eef7f2] text-[#2f6b51]"
|
||||
: sourceReady
|
||||
? "bg-black/[0.035] text-apple-label-secondary"
|
||||
: "bg-white/72 text-apple-label-tertiary",
|
||||
)}
|
||||
>
|
||||
{cardStatusLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid min-h-0 flex-1 gap-3 overflow-hidden p-3 xl:grid-cols-[minmax(0,1fr)_336px]">
|
||||
<section className="flex min-h-0 min-w-0 flex-col overflow-hidden rounded-[8px] bg-[#f1f4f5] p-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.62)]">
|
||||
<div className="mb-3 flex shrink-0 items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-[#5a6062]/64">
|
||||
Generated Views
|
||||
</p>
|
||||
<p className="truncate text-[13px] font-black text-[#2d3335]">
|
||||
九视图生成面板
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-[5px] bg-white/72 px-2.5 py-1 font-mono text-[11px] font-black text-[#5a6062] ring-1 ring-[#adb3b5]/15">
|
||||
{generatedCount}/8
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<div
|
||||
data-testid="ai-white-background-grid"
|
||||
className="grid min-h-0 flex-1 auto-rows-[minmax(168px,1fr)] grid-cols-1 gap-2 overflow-y-auto pr-1 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4"
|
||||
>
|
||||
{AI_WHITE_BACKGROUND_VIEW_KEYS.map((viewKey) => {
|
||||
const generatedFile = generatedFiles[viewKey];
|
||||
const sourceFile = sourceFilesByView[viewKey];
|
||||
const sourceReady = sourceReadyByView[viewKey];
|
||||
const displayedImageFile = isImageUploadFile(generatedFile)
|
||||
? generatedFile
|
||||
: isImageUploadFile(sourceFile)
|
||||
? sourceFile
|
||||
: null;
|
||||
const isShowingGeneratedFile =
|
||||
generatedFile != null && displayedImageFile?.id === generatedFile.id;
|
||||
const previewLabel = isShowingGeneratedFile
|
||||
? `${AI_WHITE_BACKGROUND_VIEW_LABELS[viewKey]}原图`
|
||||
: `${AI_WHITE_BACKGROUND_VIEW_LABELS[viewKey]}源图`;
|
||||
const hoverPreviewLabel = isShowingGeneratedFile
|
||||
? "点击查看原图"
|
||||
: "点击查看源图";
|
||||
const cardStatusLabel = generatedFile
|
||||
? "已生成"
|
||||
: sourceReady
|
||||
? "可生成"
|
||||
: "缺源图";
|
||||
const isViewGenerating = generatingViewKeySet.has(viewKey);
|
||||
|
||||
return (
|
||||
<article
|
||||
key={viewKey}
|
||||
className={cn(
|
||||
"mt-2 flex aspect-[5/4] min-h-0 items-center justify-center overflow-hidden rounded-[12px]",
|
||||
displayedImageFile
|
||||
? "bg-white"
|
||||
: "bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.96),rgba(243,245,246,0.9))]",
|
||||
"group flex min-h-0 flex-col rounded-[7px] bg-white/92 p-2.5 shadow-[0_4px_20px_rgba(45,51,53,0.055)] ring-1 ring-[#adb3b5]/15 transition",
|
||||
generatedFile && "bg-white",
|
||||
!generatedFile && sourceReady && "bg-[#ffffff]/86",
|
||||
!generatedFile && !sourceReady && "bg-[#ebeef0]/72",
|
||||
)}
|
||||
>
|
||||
{displayedImageFile ? (
|
||||
<button
|
||||
type="button"
|
||||
className="relative h-full w-full cursor-zoom-in overflow-hidden rounded-[12px] text-left"
|
||||
aria-label={`查看${previewLabel}`}
|
||||
onClick={() =>
|
||||
setPreviewImage({
|
||||
src: displayedImageFile.dataUrl,
|
||||
alt: previewLabel,
|
||||
})
|
||||
}
|
||||
>
|
||||
<AiWhiteBackgroundCardImage
|
||||
file={displayedImageFile}
|
||||
alt={displayedImageFile.name}
|
||||
className="h-full w-full object-contain transition duration-200 group-hover:scale-[1.01]"
|
||||
/>
|
||||
{isShowingGeneratedFile ? null : (
|
||||
<span className="pointer-events-none absolute left-3 top-3 rounded-full bg-[rgba(255,255,255,0.92)] px-2 py-0.5 text-[10px] font-medium text-[#725737] shadow-[0_6px_16px_rgba(20,24,28,0.06)]">
|
||||
源图
|
||||
</span>
|
||||
)}
|
||||
<span className="pointer-events-none absolute inset-x-3 bottom-3 rounded-full bg-white/88 px-2.5 py-1 text-center text-[10px] font-medium text-[#2d3335] opacity-0 shadow-[0_8px_18px_rgba(20,24,28,0.08)] transition group-hover:opacity-100">
|
||||
{hoverPreviewLabel}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center justify-center px-4">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-white/92 ring-1 ring-black/[0.05]">
|
||||
{sourceReady ? (
|
||||
<Sparkles className="h-4.5 w-4.5 text-[#5a6062]/62" />
|
||||
) : (
|
||||
<ImageIcon className="h-4.5 w-4.5 text-[#5a6062]/42" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[12px] font-black text-[#2d3335]">
|
||||
{AI_WHITE_BACKGROUND_VIEW_LABELS[viewKey]}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[10px] font-semibold text-[#5a6062]/58">
|
||||
{isShowingGeneratedFile ? "Generated output" : "Source reference"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-5 shrink-0 items-center rounded-[4px] px-1.5 text-[10px] font-bold",
|
||||
generatedFile && "bg-[#eef7f2] text-[#2f6b51]",
|
||||
!generatedFile && sourceReady && "bg-[#e6f3fb] text-[#39565d]",
|
||||
!generatedFile && !sourceReady && "bg-[#f1f4f5] text-[#8a9496]",
|
||||
)}
|
||||
>
|
||||
{cardStatusLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{displayedImageFile ? (
|
||||
<div className="mt-2 flex min-h-[28px] items-start justify-between gap-2 text-[10px] leading-4 text-[#5a6062]/45">
|
||||
<span className="line-clamp-2 flex-1">{displayedImageFile.name}</span>
|
||||
{isShowingGeneratedFile ? (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-2 flex min-h-0 flex-1 items-center justify-center overflow-hidden rounded-[6px]",
|
||||
displayedImageFile
|
||||
? "bg-[#f8f9fa]"
|
||||
: "bg-[linear-gradient(135deg,#ffffff_0%,#ebeef0_100%)]",
|
||||
)}
|
||||
>
|
||||
{displayedImageFile ? (
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded-full bg-black/[0.035] px-2 py-0.5 text-[10px] text-[#2d3335] transition hover:bg-black/[0.06]"
|
||||
className="relative h-full min-h-[106px] w-full cursor-zoom-in overflow-hidden rounded-[6px] text-left"
|
||||
aria-label={`查看${previewLabel}`}
|
||||
onClick={() =>
|
||||
setPreviewImage({
|
||||
src: displayedImageFile.dataUrl,
|
||||
@ -458,182 +534,255 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
|
||||
})
|
||||
}
|
||||
>
|
||||
查看原图
|
||||
<AiWhiteBackgroundCardImage
|
||||
file={displayedImageFile}
|
||||
alt={displayedImageFile.name}
|
||||
className="h-full w-full object-contain transition duration-200 group-hover:scale-[1.012]"
|
||||
/>
|
||||
<span className="pointer-events-none absolute left-2 top-2 inline-flex h-6 items-center gap-1 rounded-[5px] bg-white/88 px-2 text-[10px] font-black text-[#2d3335] shadow-[0_4px_12px_rgba(45,51,53,0.08)]">
|
||||
{isShowingGeneratedFile ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-[#2f6b51]" />
|
||||
) : (
|
||||
<ImageIcon className="h-3 w-3 text-[#725737]" />
|
||||
)}
|
||||
{isShowingGeneratedFile ? "生成图" : "源图"}
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-x-2 bottom-2 inline-flex h-7 items-center justify-center gap-1 rounded-[5px] bg-[#0c0f10]/72 px-2 text-[10px] font-bold text-white opacity-0 backdrop-blur transition group-hover:opacity-100">
|
||||
<Maximize2 className="h-3 w-3" />
|
||||
{hoverPreviewLabel}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<span className="shrink-0 rounded-full bg-[#f5efe5] px-2 py-0.5 text-[10px] text-[#7b6241]">
|
||||
源图
|
||||
</span>
|
||||
<div className="flex min-h-[106px] items-center justify-center px-4">
|
||||
<div className="grid place-items-center gap-2 text-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-[6px] bg-white/78 text-[#5a6062] shadow-[0_4px_16px_rgba(45,51,53,0.05)] ring-1 ring-[#adb3b5]/15">
|
||||
{sourceReady ? (
|
||||
<Sparkles className="h-4.5 w-4.5" />
|
||||
) : (
|
||||
<ImageIcon className="h-4.5 w-4.5 opacity-60" />
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] font-semibold text-[#5a6062]/62">
|
||||
{sourceReady ? "等待生成" : "等待补源图"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 min-h-[28px]" />
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
variant={generatedFile ? "gray" : "filled"}
|
||||
className="mt-2 h-[30px] w-full rounded-[10px] text-[11px]"
|
||||
loading={isViewGenerating}
|
||||
disabled={
|
||||
!canOperateAiWhiteBackground ||
|
||||
!selectedBarcode ||
|
||||
!sourceReady ||
|
||||
targetFinishTextureMissing ||
|
||||
isBulkGenerating ||
|
||||
isViewGenerating
|
||||
}
|
||||
onClick={() => void onGenerate([viewKey])}
|
||||
>
|
||||
{generatedFile ? "重生成" : sourceReady ? "生成" : "待补源图"}
|
||||
</Button>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
<div className="mt-2 min-h-[34px]">
|
||||
{displayedImageFile ? (
|
||||
<div className="flex items-start justify-between gap-2 text-[10px] leading-4 text-[#5a6062]/58">
|
||||
<span className="line-clamp-2 min-w-0 flex-1 font-semibold">
|
||||
{displayedImageFile.name}
|
||||
</span>
|
||||
{isShowingGeneratedFile ? (
|
||||
<button
|
||||
type="button"
|
||||
className="shrink-0 rounded-[4px] bg-[#f1f4f5] px-2 py-0.5 text-[10px] font-black text-[#2d3335] transition hover:bg-[#e5e9eb]"
|
||||
onClick={() =>
|
||||
setPreviewImage({
|
||||
src: displayedImageFile.dataUrl,
|
||||
alt: previewLabel,
|
||||
})
|
||||
}
|
||||
>
|
||||
原图
|
||||
</button>
|
||||
) : (
|
||||
<span className="shrink-0 rounded-[4px] bg-[#fff7e8] px-2 py-0.5 text-[10px] font-black text-[#725737]">
|
||||
源图
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
variant={generatedFile ? "gray" : "filled"}
|
||||
className="mt-2 h-8 w-full !min-h-8 rounded-[6px] px-2 text-[11px] font-black"
|
||||
loading={isViewGenerating}
|
||||
disabled={
|
||||
!canOperateAiWhiteBackground ||
|
||||
!selectedBarcode ||
|
||||
!sourceReady ||
|
||||
targetFinishTextureMissing ||
|
||||
isBulkGenerating ||
|
||||
isViewGenerating
|
||||
}
|
||||
onClick={() => void onGenerate([viewKey])}
|
||||
>
|
||||
{generatedFile ? (
|
||||
<>
|
||||
<RefreshCcw className="h-3.5 w-3.5" />
|
||||
重新生成
|
||||
</>
|
||||
) : sourceReady ? (
|
||||
<>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
生成
|
||||
</>
|
||||
) : (
|
||||
"待补源图"
|
||||
)}
|
||||
</Button>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside
|
||||
data-testid="ai-white-background-control-card"
|
||||
className="flex min-h-0 h-full flex-col rounded-[16px] bg-[linear-gradient(180deg,rgba(255,255,255,0.98),rgba(245,247,248,0.92))] p-3 shadow-[0_8px_18px_rgba(20,24,28,0.03)]"
|
||||
className="flex min-h-0 flex-col overflow-hidden rounded-[8px] bg-white/92 shadow-[0_4px_20px_rgba(45,51,53,0.06)] ring-1 ring-[#adb3b5]/15"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-[12px] font-apple-semibold text-[#2d3335]">
|
||||
<div className="border-b border-[#adb3b5]/15 bg-[#f8f9fa] px-4 py-3">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-[#5a6062]/64">
|
||||
Inspector
|
||||
</p>
|
||||
<div className="mt-1 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-[14px] font-black text-[#2d3335]">
|
||||
AI 白底图
|
||||
</p>
|
||||
<p className="mt-1 text-[10px] leading-4 text-[#5a6062]/72">
|
||||
<p className="mt-1 text-[11px] font-semibold leading-4 text-[#5a6062]/72">
|
||||
{statusText}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||
"inline-flex h-6 shrink-0 items-center rounded-[5px] px-2 font-mono text-[11px] font-black",
|
||||
manifestMeta.className,
|
||||
)}
|
||||
>
|
||||
{uploadedCount}/8
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[10px] font-black uppercase tracking-[0.16em] text-[#5a6062]/45">
|
||||
目标条码
|
||||
</label>
|
||||
<select
|
||||
data-testid="ai-white-background-barcode"
|
||||
value={selectedBarcode}
|
||||
onChange={(event) =>
|
||||
onSelectedBarcodeChange(event.target.value.trim().toUpperCase())
|
||||
}
|
||||
disabled={
|
||||
!canOperateAiWhiteBackground ||
|
||||
barcodeOptions.length === 0
|
||||
}
|
||||
className="h-9 w-full rounded-[10px] bg-white/88 px-3 text-sm text-[#2d3335] outline-none ring-1 ring-black/[0.05] transition focus:ring-black/[0.12] disabled:cursor-not-allowed disabled:text-[#5a6062]/35"
|
||||
>
|
||||
{barcodeOptions.length === 0 ? (
|
||||
<option value="">当前没有可用条码</option>
|
||||
) : null}
|
||||
{barcodeOptions.map((option) => (
|
||||
<option key={option.barcode} value={option.barcode}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-end gap-2">
|
||||
<Select
|
||||
label="目标花色"
|
||||
searchable
|
||||
searchPlaceholder="搜索花色名称、分类或编码"
|
||||
emptyText="基础资料库暂无匹配花色"
|
||||
placeholder={
|
||||
defaultTargetColor.trim()
|
||||
? `不改色(默认:${defaultTargetColor.trim()})`
|
||||
: "不改色 / 选择目标花色"
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-3">
|
||||
<div className="grid gap-3">
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-[10px] font-black uppercase tracking-[0.18em] text-[#5a6062]/62">
|
||||
目标条码
|
||||
</label>
|
||||
<select
|
||||
data-testid="ai-white-background-barcode"
|
||||
value={selectedBarcode}
|
||||
onChange={(event) =>
|
||||
onSelectedBarcodeChange(event.target.value.trim().toUpperCase())
|
||||
}
|
||||
value={targetFinishId}
|
||||
onChange={(event) => onTargetFinishChange(event.target.value)}
|
||||
disabled={
|
||||
!canOperateAiWhiteBackground ||
|
||||
hasAnyGenerationInFlight ||
|
||||
targetFinishOptions.length === 0
|
||||
barcodeOptions.length === 0
|
||||
}
|
||||
options={targetFinishOptions.map((finish) => ({
|
||||
value: finish.id,
|
||||
label: getTargetFinishLabel(finish),
|
||||
}))}
|
||||
size="small"
|
||||
tone="obsidian"
|
||||
containerClassName="min-w-0 flex-1 space-y-1.5"
|
||||
className="h-9 min-h-9 rounded-[10px] bg-white/88 px-3 text-sm text-[#2d3335] ring-1 ring-black/[0.05] hover:bg-white focus:ring-black/[0.12] disabled:cursor-not-allowed"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mb-0 h-9 w-9 shrink-0 overflow-hidden rounded-[10px] bg-[#f3f4f6] ring-1 ring-black/[0.06]"
|
||||
style={{
|
||||
backgroundColor:
|
||||
!selectedTargetFinishPreviewUrl &&
|
||||
isHexColor(selectedTargetFinish?.color ?? effectiveTargetColor)
|
||||
? (selectedTargetFinish?.color ?? effectiveTargetColor)
|
||||
: "#f3f4f6",
|
||||
}}
|
||||
className="h-10 w-full rounded-[6px] bg-[#f1f4f5] px-3 text-[13px] font-semibold text-[#2d3335] outline-none ring-1 ring-[#adb3b5]/20 transition focus:bg-white focus:ring-[#47646b]/40 disabled:cursor-not-allowed disabled:text-[#5a6062]/35"
|
||||
>
|
||||
{selectedTargetFinishPreviewUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={selectedTargetFinishPreviewUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
{barcodeOptions.length === 0 ? (
|
||||
<option value="">当前没有可用条码</option>
|
||||
) : null}
|
||||
</span>
|
||||
{barcodeOptions.map((option) => (
|
||||
<option key={option.barcode} value={option.barcode}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{targetFinishDisabledReason ? (
|
||||
<p className="text-[10px] leading-4 text-[#9a6a32]">
|
||||
{targetFinishDisabledReason}
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-end gap-2">
|
||||
<Select
|
||||
label="目标花色"
|
||||
searchable
|
||||
searchPlaceholder="搜索花色名称、分类或编码"
|
||||
emptyText="基础资料库暂无匹配花色"
|
||||
placeholder={
|
||||
defaultTargetColor.trim()
|
||||
? `不改色(默认:${defaultTargetColor.trim()})`
|
||||
: "不改色 / 选择目标花色"
|
||||
}
|
||||
value={targetFinishId}
|
||||
onChange={(event) => onTargetFinishChange(event.target.value)}
|
||||
disabled={
|
||||
!canOperateAiWhiteBackground ||
|
||||
hasAnyGenerationInFlight ||
|
||||
targetFinishOptions.length === 0
|
||||
}
|
||||
options={targetFinishOptions.map((finish) => ({
|
||||
value: finish.id,
|
||||
label: getTargetFinishLabel(finish),
|
||||
}))}
|
||||
size="small"
|
||||
tone="obsidian"
|
||||
containerClassName="min-w-0 flex-1 space-y-1.5 [&>label]:mb-0 [&>label]:text-[10px] [&>label]:font-black [&>label]:uppercase [&>label]:tracking-[0.18em] [&>label]:text-[#5a6062]/62"
|
||||
className="h-10 min-h-10 rounded-[6px] bg-[#f1f4f5] px-3 text-[13px] font-semibold text-[#2d3335] ring-1 ring-[#adb3b5]/20 hover:bg-[#ebeef0] focus:ring-[#47646b]/40 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="mb-0 h-10 w-10 shrink-0 overflow-hidden rounded-[6px] bg-[#dee3e6] ring-1 ring-[#adb3b5]/22"
|
||||
style={{
|
||||
backgroundColor:
|
||||
!selectedTargetFinishPreviewUrl &&
|
||||
isHexColor(selectedTargetFinish?.color ?? effectiveTargetColor)
|
||||
? (selectedTargetFinish?.color ?? effectiveTargetColor)
|
||||
: "#dee3e6",
|
||||
}}
|
||||
>
|
||||
{selectedTargetFinishPreviewUrl ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={selectedTargetFinishPreviewUrl}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
{targetFinishDisabledReason ? (
|
||||
<p className="text-[11px] font-semibold leading-4 text-[#725737]">
|
||||
{targetFinishDisabledReason}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{operationDisabledReason ? (
|
||||
<div className="rounded-[6px] bg-[#f1f4f5] px-3 py-2 text-[11px] font-semibold leading-4 text-[#5a6062] ring-1 ring-[#adb3b5]/15">
|
||||
{operationDisabledReason}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{operationDisabledReason ? (
|
||||
<div className="rounded-[12px] border border-[#e0e4e5] bg-white/78 px-3 py-2 text-[10px] leading-4 text-[#5a6062]">
|
||||
{operationDisabledReason}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{missingSourceSlotLabels.length > 0 ? (
|
||||
<div className="rounded-[12px] bg-[#f7efe2] px-2.5 py-2 text-[#7c6748]">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#b6905d]" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-medium">待补角度</p>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{missingSourceSlotLabels.map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="rounded-full bg-white/72 px-1.5 py-0.5 text-[10px] leading-4 text-[#87653f]"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
{missingSourceSlotLabels.length > 0 ? (
|
||||
<div className="rounded-[6px] bg-[#fff7e8] px-3 py-2 text-[#725737] ring-1 ring-[#b6905d]/18">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-[#b6905d]" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-black">待补角度</p>
|
||||
<div className="mt-1.5 flex flex-wrap gap-1">
|
||||
{missingSourceSlotLabels.map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="rounded-[4px] bg-white/74 px-1.5 py-0.5 text-[10px] font-bold leading-4 text-[#725737]"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
) : null}
|
||||
|
||||
{!canUploadToMaterialLibrary && uploadDisabledReason ? (
|
||||
<div className="rounded-[12px] border border-[#f1d7c1] bg-[#fff8f2] px-3 py-2 text-[10px] leading-4 text-[#8a6538]">
|
||||
{uploadDisabledReason}
|
||||
</div>
|
||||
) : null}
|
||||
{!canUploadToMaterialLibrary && uploadDisabledReason ? (
|
||||
<div className="rounded-[6px] bg-[#fff7e8] px-3 py-2 text-[11px] font-semibold leading-4 text-[#725737] ring-1 ring-[#b6905d]/18">
|
||||
{uploadDisabledReason}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto grid gap-2 pt-3">
|
||||
<div className="grid shrink-0 gap-2 border-t border-[#adb3b5]/15 bg-[#f8f9fa] px-4 py-3">
|
||||
<Button
|
||||
type="button"
|
||||
size="small"
|
||||
@ -645,7 +794,7 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
|
||||
targetFinishTextureMissing ||
|
||||
hasAnyGenerationInFlight
|
||||
}
|
||||
className="h-9 rounded-[10px] text-[11px]"
|
||||
className="h-10 !min-h-10 rounded-[6px] text-[12px] font-black"
|
||||
onClick={() => void onGenerate()}
|
||||
>
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
@ -663,7 +812,7 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
|
||||
!canUploadToMaterialLibrary ||
|
||||
isUploading
|
||||
}
|
||||
className="h-9 rounded-[10px] text-[11px]"
|
||||
className="h-10 !min-h-10 rounded-[6px] text-[12px] font-black"
|
||||
onClick={() => void onUpload()}
|
||||
>
|
||||
<UploadCloud className="h-3.5 w-3.5" />
|
||||
@ -683,7 +832,7 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
|
||||
currentManifest.status === "cleanup_pending" ||
|
||||
cleaningManifestId === currentManifest.manifestId
|
||||
}
|
||||
className="h-8 rounded-[10px] text-[11px]"
|
||||
className="h-9 !min-h-9 rounded-[6px] text-[11px] font-black"
|
||||
onClick={() => void onCleanup(currentManifest)}
|
||||
>
|
||||
清理当前上传清单
|
||||
@ -692,6 +841,7 @@ export default function NonIntegratedAiWhiteBackgroundPanel({
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{previewImage ? (
|
||||
<AiWhiteBackgroundImagePreviewDialog
|
||||
src={previewImage.src}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user