feat: streamline agent detail selection workflow
This commit is contained in:
parent
dc1665d7ba
commit
467de8ff1e
@ -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) => (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user