feat: streamline agent detail selection workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-23 05:23:55 +08:00
parent dc1665d7ba
commit 467de8ff1e
3 changed files with 347 additions and 342 deletions

View File

@ -2247,6 +2247,198 @@ export function AgentGenerationWorkspaceDarkV2({
</div>
</motion.section>
{isDetailReplacementPreset ? (
<motion.section
className="relative overflow-hidden rounded-[28px] border border-white/12 bg-white/[0.04] shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.08 }}
>
<div className="border-b border-white/12 px-5 py-4">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<div className="text-sm font-semibold text-white">参考详情搜索</div>
<div className="text-xs text-white/40">
按 SPU 查询候选整套,并选择一套{detailReferenceSourceLabel}参考图进入下方结果区。
</div>
</div>
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/55">
当前已选 {detailReplacementReferenceCards.length} 张
</div>
</div>
</div>
<div className="space-y-4 p-5">
<div className="inline-flex rounded-full border border-emerald-400/35 bg-emerald-500/[0.10] px-3 py-1.5 text-xs text-emerald-200">
按SPU查询
</div>
<div className="flex flex-col gap-2 lg:flex-row">
<Input
value={detailReplacementQueryInput}
onChange={(e) =>
updateDetailReplacementState({
query: e.target.value,
queryType: "spu",
})
}
placeholder={`输入 SPU,查询任务整套中的${detailReferenceSourceLabel}参考图`}
className="h-12 rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30"
/>
<Button
type="button"
onClick={handleDetailReplacementSearch}
disabled={detailReplacementSearchState.isLoading}
className="h-12 rounded-[22px] bg-white/[0.04] px-5 text-sm text-white hover:bg-white/[0.08]"
>
{detailReplacementSearchState.isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
查询中
</>
) : (
`查询${detailReferenceSourceLabel}`
)}
</Button>
{detailReplacementReferenceCards.length > 0 ? (
<Button
type="button"
variant="outline"
onClick={handleClearDetailReferenceCards}
className="h-12 rounded-[22px] border-white/10 bg-white/[0.03] px-5 text-sm text-white hover:bg-white/[0.08]"
>
清空参考图
</Button>
) : null}
</div>
{(detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim()) ? (
<div className="rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-white/48">
当前参考图查询:
SPU /
{detailReplacementSearchState.lastQuery ||
detailReplacementQueryInput.trim().toUpperCase()}
{workflowStoreName ? (
<span className="ml-2 text-white/40">{`店铺 / ${workflowStoreName}`}</span>
) : null}
<span className="ml-2 text-emerald-200/70">
{`已接入真实${detailReferenceSourceLabel}参考图接口`}
</span>
</div>
) : null}
{detailReplacementCandidates.length > 0 ? (
<div>
<div className="mb-2 flex items-center justify-between gap-3">
<div className="text-xs text-white/42">
候选整套素材 {detailReplacementCandidates.length} 套
</div>
<div className="text-[11px] text-emerald-200/70">
默认载入最新一套,可随时切换
</div>
</div>
<div className="agent-dark-scrollbar grid max-h-[360px] grid-cols-1 gap-3 overflow-y-auto pr-1 xl:grid-cols-2">
{detailReplacementCandidates.map((candidate) => {
const isSelected = detailReplacementSelectedCandidateId === candidate.id;
const previewUrls = getCandidatePreviewUrls(
candidate,
detailReferenceTarget,
4
);
const primaryPreviewUrl =
previewUrls[0] || getCandidatePreviewUrl(candidate, detailReferenceTarget);
const uploadedAtText = (() => {
const raw = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null;
if (!raw || Number.isNaN(raw.getTime())) return "未记录";
return raw.toLocaleString("zh-CN", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
})();
const summaryBadges = [
`${detailReferenceSourceLabel}`,
`${getCandidateTargetCount(candidate, detailReferenceTarget)}张`,
candidate.categoryName || null,
].filter((item): item is string => Boolean(item));
return (
<button
key={candidate.id}
type="button"
onClick={() => handleSelectDetailReplacementCandidate(candidate.id)}
className={cn(
"group relative min-w-0 overflow-hidden rounded-2xl border p-2.5 text-left transition-all duration-300",
isSelected
? "border-emerald-400/40 bg-emerald-500/[0.08] shadow-[0_0_24px_rgba(16,185,129,0.14)]"
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]"
)}
>
<div className="flex items-stretch gap-3">
<div className="relative w-[88px] min-h-[112px] shrink-0 overflow-hidden rounded-[20px] border border-white/10 bg-black/30">
<div className="h-full w-full">
{primaryPreviewUrl ? (
<img
src={buildPreviewImageUrl(primaryPreviewUrl, 360)}
alt={candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="h-full w-full bg-[linear-gradient(180deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]" />
)}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/35 to-transparent" />
<div className="absolute left-1.5 top-1.5 rounded-full border border-white/10 bg-black/45 px-1.5 py-0.5 text-[9px] text-white/78">
{getCandidateTargetCount(candidate, detailReferenceTarget)}张
</div>
</div>
<div className="flex min-h-[112px] min-w-0 flex-1 flex-col justify-between pr-7">
<div className="line-clamp-2 text-[13px] font-semibold leading-5 text-white">
{candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{summaryBadges.map((badge, badgeIndex) => (
<span
key={`${candidate.id}-badge-${badgeIndex}-${badge}`}
className={cn(
"rounded-full border px-2 py-1 text-[9px] leading-none",
badgeIndex === 0
? "border-emerald-400/25 bg-emerald-500/[0.12] text-emerald-100"
: "border-white/10 bg-white/[0.04] text-white/62"
)}
>
{badge}
</span>
))}
</div>
<div className="mt-2 text-[10px] leading-4 text-white/45">
<div className="truncate">
场景名称:{candidate.sceneName || candidate.productModel || "未命名整套"}
</div>
<div className="truncate">
任务编号:{candidate.taskNo || "未记录"}
</div>
</div>
<div className="mt-2 flex flex-col gap-1 text-[10px] text-white/50">
<div className="truncate">店铺:{candidate.shopName || "未记录"}</div>
<div className="truncate">更新时间:{uploadedAtText}</div>
</div>
</div>
{isSelected ? (
<div className="absolute right-4 top-4 h-3 w-3 rounded-full bg-emerald-400 shadow-[0_0_0_4px_rgba(16,185,129,0.18),0_0_18px_rgba(52,211,153,0.45)]" />
) : null}
</div>
</button>
);
})}
</div>
</div>
) : null}
</div>
</motion.section>
) : null}
<motion.section
id="agent-anchor-results"
className={cn(
@ -2414,174 +2606,6 @@ export function AgentGenerationWorkspaceDarkV2({
</div>
</div>
<div className="mb-3">
<div className="inline-flex rounded-full border border-emerald-400/35 bg-emerald-500/[0.10] px-3 py-1.5 text-xs text-emerald-200">
按SPU查询
</div>
</div>
<div className="mb-3 flex flex-col gap-2 sm:flex-row">
<Input
value={detailReplacementQueryInput}
onChange={(e) =>
updateDetailReplacementState({
query: e.target.value,
queryType: "spu",
})
}
placeholder={`输入 SPU,查询任务整套中的${detailReferenceSourceLabel}参考图`}
className="h-12 rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30"
/>
<Button
type="button"
onClick={handleDetailReplacementSearch}
disabled={detailReplacementSearchState.isLoading}
className="h-12 rounded-[22px] bg-white/[0.04] px-5 text-sm text-white hover:bg-white/[0.08]"
>
{detailReplacementSearchState.isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
查询中
</>
) : (
`查询${detailReferenceSourceLabel}`
)}
</Button>
{detailReplacementReferenceCards.length > 0 ? (
<Button
type="button"
variant="outline"
onClick={handleClearDetailReferenceCards}
className="h-12 rounded-[22px] border-white/10 bg-white/[0.03] px-5 text-sm text-white hover:bg-white/[0.08]"
>
清空参考图
</Button>
) : null}
</div>
{(detailReplacementSearchState.lastQuery || detailReplacementQueryInput.trim()) ? (
<div className="mb-3 rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-white/48">
当前参考图查询:
SPU /
{detailReplacementSearchState.lastQuery ||
detailReplacementQueryInput.trim().toUpperCase()}
{workflowStoreName ? (
<span className="ml-2 text-white/40">{`店铺 / ${workflowStoreName}`}</span>
) : null}
<span className="ml-2 text-emerald-200/70">
{`已接入真实${detailReferenceSourceLabel}参考图接口`}
</span>
</div>
) : null}
{detailReplacementCandidates.length > 0 ? (
<div className="mb-3">
<div className="mb-2 flex items-center justify-between gap-3">
<div className="text-xs text-white/42">
候选整套素材 {detailReplacementCandidates.length} 套
</div>
<div className="text-[11px] text-emerald-200/70">
默认载入最新一套,可随时切换
</div>
</div>
<div className="agent-dark-scrollbar grid max-h-[360px] grid-cols-1 gap-3 overflow-y-auto pr-1 xl:grid-cols-2">
{detailReplacementCandidates.map((candidate) => {
const isSelected = detailReplacementSelectedCandidateId === candidate.id;
const previewUrls = getCandidatePreviewUrls(
candidate,
detailReferenceTarget,
4
);
const primaryPreviewUrl = previewUrls[0] || getCandidatePreviewUrl(candidate, detailReferenceTarget);
const uploadedAtText = (() => {
const raw = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null;
if (!raw || Number.isNaN(raw.getTime())) return "未记录";
return raw.toLocaleString("zh-CN", {
timeZone: "Asia/Shanghai",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
})();
const summaryBadges = [
`${detailReferenceSourceLabel}`,
`${getCandidateTargetCount(candidate, detailReferenceTarget)}张`,
candidate.categoryName || null,
].filter((item): item is string => Boolean(item));
return (
<button
key={candidate.id}
type="button"
onClick={() => handleSelectDetailReplacementCandidate(candidate.id)}
className={cn(
"group relative min-w-0 overflow-hidden rounded-2xl border p-2.5 text-left transition-all duration-300",
isSelected
? "border-emerald-400/40 bg-emerald-500/[0.08] shadow-[0_0_24px_rgba(16,185,129,0.14)]"
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]"
)}
>
<div className="flex items-stretch gap-3">
<div className="relative w-[88px] min-h-[112px] shrink-0 overflow-hidden rounded-[20px] border border-white/10 bg-black/30">
<div className="h-full w-full">
{primaryPreviewUrl ? (
<img
src={buildPreviewImageUrl(primaryPreviewUrl, 360)}
alt={candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="h-full w-full bg-[linear-gradient(180deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]" />
)}
</div>
<div className="absolute inset-0 bg-gradient-to-t from-black/35 to-transparent" />
<div className="absolute left-1.5 top-1.5 rounded-full border border-white/10 bg-black/45 px-1.5 py-0.5 text-[9px] text-white/78">
{getCandidateTargetCount(candidate, detailReferenceTarget)}张
</div>
</div>
<div className="flex min-h-[112px] min-w-0 flex-1 flex-col justify-between pr-7">
<div className="line-clamp-2 text-[13px] font-semibold leading-5 text-white">
{candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
</div>
<div className="mt-2 flex flex-wrap gap-1.5">
{summaryBadges.map((badge, badgeIndex) => (
<span
key={`${candidate.id}-badge-${badgeIndex}-${badge}`}
className={cn(
"rounded-full border px-2 py-1 text-[9px] leading-none",
badgeIndex === 0
? "border-emerald-400/25 bg-emerald-500/[0.12] text-emerald-100"
: "border-white/10 bg-white/[0.04] text-white/62"
)}
>
{badge}
</span>
))}
</div>
<div className="mt-2 text-[10px] leading-4 text-white/45">
<div className="truncate">
场景名称:{candidate.sceneName || candidate.productModel || "未命名整套"}
</div>
<div className="truncate">
任务编号:{candidate.taskNo || "未记录"}
</div>
</div>
<div className="mt-2 flex flex-col gap-1 text-[10px] text-white/50">
<div className="truncate">店铺:{candidate.shopName || "未记录"}</div>
<div className="truncate">更新时间:{uploadedAtText}</div>
</div>
</div>
{isSelected ? (
<div className="absolute right-4 top-4 h-3 w-3 rounded-full bg-emerald-400 shadow-[0_0_0_4px_rgba(16,185,129,0.18),0_0_18px_rgba(52,211,153,0.45)]" />
) : null}
</div>
</button>
);
})}
</div>
</div>
) : null}
<div className="grid grid-cols-1 gap-3">
{detailReplacementReferenceCards.length > 0 ? (
detailReplacementReferenceCards.map((card, index) => (

View File

@ -1,7 +1,7 @@
"use client";
import { useMemo } from "react";
import { Check, ImageIcon, Loader2, RefreshCw, Search } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Check, ChevronDown, ChevronUp, ImageIcon, Loader2, RefreshCw, Search } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -59,10 +59,19 @@ export function AgentWorkflowMaterialPicker({
theme = "light",
}: AgentWorkflowMaterialPickerProps) {
const isDark = theme === "dark";
const [isImagePreviewExpanded, setIsImagePreviewExpanded] = useState(true);
const [hasManualSelectionInteraction, setHasManualSelectionInteraction] = useState(false);
const selectedSet = useMemo(
() => new Set((selectedImageIds || []).map((item) => String(item || "").trim()).filter(Boolean)),
[selectedImageIds]
);
const recommendedImageIds = useMemo(
() =>
Array.isArray(result?.recommendedImageIds)
? result.recommendedImageIds.map((item) => String(item || "").trim()).filter(Boolean)
: [],
[result?.recommendedImageIds]
);
const selectedCount = result?.images
? result.images.filter((item) => selectedSet.has(item.id)).length
: 0;
@ -102,8 +111,31 @@ export function AgentWorkflowMaterialPicker({
].filter((item) => Boolean(item.value));
const remainingSelectableCount =
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
const hasSystemRecommendedSelection =
result?.queryType === "sku" &&
recommendedImageIds.length > 0 &&
selectedImageIds.length === recommendedImageIds.length &&
recommendedImageIds.every((id) => selectedSet.has(id));
const isAutoSelectedResult =
Boolean(hasSystemRecommendedSelection) && !hasManualSelectionInteraction;
const previewResetKey = useMemo(
() =>
JSON.stringify({
queryType: result?.queryType || "",
matchedSku: result?.matchedSku || "",
imageIds: result?.images?.map((image) => image.id) || [],
recommendedImageIds,
}),
[recommendedImageIds, result?.images, result?.matchedSku, result?.queryType]
);
useEffect(() => {
setHasManualSelectionInteraction(false);
setIsImagePreviewExpanded(!hasSystemRecommendedSelection);
}, [hasSystemRecommendedSelection, previewResetKey]);
const handleToggleImage = (imageId: string) => {
setHasManualSelectionInteraction(true);
const next = new Set(selectedImageIds);
if (next.has(imageId)) {
next.delete(imageId);
@ -118,6 +150,7 @@ export function AgentWorkflowMaterialPicker({
};
const handleSelectAll = () => {
setHasManualSelectionInteraction(true);
const allIds =
result?.images
?.filter((item) => Boolean(String(item.imageUrl || "").trim()))
@ -324,7 +357,10 @@ export function AgentWorkflowMaterialPicker({
type="button"
variant="outline"
size="sm"
onClick={() => onSelectedImageIdsChange([])}
onClick={() => {
setHasManualSelectionInteraction(true);
onSelectedImageIdsChange([]);
}}
disabled={selectedCount === 0 && !hasSelectableImages}
className={cn(
isDark &&
@ -429,91 +465,133 @@ export function AgentWorkflowMaterialPicker({
) : null}
{result.images?.length ? (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-4">
{result.images.map((image) => {
const selected = selectedSet.has(image.id);
const isPlaceholder = !String(image.imageUrl || "").trim();
return (
<button
key={image.id}
type="button"
onClick={() => {
if (isPlaceholder) return;
handleToggleImage(image.id);
}}
disabled={isPlaceholder}
<div className="space-y-3">
{isAutoSelectedResult ? (
<div
className={cn(
"group overflow-hidden rounded-[24px] border bg-background text-left transition-all",
isPlaceholder && "cursor-wait",
selected
? isDark
? "border-emerald-400/70 bg-white/[0.03] shadow-[0_0_0_1px_rgba(52,211,153,0.2)]"
: "border-primary shadow-[0_0_0_1px_rgba(59,130,246,0.18)]"
: isDark
? "border-white/12 bg-white/[0.02] hover:border-white/25 hover:shadow-sm"
: "border-border hover:border-primary/40 hover:shadow-sm"
"flex flex-wrap items-center justify-between gap-3 rounded-[20px] border px-3 py-2.5",
isDark
? "border-emerald-500/18 bg-emerald-500/[0.06] text-emerald-100"
: "border-emerald-200 bg-emerald-50 text-emerald-700"
)}
>
<div
<div className="text-xs leading-5">
系统已按权重自动选中 {selectedCount} 张 SKU 图,默认收起预览;需要检查或微调时再手动展开。
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsImagePreviewExpanded((current) => !current)}
className={cn(
"relative aspect-square overflow-hidden",
isDark ? "bg-white/[0.04]" : "bg-muted/40"
"shrink-0",
isDark &&
"border-white/12 bg-white/[0.04] text-white/82 hover:bg-white/[0.10] hover:text-white"
)}
>
{isPlaceholder ? (
<div className="flex h-full w-full flex-col items-center justify-center gap-3 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]">
<Loader2 className="h-6 w-6 animate-spin text-white/55" />
<div className="h-2 w-20 rounded-full bg-white/10" />
<div className="h-2 w-14 rounded-full bg-white/10" />
</div>
{isImagePreviewExpanded ? (
<>
<ChevronUp className="mr-2 h-4 w-4" />
收起预览
</>
) : (
<img
src={image.imageUrl}
alt={image.label}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
<>
<ChevronDown className="mr-2 h-4 w-4" />
展开预览
</>
)}
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-2 py-2">
<span className="rounded-full bg-black/60 px-2 py-1 text-[11px] text-white">
{isPlaceholder ? "加载中" : image.group === "fixed" ? "固定角度" : "其他图片"}
</span>
{!isPlaceholder ? (
<span
</Button>
</div>
) : null}
{(!isAutoSelectedResult || isImagePreviewExpanded) ? (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3 xl:grid-cols-4">
{result.images.map((image) => {
const selected = selectedSet.has(image.id);
const isPlaceholder = !String(image.imageUrl || "").trim();
return (
<button
key={image.id}
type="button"
onClick={() => {
if (isPlaceholder) return;
handleToggleImage(image.id);
}}
disabled={isPlaceholder}
className={cn(
"group overflow-hidden rounded-[24px] border bg-background text-left transition-all",
isPlaceholder && "cursor-wait",
selected
? isDark
? "border-emerald-400/70 bg-white/[0.03] shadow-[0_0_0_1px_rgba(52,211,153,0.2)]"
: "border-primary shadow-[0_0_0_1px_rgba(59,130,246,0.18)]"
: isDark
? "border-white/12 bg-white/[0.02] hover:border-white/25 hover:shadow-sm"
: "border-border hover:border-primary/40 hover:shadow-sm"
)}
>
<div
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border text-white transition-colors",
selected
? isDark
? "border-emerald-400 bg-emerald-400 text-black"
: "border-primary bg-primary"
: "border-white/60 bg-black/35 opacity-0 group-hover:opacity-100"
"relative aspect-square overflow-hidden",
isDark ? "bg-white/[0.04]" : "bg-muted/40"
)}
>
<Check className="h-3.5 w-3.5" />
</span>
) : null}
</div>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 via-black/25 to-transparent px-3 py-2 text-[11px] text-white">
{isPlaceholder ? "白底图加载中..." : image.label}
</div>
</div>
<div className="space-y-1 px-3 py-2">
<div className={cn("truncate text-xs font-medium", isDark ? "text-white/88" : "")}>
{isPlaceholder ? "正在获取 SKU 图片..." : image.fileName}
</div>
<div className={cn("text-[11px]", isDark ? "text-white/60" : "text-muted-foreground")}>
{isPlaceholder
? "已先展示预览卡位,图片会逐步加载出来。"
: `系统会先按权重自动选图,你也可以点击${selected ? "取消" : "调整"}。`}
</div>
</div>
</button>
);
})}
{isPlaceholder ? (
<div className="flex h-full w-full flex-col items-center justify-center gap-3 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.08),transparent_55%),linear-gradient(180deg,rgba(255,255,255,0.05),rgba(255,255,255,0.02))]">
<Loader2 className="h-6 w-6 animate-spin text-white/55" />
<div className="h-2 w-20 rounded-full bg-white/10" />
<div className="h-2 w-14 rounded-full bg-white/10" />
</div>
) : (
<img
src={image.imageUrl}
alt={image.label}
className="h-full w-full object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
event.currentTarget.style.display = "none";
}}
/>
)}
<div className="absolute inset-x-0 top-0 flex items-center justify-between px-2 py-2">
<span className="rounded-full bg-black/60 px-2 py-1 text-[11px] text-white">
{isPlaceholder ? "加载中" : image.group === "fixed" ? "固定角度" : "其他图片"}
</span>
{!isPlaceholder ? (
<span
className={cn(
"flex h-6 w-6 items-center justify-center rounded-full border text-white transition-colors",
selected
? isDark
? "border-emerald-400 bg-emerald-400 text-black"
: "border-primary bg-primary"
: "border-white/60 bg-black/35 opacity-0 group-hover:opacity-100"
)}
>
<Check className="h-3.5 w-3.5" />
</span>
) : null}
</div>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 via-black/25 to-transparent px-3 py-2 text-[11px] text-white">
{isPlaceholder ? "白底图加载中..." : image.label}
</div>
</div>
<div className="space-y-1 px-3 py-2">
<div className={cn("truncate text-xs font-medium", isDark ? "text-white/88" : "")}>
{isPlaceholder ? "正在获取 SKU 图片..." : image.fileName}
</div>
<div className={cn("text-[11px]", isDark ? "text-white/60" : "text-muted-foreground")}>
{isPlaceholder
? "已先展示预览卡位,图片会逐步加载出来。"
: `系统会先按权重自动选图,你也可以点击${selected ? "取消" : "调整"}。`}
</div>
</div>
</button>
);
})}
</div>
) : null}
</div>
) : null}
</div>

View File

@ -102,38 +102,6 @@ export function AgentWorkflowMaterialSelectionPanel({
typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0
? Math.max(productSelectionLimit - primarySelectedCount, secondarySelectedCount, 0)
: null;
const hasAnyQueryInput =
Boolean(String(inputs.primarySku || "").trim()) ||
Boolean(String(inputs.secondarySku || "").trim());
const hasSkuSelectionPending =
Boolean(materials.primary.result?.skuOptions?.length) ||
Boolean(materials.secondary.result?.skuOptions?.length);
const hasAnyMaterialResult =
Boolean(materials.primary.result?.images?.length) ||
Boolean(materials.secondary.result?.images?.length) ||
hasSkuSelectionPending;
const hasAnySelectedImages =
materials.primary.selectedImageIds.length > 0 || materials.secondary.selectedImageIds.length > 0;
const materialGuideSteps = [
{
badge: "01",
label: "输入型号",
description: "先选择按 SPU 或 SKU 搜索,再输入商品型号。",
status: hasAnyQueryInput ? "completed" : "current",
},
{
badge: "02",
label: "选择SKU",
description: "如果是 SPU 查询,先从返回的 SKU 列表里点选具体 SKU。",
status: hasSkuSelectionPending ? "current" : hasAnyMaterialResult ? "completed" : "pending",
},
{
badge: "03",
label: "自动选图",
description: "系统会按角度权重自动选图并映射到产品槽位,你也可以手动微调。",
status: hasAnySelectedImages ? "completed" : hasAnyMaterialResult ? "current" : "pending",
},
] as const;
useEffect(() => {
if (hasSecondaryMaterialContent) {
@ -273,71 +241,6 @@ export function AgentWorkflowMaterialSelectionPanel({
</div>
) : null}
<div
className={cn(
"space-y-3 rounded-[24px] border px-4 py-4",
isDark ? "border-white/10 bg-black/25" : "border-[#dbe3ff] bg-white"
)}
>
<div className="flex flex-col gap-1">
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "text-[#334155]")}>
选图流程
</div>
<div className={cn("text-xs leading-5", isDark ? "text-white/50" : "text-[#64748b]")}>
按这 3 步操作就能完成输入图准备,避免来回试错。
</div>
</div>
<div className="grid grid-cols-1 gap-3 xl:grid-cols-3">
{materialGuideSteps.map((step) => (
<div
key={step.badge}
className={cn(
"rounded-[20px] border px-4 py-3",
step.status === "completed"
? isDark
? "border-emerald-400/20 bg-emerald-500/[0.08]"
: "border-emerald-200 bg-emerald-50"
: step.status === "current"
? isDark
? "border-amber-400/20 bg-amber-500/[0.08]"
: "border-amber-200 bg-amber-50"
: isDark
? "border-white/10 bg-white/[0.03]"
: "border-[#dbe3ff] bg-[#f8faff]"
)}
>
<div className="flex items-center gap-2">
<span
className={cn(
"rounded-full px-2.5 py-1 text-[11px] font-semibold",
step.status === "completed"
? isDark
? "bg-emerald-300 text-black"
: "bg-emerald-600 text-white"
: step.status === "current"
? isDark
? "bg-amber-300 text-black"
: "bg-amber-500 text-white"
: isDark
? "bg-white/10 text-white/60"
: "bg-white text-[#64748b]"
)}
>
{step.badge}
</span>
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "text-[#334155]")}>
{step.label}
</div>
</div>
<div className={cn("mt-2 text-xs leading-5", isDark ? "text-white/50" : "text-[#64748b]")}>
{step.description}
</div>
</div>
))}
</div>
</div>
<AgentWorkflowMaterialPicker
value={inputs.primarySku}
queryMode={materialSearchMode.primary}