feat: extend workflow category and card controls
This commit is contained in:
parent
f0086e26da
commit
7eda4720d3
@ -6,10 +6,11 @@ import useSWR from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
GripVertical,
|
||||
Image as ImageIcon,
|
||||
Layers,
|
||||
Loader2,
|
||||
@ -95,6 +96,7 @@ import type {
|
||||
AgentWorkflowMaterialApiConfig,
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentPsdTextColorConfig,
|
||||
AgentWorkflowStoreEntry,
|
||||
AgentWorkflowShowcaseConfig,
|
||||
AgentWorkflowStepUiConfig,
|
||||
AgentWorkflowUiConfig,
|
||||
@ -306,6 +308,23 @@ function removeWorkflowStepTree(
|
||||
}));
|
||||
}
|
||||
|
||||
function moveArrayItem<T>(items: T[], fromIndex: number, toIndex: number): T[] {
|
||||
if (
|
||||
fromIndex === toIndex ||
|
||||
fromIndex < 0 ||
|
||||
toIndex < 0 ||
|
||||
fromIndex >= items.length ||
|
||||
toIndex >= items.length
|
||||
) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const nextItems = [...items];
|
||||
const [movedItem] = nextItems.splice(fromIndex, 1);
|
||||
nextItems.splice(toIndex, 0, movedItem);
|
||||
return nextItems;
|
||||
}
|
||||
|
||||
function collectWorkflowStepKeys(steps: AgentWorkflowStepUiConfig[]): string[] {
|
||||
return flattenAgentWorkflowSteps(steps).map(({ step }) => step.key);
|
||||
}
|
||||
@ -437,13 +456,33 @@ function buildWorkflowInputOptionsFromBrandEntries(
|
||||
) {
|
||||
const normalizedEntries = entries.map((entry, index) => ({
|
||||
brandName: entry.brandName.trim(),
|
||||
stores: Array.from(
|
||||
new Set(
|
||||
(entry.stores || [])
|
||||
.map((store) => store.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
),
|
||||
storeEntries: (() => {
|
||||
const uniqueStoreEntries = new Map<string, AgentWorkflowStoreEntry>();
|
||||
|
||||
(entry.storeEntries || []).forEach((storeEntry, storeIndex) => {
|
||||
const storeName = String(storeEntry.storeName || "").trim();
|
||||
if (!storeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
uniqueStoreEntries.set(storeName.toLowerCase(), {
|
||||
storeName,
|
||||
thirdCategories: Array.from(
|
||||
new Set(
|
||||
(storeEntry.thirdCategories || [])
|
||||
.map((thirdCategory) => thirdCategory.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
),
|
||||
sortOrder: storeIndex,
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(uniqueStoreEntries.values()).map((storeEntry, storeIndex) => ({
|
||||
...storeEntry,
|
||||
sortOrder: storeIndex,
|
||||
}));
|
||||
})(),
|
||||
sortOrder: index,
|
||||
}));
|
||||
const normalizedPriceBands = Array.from(
|
||||
@ -455,9 +494,25 @@ function buildWorkflowInputOptionsFromBrandEntries(
|
||||
);
|
||||
|
||||
return {
|
||||
brandStoreEntries: normalizedEntries,
|
||||
brandStoreEntries: normalizedEntries.map((entry) => ({
|
||||
...entry,
|
||||
stores: entry.storeEntries.map((storeEntry) => storeEntry.storeName),
|
||||
})),
|
||||
brands: normalizedEntries.map((entry) => entry.brandName),
|
||||
stores: Array.from(new Set(normalizedEntries.flatMap((entry) => entry.stores))),
|
||||
stores: Array.from(
|
||||
new Set(
|
||||
normalizedEntries.flatMap((entry) =>
|
||||
entry.storeEntries.map((storeEntry) => storeEntry.storeName)
|
||||
)
|
||||
)
|
||||
),
|
||||
thirdCategories: Array.from(
|
||||
new Set(
|
||||
normalizedEntries.flatMap((entry) =>
|
||||
entry.storeEntries.flatMap((storeEntry) => storeEntry.thirdCategories || [])
|
||||
)
|
||||
)
|
||||
),
|
||||
priceBands: normalizedPriceBands,
|
||||
};
|
||||
}
|
||||
@ -517,6 +572,8 @@ export function AgentWorkflowSettingsClient({
|
||||
const [brandDraft, setBrandDraft] = useState("");
|
||||
const [priceBandDraft, setPriceBandDraft] = useState("");
|
||||
const [storeDrafts, setStoreDrafts] = useState<Record<string, string>>({});
|
||||
const [thirdCategoryDrafts, setThirdCategoryDrafts] = useState<Record<string, string>>({});
|
||||
const [blockedOptionSearches, setBlockedOptionSearches] = useState<Record<string, string>>({});
|
||||
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
|
||||
|
||||
useEffect(() => {
|
||||
@ -597,6 +654,14 @@ export function AgentWorkflowSettingsClient({
|
||||
[...presetLibraryConfig.sections].sort((left, right) => left.sortOrder - right.sortOrder),
|
||||
[presetLibraryConfig.sections]
|
||||
);
|
||||
const workflowOptionRelations = useMemo(
|
||||
() => collectWorkflowOptionRelationsFromSteps(config.steps || []),
|
||||
[config.steps]
|
||||
);
|
||||
const workflowOptionRelationLookup = useMemo(
|
||||
() => new Map(workflowOptionRelations.map((item) => [item.key, item] as const)),
|
||||
[workflowOptionRelations]
|
||||
);
|
||||
const updateInputOptions = (nextEntries: AgentWorkflowBrandStoreEntry[]) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
@ -629,6 +694,7 @@ export function AgentWorkflowSettingsClient({
|
||||
{
|
||||
brandName: nextValue,
|
||||
stores: [],
|
||||
storeEntries: [],
|
||||
sortOrder: brandStoreEntries.length,
|
||||
},
|
||||
]);
|
||||
@ -642,6 +708,15 @@ export function AgentWorkflowSettingsClient({
|
||||
delete next[brandName];
|
||||
return next;
|
||||
});
|
||||
setThirdCategoryDrafts((prev) => {
|
||||
const next = { ...prev };
|
||||
Object.keys(next).forEach((key) => {
|
||||
if (key.startsWith(`${brandName}::`)) {
|
||||
delete next[key];
|
||||
}
|
||||
});
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddStoreToBrand = (brandName: string) => {
|
||||
@ -653,7 +728,12 @@ export function AgentWorkflowSettingsClient({
|
||||
}
|
||||
|
||||
const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName);
|
||||
if (targetEntry?.stores.some((store) => store.toLowerCase() === nextValue.toLowerCase())) {
|
||||
if (
|
||||
targetEntry?.storeEntries?.some(
|
||||
(storeEntry) => storeEntry.storeName.toLowerCase() === nextValue.toLowerCase()
|
||||
) ||
|
||||
targetEntry?.stores.some((store) => store.toLowerCase() === nextValue.toLowerCase())
|
||||
) {
|
||||
toast.error("该店铺已存在");
|
||||
return;
|
||||
}
|
||||
@ -664,6 +744,17 @@ export function AgentWorkflowSettingsClient({
|
||||
return {
|
||||
...entry,
|
||||
stores: [...entry.stores, nextValue],
|
||||
storeEntries: [
|
||||
...(entry.storeEntries || []).map((storeEntry, storeIndex) => ({
|
||||
...storeEntry,
|
||||
sortOrder: storeIndex,
|
||||
})),
|
||||
{
|
||||
storeName: nextValue,
|
||||
thirdCategories: [],
|
||||
sortOrder: (entry.storeEntries || []).length,
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
);
|
||||
@ -680,10 +771,86 @@ export function AgentWorkflowSettingsClient({
|
||||
? {
|
||||
...entry,
|
||||
stores: entry.stores.filter((store) => store !== storeName),
|
||||
storeEntries: (entry.storeEntries || []).filter(
|
||||
(storeEntry) => storeEntry.storeName !== storeName
|
||||
),
|
||||
}
|
||||
: entry
|
||||
)
|
||||
);
|
||||
setThirdCategoryDrafts((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[`${brandName}::${storeName}`];
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddThirdCategoryToStore = (brandName: string, storeName: string) => {
|
||||
const draftKey = `${brandName}::${storeName}`;
|
||||
const nextValue = normalizeInputOptionValue(thirdCategoryDrafts[draftKey] || "");
|
||||
if (!nextValue) {
|
||||
toast.error("请输入三级分类名称");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEntry = brandStoreEntries.find((entry) => entry.brandName === brandName);
|
||||
const targetStoreEntry = targetEntry?.storeEntries?.find(
|
||||
(storeEntry) => storeEntry.storeName === storeName
|
||||
);
|
||||
if (
|
||||
targetStoreEntry?.thirdCategories.some(
|
||||
(thirdCategory) => thirdCategory.toLowerCase() === nextValue.toLowerCase()
|
||||
)
|
||||
) {
|
||||
toast.error("该三级分类已存在");
|
||||
return;
|
||||
}
|
||||
|
||||
updateInputOptions(
|
||||
brandStoreEntries.map((entry) => {
|
||||
if (entry.brandName !== brandName) return entry;
|
||||
return {
|
||||
...entry,
|
||||
storeEntries: (entry.storeEntries || []).map((storeEntry) =>
|
||||
storeEntry.storeName === storeName
|
||||
? {
|
||||
...storeEntry,
|
||||
thirdCategories: [...(storeEntry.thirdCategories || []), nextValue],
|
||||
}
|
||||
: storeEntry
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
setThirdCategoryDrafts((prev) => ({
|
||||
...prev,
|
||||
[draftKey]: "",
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveThirdCategoryFromStore = (
|
||||
brandName: string,
|
||||
storeName: string,
|
||||
thirdCategory: string
|
||||
) => {
|
||||
updateInputOptions(
|
||||
brandStoreEntries.map((entry) => {
|
||||
if (entry.brandName !== brandName) return entry;
|
||||
return {
|
||||
...entry,
|
||||
storeEntries: (entry.storeEntries || []).map((storeEntry) =>
|
||||
storeEntry.storeName === storeName
|
||||
? {
|
||||
...storeEntry,
|
||||
thirdCategories: (storeEntry.thirdCategories || []).filter(
|
||||
(item) => item !== thirdCategory
|
||||
),
|
||||
}
|
||||
: storeEntry
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddPriceBand = () => {
|
||||
@ -1020,6 +1187,33 @@ export function AgentWorkflowSettingsClient({
|
||||
}));
|
||||
};
|
||||
|
||||
const handleMoveRootOption = (stepKey: string, optionKey: string, direction: "up" | "down") => {
|
||||
updateStep(stepKey, (step) => {
|
||||
const sortedRootOptions = [...(step.options || [])].sort(
|
||||
(left, right) => left.sortOrder - right.sortOrder
|
||||
);
|
||||
const currentIndex = sortedRootOptions.findIndex((option) => option.key === optionKey);
|
||||
if (currentIndex < 0) {
|
||||
return step;
|
||||
}
|
||||
|
||||
const targetIndex = direction === "up" ? currentIndex - 1 : currentIndex + 1;
|
||||
if (targetIndex < 0 || targetIndex >= sortedRootOptions.length) {
|
||||
return step;
|
||||
}
|
||||
|
||||
return {
|
||||
...step,
|
||||
options: moveArrayItem(sortedRootOptions, currentIndex, targetIndex).map(
|
||||
(option, index) => ({
|
||||
...option,
|
||||
sortOrder: index,
|
||||
})
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleUploadPreview = async (stepKey: string, optionKey: string, file: File) => {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toast.error("请上传图片文件");
|
||||
@ -1346,13 +1540,26 @@ export function AgentWorkflowSettingsClient({
|
||||
(left, right) => left.sortOrder - right.sortOrder
|
||||
);
|
||||
const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH;
|
||||
const optionRelations = collectWorkflowOptionRelationsFromSteps(config.steps || []);
|
||||
const optionLookup = new Map(optionRelations.map((item) => [item.key, item] as const));
|
||||
const blockedCandidates = optionRelations.filter((item) => item.key !== option.key);
|
||||
const blockedCandidates = workflowOptionRelations.filter(
|
||||
(item) => item.key !== option.key && item.depth === depth
|
||||
);
|
||||
const selectedBlockedItems = (option.blockedOptionKeys || [])
|
||||
.map((blockedKey) => optionLookup.get(blockedKey))
|
||||
.map((blockedKey) => workflowOptionRelationLookup.get(blockedKey))
|
||||
.filter((item): item is WorkflowOptionRelation => Boolean(item));
|
||||
const isExpanded = isWorkflowOptionExpanded(option.key, depth);
|
||||
const blockedSearchValue = blockedOptionSearches[refKey] || "";
|
||||
const normalizedBlockedSearchValue = blockedSearchValue.trim().toLowerCase();
|
||||
const filteredBlockedCandidates = blockedCandidates.filter((candidate) => {
|
||||
if (!normalizedBlockedSearchValue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return [candidate.label, candidate.stepTitle, candidate.key].some((value) =>
|
||||
value.toLowerCase().includes(normalizedBlockedSearchValue)
|
||||
);
|
||||
});
|
||||
const canMoveUp = depth === 1 && indexPath[0] > 1;
|
||||
const canMoveDown = depth === 1 && indexPath[0] < (step.options || []).length;
|
||||
|
||||
return (
|
||||
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
|
||||
@ -1403,6 +1610,30 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{depth === 1 ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
disabled={!canMoveUp}
|
||||
onClick={() => handleMoveRootOption(step.key, option.key, "up")}
|
||||
>
|
||||
<ArrowUp className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
disabled={!canMoveDown}
|
||||
onClick={() => handleMoveRootOption(step.key, option.key, "down")}
|
||||
>
|
||||
<ArrowDown className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{canAddChild ? (
|
||||
<Button
|
||||
type="button"
|
||||
@ -1535,7 +1766,7 @@ export function AgentWorkflowSettingsClient({
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">互斥卡片限制</Label>
|
||||
<p className="text-[11px] leading-5 text-muted-foreground">
|
||||
选中当前卡片后,下方指定的其它卡片会在前台变灰并不可选。可跨所有流程节点,并覆盖各节点下的所有层级子卡片。
|
||||
当前只展示同级卡片供你配置互斥。一级只与所有一级卡片互斥,二级只与所有二级卡片互斥;生效时会自动覆盖目标卡片下的全部子级。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -1574,9 +1805,24 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-h-44 space-y-2 overflow-y-auto rounded-md border bg-background/70 p-2">
|
||||
{blockedCandidates.length > 0 ? (
|
||||
blockedCandidates.map((candidate) => {
|
||||
<div className="space-y-2 rounded-md border bg-background/70 p-2">
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={blockedSearchValue}
|
||||
onChange={(e) =>
|
||||
setBlockedOptionSearches((prev) => ({
|
||||
...prev,
|
||||
[refKey]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={`搜索同级卡片(${blockedCandidates.length} 个候选)`}
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-44 space-y-2 overflow-y-auto">
|
||||
{filteredBlockedCandidates.length > 0 ? (
|
||||
filteredBlockedCandidates.map((candidate) => {
|
||||
const active = (option.blockedOptionKeys || []).includes(candidate.key);
|
||||
return (
|
||||
<button
|
||||
@ -1610,7 +1856,7 @@ export function AgentWorkflowSettingsClient({
|
||||
{candidate.label}
|
||||
</span>
|
||||
<span className="block text-[10px] text-muted-foreground">
|
||||
{getWorkflowLevelLabel(candidate.depth, "卡片")}
|
||||
{getWorkflowLevelLabel(candidate.depth, "卡片")} · {candidate.key}
|
||||
</span>
|
||||
</span>
|
||||
<Badge variant={active ? "default" : "outline"}>
|
||||
@ -1618,12 +1864,15 @@ export function AgentWorkflowSettingsClient({
|
||||
</Badge>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
||||
当前工作流下没有其他可限制卡片
|
||||
</div>
|
||||
)}
|
||||
})
|
||||
) : (
|
||||
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
||||
{blockedCandidates.length > 0
|
||||
? "没有命中搜索结果,换个关键词试试"
|
||||
: "当前工作流下没有其他可限制的同级卡片"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2477,7 +2726,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">基础下拉选项</CardTitle>
|
||||
<CardDescription>配置品牌、店铺母子关系,以及第一页可选的价格带区间</CardDescription>
|
||||
<CardDescription>配置品牌、店铺、三级分类的层级关系,以及第一页可选的价格带区间</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -2530,7 +2779,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{entry.stores.length} 个店铺
|
||||
{(entry.storeEntries || []).length} 个店铺
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -2564,24 +2813,112 @@ export function AgentWorkflowSettingsClient({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{entry.stores.length > 0 ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{entry.stores.map((store) => (
|
||||
<div
|
||||
key={`${entry.brandName}-${store}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm"
|
||||
>
|
||||
<span>{store}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveStoreFromBrand(entry.brandName, store)}
|
||||
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
||||
aria-label={`删除店铺 ${store}`}
|
||||
{(entry.storeEntries || []).length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{(entry.storeEntries || []).map((storeEntry) => {
|
||||
const draftKey = `${entry.brandName}::${storeEntry.storeName}`;
|
||||
return (
|
||||
<div
|
||||
key={`${entry.brandName}-${storeEntry.storeName}`}
|
||||
className="rounded-xl border bg-background/80 p-3"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm font-medium">
|
||||
<span>{storeEntry.storeName}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleRemoveStoreFromBrand(
|
||||
entry.brandName,
|
||||
storeEntry.storeName
|
||||
)
|
||||
}
|
||||
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
||||
aria-label={`删除店铺 ${storeEntry.storeName}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{(storeEntry.thirdCategories || []).length} 个三级分类
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-xl border bg-muted/20 p-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
value={thirdCategoryDrafts[draftKey] || ""}
|
||||
onChange={(e) =>
|
||||
setThirdCategoryDrafts((prev) => ({
|
||||
...prev,
|
||||
[draftKey]: e.target.value,
|
||||
}))
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleAddThirdCategoryToStore(
|
||||
entry.brandName,
|
||||
storeEntry.storeName
|
||||
);
|
||||
}
|
||||
}}
|
||||
placeholder={`给店铺“${storeEntry.storeName}”添加三级分类`}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleAddThirdCategoryToStore(
|
||||
entry.brandName,
|
||||
storeEntry.storeName
|
||||
)
|
||||
}
|
||||
className="shrink-0"
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
添加三级分类
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(storeEntry.thirdCategories || []).length > 0 ? (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{(storeEntry.thirdCategories || []).map(
|
||||
(thirdCategory) => (
|
||||
<div
|
||||
key={`${entry.brandName}-${storeEntry.storeName}-${thirdCategory}`}
|
||||
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-sm"
|
||||
>
|
||||
<span>{thirdCategory}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleRemoveThirdCategoryFromStore(
|
||||
entry.brandName,
|
||||
storeEntry.storeName,
|
||||
thirdCategory
|
||||
)
|
||||
}
|
||||
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
||||
aria-label={`删除三级分类 ${thirdCategory}`}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
这个店铺下还没有添加任何三级分类。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
@ -2594,7 +2931,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 rounded-lg border border-dashed bg-background/70 px-3 py-4 text-sm text-muted-foreground">
|
||||
还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺。
|
||||
还没有添加品牌。先添加品牌,再在每个品牌下继续添加店铺和三级分类。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -730,12 +730,26 @@ const updateSchema = z.object({
|
||||
.object({
|
||||
brands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
stores: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
thirdCategories: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
priceBands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
brandStoreEntries: z
|
||||
.array(
|
||||
z.object({
|
||||
brandName: z.string().trim().min(1).max(64),
|
||||
stores: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
storeEntries: z
|
||||
.array(
|
||||
z.object({
|
||||
storeName: z.string().trim().min(1).max(64),
|
||||
thirdCategories: z
|
||||
.array(z.string().trim().min(1).max(64))
|
||||
.max(300)
|
||||
.optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
})
|
||||
)
|
||||
.max(300)
|
||||
.optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
})
|
||||
)
|
||||
|
||||
@ -152,6 +152,7 @@ function hasMeaningfulWorkflowState(
|
||||
String(inputs?.secondarySku || "").trim() ||
|
||||
String(inputs?.brand || "").trim() ||
|
||||
String(inputs?.store || "").trim() ||
|
||||
String(inputs?.thirdCategory || "").trim() ||
|
||||
String(inputs?.priceBand || "").trim()
|
||||
);
|
||||
|
||||
@ -320,6 +321,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
secondarySku: "",
|
||||
brand: "",
|
||||
store: "",
|
||||
thirdCategory: "",
|
||||
priceBand: "",
|
||||
});
|
||||
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(
|
||||
@ -486,6 +488,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
secondarySku: workflowInputs.secondarySku.trim(),
|
||||
brand: workflowInputs.brand.trim(),
|
||||
store: workflowInputs.store.trim(),
|
||||
thirdCategory: workflowInputs.thirdCategory.trim(),
|
||||
priceBand: workflowInputs.priceBand.trim(),
|
||||
},
|
||||
materials: workflowMaterials,
|
||||
@ -531,6 +534,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
secondarySku: "",
|
||||
brand: "",
|
||||
store: "",
|
||||
thirdCategory: "",
|
||||
priceBand: "",
|
||||
});
|
||||
setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections());
|
||||
@ -562,6 +566,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
|
||||
brand: String(persistedWorkflow.inputs?.brand || "").trim(),
|
||||
store: String(persistedWorkflow.inputs?.store || "").trim(),
|
||||
thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(),
|
||||
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
|
||||
});
|
||||
setWorkflowMaterials(
|
||||
@ -670,6 +675,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
secondarySku: workflowInputs.secondarySku.trim(),
|
||||
brand: workflowInputs.brand.trim(),
|
||||
store: workflowInputs.store.trim(),
|
||||
thirdCategory: workflowInputs.thirdCategory.trim(),
|
||||
priceBand: workflowInputs.priceBand.trim(),
|
||||
},
|
||||
materials: workflowMaterials,
|
||||
@ -756,6 +762,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
secondarySku: String(parsed.inputs?.secondarySku || "").trim(),
|
||||
brand: String(parsed.inputs?.brand || "").trim(),
|
||||
store: String(parsed.inputs?.store || "").trim(),
|
||||
thirdCategory: String(parsed.inputs?.thirdCategory || "").trim(),
|
||||
priceBand: String(parsed.inputs?.priceBand || "").trim(),
|
||||
});
|
||||
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(parsed.materials));
|
||||
@ -882,6 +889,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
|
||||
brand: String(persistedWorkflow.inputs?.brand || "").trim(),
|
||||
store: String(persistedWorkflow.inputs?.store || "").trim(),
|
||||
thirdCategory: String(persistedWorkflow.inputs?.thirdCategory || "").trim(),
|
||||
priceBand: String(persistedWorkflow.inputs?.priceBand || "").trim(),
|
||||
});
|
||||
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials));
|
||||
|
||||
@ -37,6 +37,7 @@ export function AgentPageDark({
|
||||
secondarySku: "",
|
||||
brand: "",
|
||||
store: "",
|
||||
thirdCategory: "",
|
||||
priceBand: "",
|
||||
});
|
||||
const [workflowMaterials, setWorkflowMaterials] = useState<AgentWorkflowMaterialSelections>(
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowPriceBandOptions,
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||
@ -31,6 +32,7 @@ export interface AgentWorkflowInputs {
|
||||
secondarySku: string;
|
||||
brand: string;
|
||||
store: string;
|
||||
thirdCategory: string;
|
||||
priceBand: string;
|
||||
}
|
||||
|
||||
@ -98,6 +100,15 @@ export function AgentWorkflowPanel({
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
const thirdCategoryOptions = useMemo(
|
||||
() =>
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore({
|
||||
config,
|
||||
brand: inputs.brand,
|
||||
store: inputs.store,
|
||||
}),
|
||||
[config, inputs.brand, inputs.store]
|
||||
);
|
||||
const priceBandOptions = useMemo(
|
||||
() => getAgentWorkflowPriceBandOptions(config),
|
||||
[config]
|
||||
@ -181,13 +192,14 @@ export function AgentWorkflowPanel({
|
||||
onReset();
|
||||
return;
|
||||
}
|
||||
onInputsChange({
|
||||
primarySku: "",
|
||||
secondarySku: "",
|
||||
brand: "",
|
||||
store: "",
|
||||
priceBand: "",
|
||||
});
|
||||
onInputsChange({
|
||||
primarySku: "",
|
||||
secondarySku: "",
|
||||
brand: "",
|
||||
store: "",
|
||||
thirdCategory: "",
|
||||
priceBand: "",
|
||||
});
|
||||
onMaterialsChange({
|
||||
primary: createEmptyAgentWorkflowMaterialSelection(),
|
||||
secondary: createEmptyAgentWorkflowMaterialSelection(),
|
||||
@ -205,7 +217,7 @@ export function AgentWorkflowPanel({
|
||||
<div className="space-y-5 p-4">
|
||||
<section className="space-y-3">
|
||||
<div className="text-sm font-medium">基础配置</div>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<SearchableSelect
|
||||
value={inputs.brand}
|
||||
options={brandOptions}
|
||||
@ -220,6 +232,10 @@ export function AgentWorkflowPanel({
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
@ -230,7 +246,25 @@ export function AgentWorkflowPanel({
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
store: value,
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.thirdCategory}
|
||||
options={thirdCategoryOptions}
|
||||
placeholder="选择三级分类"
|
||||
searchPlaceholder="输入三级分类筛选"
|
||||
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.priceBand}
|
||||
@ -322,6 +356,7 @@ export function AgentWorkflowPanel({
|
||||
inputs.secondarySku ||
|
||||
inputs.brand ||
|
||||
inputs.store ||
|
||||
inputs.thirdCategory ||
|
||||
inputs.priceBand ||
|
||||
materials.primary.selectedImageIds.length > 0 ||
|
||||
materials.secondary.selectedImageIds.length > 0 ? (
|
||||
@ -338,6 +373,9 @@ export function AgentWorkflowPanel({
|
||||
{inputs.store ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">店铺:{inputs.store}</span>
|
||||
) : null}
|
||||
{inputs.thirdCategory ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">三级分类:{inputs.thirdCategory}</span>
|
||||
) : null}
|
||||
{inputs.priceBand ? (
|
||||
<span className="rounded-full bg-background px-2 py-1">价格带:{inputs.priceBand}</span>
|
||||
) : null}
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowPriceBandOptions,
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||
@ -32,6 +33,7 @@ export interface AgentWorkflowInputs {
|
||||
secondarySku: string;
|
||||
brand: string;
|
||||
store: string;
|
||||
thirdCategory: string;
|
||||
priceBand: string;
|
||||
}
|
||||
|
||||
@ -180,6 +182,15 @@ export function AgentWorkflowPanelDark({
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
const thirdCategoryOptions = useMemo(
|
||||
() =>
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore({
|
||||
config,
|
||||
brand: inputs.brand,
|
||||
store: inputs.store,
|
||||
}),
|
||||
[config, inputs.brand, inputs.store]
|
||||
);
|
||||
const priceBandOptions = useMemo(
|
||||
() => getAgentWorkflowPriceBandOptions(config),
|
||||
[config]
|
||||
@ -273,7 +284,7 @@ export function AgentWorkflowPanelDark({
|
||||
onReset();
|
||||
return;
|
||||
}
|
||||
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", priceBand: "" });
|
||||
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" });
|
||||
onMaterialsChange({
|
||||
primary: createEmptyAgentWorkflowMaterialSelection(),
|
||||
secondary: createEmptyAgentWorkflowMaterialSelection(),
|
||||
@ -307,7 +318,7 @@ export function AgentWorkflowPanelDark({
|
||||
基础配置
|
||||
</motion.h2>
|
||||
<motion.div
|
||||
className="grid grid-cols-1 gap-4 md:grid-cols-3"
|
||||
className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
@ -326,6 +337,10 @@ export function AgentWorkflowPanelDark({
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
@ -336,7 +351,25 @@ export function AgentWorkflowPanelDark({
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
store: value,
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.thirdCategory}
|
||||
options={thirdCategoryOptions}
|
||||
placeholder="选择三级分类"
|
||||
searchPlaceholder="输入三级分类筛选"
|
||||
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.priceBand}
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
getAgentWorkflowBlockedOptionKeysForSelections,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowPriceBandOptions,
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore,
|
||||
getAgentWorkflowSelectedOptionPath,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
getEnabledAgentWorkflowOptionTree,
|
||||
@ -41,6 +42,7 @@ export interface AgentWorkflowInputs {
|
||||
secondarySku: string;
|
||||
brand: string;
|
||||
store: string;
|
||||
thirdCategory: string;
|
||||
priceBand: string;
|
||||
}
|
||||
|
||||
@ -271,8 +273,8 @@ export function AgentWorkflowBrandStorePanel({
|
||||
inputs,
|
||||
onInputsChange,
|
||||
theme = "dark",
|
||||
title = "品牌和店铺选择",
|
||||
description = "先选择品牌、店铺和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
|
||||
title = "品牌层级选择",
|
||||
description = "先选择品牌、店铺、三级分类和价格带,后续品牌模特与风格步骤会按这里的选择联动。",
|
||||
}: AgentWorkflowBrandStorePanelProps) {
|
||||
const brandOptions = useMemo(
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
@ -286,6 +288,15 @@ export function AgentWorkflowBrandStorePanel({
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
const thirdCategoryOptions = useMemo(
|
||||
() =>
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore({
|
||||
config,
|
||||
brand: inputs.brand,
|
||||
store: inputs.store,
|
||||
}),
|
||||
[config, inputs.brand, inputs.store]
|
||||
);
|
||||
const priceBandOptions = useMemo(
|
||||
() => getAgentWorkflowPriceBandOptions(config),
|
||||
[config]
|
||||
@ -307,7 +318,7 @@ export function AgentWorkflowBrandStorePanel({
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid max-w-5xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div className="grid max-w-6xl grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<SearchableSelect
|
||||
value={inputs.brand}
|
||||
options={brandOptions}
|
||||
@ -323,6 +334,10 @@ export function AgentWorkflowBrandStorePanel({
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
@ -334,7 +349,26 @@ export function AgentWorkflowBrandStorePanel({
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
theme={theme}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
store: value,
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.thirdCategory}
|
||||
options={thirdCategoryOptions}
|
||||
placeholder="选择三级分类"
|
||||
searchPlaceholder="输入三级分类筛选"
|
||||
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
|
||||
theme={theme}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.priceBand}
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowPriceBandOptions,
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||
@ -32,6 +33,7 @@ export interface AgentWorkflowInputs {
|
||||
secondarySku: string;
|
||||
brand: string;
|
||||
store: string;
|
||||
thirdCategory: string;
|
||||
priceBand: string;
|
||||
}
|
||||
|
||||
@ -192,6 +194,15 @@ export function AgentWorkflowPanelNew({
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
const thirdCategoryOptions = useMemo(
|
||||
() =>
|
||||
getAgentWorkflowThirdCategoryOptionsByBrandStore({
|
||||
config,
|
||||
brand: inputs.brand,
|
||||
store: inputs.store,
|
||||
}),
|
||||
[config, inputs.brand, inputs.store]
|
||||
);
|
||||
const priceBandOptions = useMemo(
|
||||
() => getAgentWorkflowPriceBandOptions(config),
|
||||
[config]
|
||||
@ -294,7 +305,7 @@ export function AgentWorkflowPanelNew({
|
||||
onReset();
|
||||
return;
|
||||
}
|
||||
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", priceBand: "" });
|
||||
onInputsChange({ primarySku: "", secondarySku: "", brand: "", store: "", thirdCategory: "", priceBand: "" });
|
||||
onMaterialsChange({
|
||||
primary: createEmptyAgentWorkflowMaterialSelection(),
|
||||
secondary: createEmptyAgentWorkflowMaterialSelection(),
|
||||
@ -346,7 +357,7 @@ export function AgentWorkflowPanelNew({
|
||||
</span>
|
||||
基础配置
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<SearchableSelect
|
||||
value={inputs.brand}
|
||||
options={brandOptions}
|
||||
@ -361,6 +372,10 @@ export function AgentWorkflowPanelNew({
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
@ -371,7 +386,25 @@ export function AgentWorkflowPanelNew({
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
store: value,
|
||||
thirdCategory:
|
||||
value.trim().toLowerCase() === inputs.store.trim().toLowerCase()
|
||||
? inputs.thirdCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.thirdCategory}
|
||||
options={thirdCategoryOptions}
|
||||
placeholder="选择三级分类"
|
||||
searchPlaceholder="输入三级分类筛选"
|
||||
emptyText={inputs.store ? "当前店铺下暂无三级分类" : "请先选择店铺"}
|
||||
disabled={!inputs.store || thirdCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, thirdCategory: value })}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.priceBand}
|
||||
|
||||
@ -34,6 +34,7 @@ export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersisted
|
||||
secondarySku: "",
|
||||
brand: "",
|
||||
store: "",
|
||||
thirdCategory: "",
|
||||
priceBand: "",
|
||||
},
|
||||
materials: createEmptyAgentWorkflowMaterialSelections(),
|
||||
@ -229,6 +230,7 @@ function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState {
|
||||
secondarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).secondarySku || "" : "").trim(),
|
||||
brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).brand || "" : "").trim(),
|
||||
store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).store || "" : "").trim(),
|
||||
thirdCategory: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).thirdCategory || "" : "").trim(),
|
||||
priceBand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).priceBand || "" : "").trim(),
|
||||
},
|
||||
materials: normalizeAgentWorkflowMaterialSelections(raw.materials),
|
||||
|
||||
@ -20,6 +20,7 @@ export interface AgentWorkflowInputsValue {
|
||||
secondarySku: string;
|
||||
brand: string;
|
||||
store: string;
|
||||
thirdCategory: string;
|
||||
priceBand: string;
|
||||
}
|
||||
|
||||
@ -212,6 +213,9 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
if (params.inputs.store.trim()) {
|
||||
summaryParts.push(`店铺:${params.inputs.store.trim()}`);
|
||||
}
|
||||
if (params.inputs.thirdCategory.trim()) {
|
||||
summaryParts.push(`三级分类:${params.inputs.thirdCategory.trim()}`);
|
||||
}
|
||||
if (params.inputs.priceBand.trim()) {
|
||||
summaryParts.push(`价格带:${params.inputs.priceBand.trim()}`);
|
||||
}
|
||||
@ -269,6 +273,9 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
if (params.inputs.store.trim()) {
|
||||
promptLines.push(`店铺:${params.inputs.store.trim()}`);
|
||||
}
|
||||
if (params.inputs.thirdCategory.trim()) {
|
||||
promptLines.push(`三级分类:${params.inputs.thirdCategory.trim()}`);
|
||||
}
|
||||
if (params.inputs.priceBand.trim()) {
|
||||
promptLines.push(`价格带:${params.inputs.priceBand.trim()}`);
|
||||
}
|
||||
@ -354,6 +361,7 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
secondarySku: params.inputs.secondarySku.trim() || undefined,
|
||||
brand: params.inputs.brand.trim() || undefined,
|
||||
store: params.inputs.store.trim() || undefined,
|
||||
thirdCategory: params.inputs.thirdCategory.trim() || undefined,
|
||||
priceBand: params.inputs.priceBand.trim() || undefined,
|
||||
selections,
|
||||
referenceImages,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AgentWorkflowBrandStoreEntry,
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentWorkflowStoreEntry,
|
||||
AgentWorkflowStepKey,
|
||||
AgentWorkflowStepUiConfig,
|
||||
AgentWorkflowUiConfig,
|
||||
@ -139,6 +140,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
|
||||
inputOptions: {
|
||||
brands: [],
|
||||
stores: [],
|
||||
thirdCategories: [],
|
||||
priceBands: [],
|
||||
brandStoreEntries: [],
|
||||
},
|
||||
@ -432,6 +434,58 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
const normalizeBrandStoreEntries = (input: unknown) => {
|
||||
if (!Array.isArray(input)) return [] as AgentWorkflowBrandStoreEntry[];
|
||||
|
||||
const normalizeStoreEntries = (
|
||||
rawStoreEntries: unknown,
|
||||
legacyStores: string[]
|
||||
): AgentWorkflowStoreEntry[] => {
|
||||
const mappedEntries: Array<AgentWorkflowStoreEntry | null> = Array.isArray(rawStoreEntries)
|
||||
? rawStoreEntries.map((storeItem, storeIndex) => {
|
||||
if (typeof storeItem === "string") {
|
||||
const storeName = normalizeText(storeItem, 64);
|
||||
if (!storeName) return null;
|
||||
return {
|
||||
storeName,
|
||||
thirdCategories: [],
|
||||
sortOrder: storeIndex,
|
||||
};
|
||||
}
|
||||
|
||||
const storeRecord =
|
||||
storeItem && typeof storeItem === "object" && !Array.isArray(storeItem)
|
||||
? (storeItem as Record<string, unknown>)
|
||||
: {};
|
||||
const storeName = normalizeText(storeRecord.storeName, 64);
|
||||
if (!storeName) return null;
|
||||
return {
|
||||
storeName,
|
||||
thirdCategories: normalizeStringList(storeRecord.thirdCategories).slice(0, 300),
|
||||
sortOrder: normalizeNumber(storeRecord.sortOrder, storeIndex),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const normalizedFromEntries = mappedEntries
|
||||
.filter((item): item is AgentWorkflowStoreEntry => item !== null)
|
||||
.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0))
|
||||
.map((entry, index) => ({
|
||||
...entry,
|
||||
thirdCategories: Array.from(
|
||||
new Set((entry.thirdCategories || []).map((item) => item.trim()).filter(Boolean))
|
||||
).slice(0, 300),
|
||||
sortOrder: index,
|
||||
}));
|
||||
|
||||
if (normalizedFromEntries.length > 0) {
|
||||
return normalizedFromEntries;
|
||||
}
|
||||
|
||||
return legacyStores.map((storeName, index) => ({
|
||||
storeName,
|
||||
thirdCategories: [],
|
||||
sortOrder: index,
|
||||
}));
|
||||
};
|
||||
|
||||
const mappedEntries: Array<AgentWorkflowBrandStoreEntry | null> = input.map((item, index) => {
|
||||
const record =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
@ -439,11 +493,14 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
: {};
|
||||
const brandName = normalizeText(record.brandName, 64);
|
||||
if (!brandName) return null;
|
||||
const stores = normalizeStringList(record.stores).slice(0, 300);
|
||||
const legacyStores = normalizeStringList(record.stores).slice(0, 300);
|
||||
const storeEntries = normalizeStoreEntries(record.storeEntries, legacyStores);
|
||||
const stores = storeEntries.map((entry) => entry.storeName);
|
||||
|
||||
return {
|
||||
brandName,
|
||||
stores,
|
||||
storeEntries,
|
||||
sortOrder: normalizeNumber(record.sortOrder, index),
|
||||
};
|
||||
});
|
||||
@ -461,6 +518,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
);
|
||||
const normalizedBrands = normalizeStringList(rawInputOptions.brands);
|
||||
const normalizedStores = normalizeStringList(rawInputOptions.stores);
|
||||
const normalizedThirdCategories = normalizeStringList(rawInputOptions.thirdCategories);
|
||||
const normalizedPriceBands = normalizeStringList(rawInputOptions.priceBands);
|
||||
const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] =
|
||||
normalizedBrandStoreEntries.length > 0
|
||||
@ -468,6 +526,14 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
: normalizedBrands.map((brandName, index) => ({
|
||||
brandName,
|
||||
stores: normalizedBrands.length === 1 ? normalizedStores : [],
|
||||
storeEntries:
|
||||
normalizedBrands.length === 1
|
||||
? normalizedStores.map((storeName, storeIndex) => ({
|
||||
storeName,
|
||||
thirdCategories: [],
|
||||
sortOrder: storeIndex,
|
||||
}))
|
||||
: [],
|
||||
sortOrder: index,
|
||||
}));
|
||||
const defaultStepByKey = new Map(
|
||||
@ -633,9 +699,25 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
stores:
|
||||
fallbackBrandStoreEntries.length > 0
|
||||
? Array.from(
|
||||
new Set(fallbackBrandStoreEntries.flatMap((entry) => entry.stores))
|
||||
new Set(
|
||||
fallbackBrandStoreEntries.flatMap((entry) =>
|
||||
(entry.storeEntries || []).map((storeEntry) => storeEntry.storeName)
|
||||
)
|
||||
)
|
||||
)
|
||||
: normalizedStores,
|
||||
thirdCategories:
|
||||
fallbackBrandStoreEntries.length > 0
|
||||
? Array.from(
|
||||
new Set(
|
||||
fallbackBrandStoreEntries.flatMap((entry) =>
|
||||
(entry.storeEntries || []).flatMap(
|
||||
(storeEntry) => storeEntry.thirdCategories || []
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: normalizedThirdCategories,
|
||||
priceBands: normalizedPriceBands,
|
||||
brandStoreEntries: fallbackBrandStoreEntries,
|
||||
},
|
||||
@ -660,6 +742,12 @@ export function getAgentWorkflowStoreOptionsByBrand(params: {
|
||||
const matchedEntry = entries.find(
|
||||
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
|
||||
);
|
||||
const storeOptions = matchedEntry
|
||||
? (matchedEntry.storeEntries || []).map((entry) => entry.storeName).filter(Boolean)
|
||||
: [];
|
||||
if (storeOptions.length > 0) {
|
||||
return storeOptions;
|
||||
}
|
||||
if (matchedEntry && matchedEntry.stores.length > 0) {
|
||||
return matchedEntry.stores;
|
||||
}
|
||||
@ -667,6 +755,31 @@ export function getAgentWorkflowStoreOptionsByBrand(params: {
|
||||
return params.config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [];
|
||||
}
|
||||
|
||||
export function getAgentWorkflowThirdCategoryOptionsByBrandStore(params: {
|
||||
config?: AgentWorkflowUiConfig | null;
|
||||
brand?: string | null;
|
||||
store?: string | null;
|
||||
}) {
|
||||
const entries = getAgentWorkflowBrandStoreEntries(params.config);
|
||||
const normalizedBrand = String(params.brand || "").trim().toLowerCase();
|
||||
const normalizedStore = String(params.store || "").trim().toLowerCase();
|
||||
if (!normalizedBrand || !normalizedStore) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matchedEntry = entries.find(
|
||||
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
|
||||
);
|
||||
const matchedStoreEntry = matchedEntry?.storeEntries?.find(
|
||||
(storeEntry) => storeEntry.storeName.trim().toLowerCase() === normalizedStore
|
||||
);
|
||||
if (matchedStoreEntry && matchedStoreEntry.thirdCategories.length > 0) {
|
||||
return matchedStoreEntry.thirdCategories;
|
||||
}
|
||||
|
||||
return params.config?.inputOptions?.thirdCategories || defaultAgentWorkflowUiConfig.inputOptions?.thirdCategories || [];
|
||||
}
|
||||
|
||||
export function getAgentWorkflowPriceBandOptions(config?: AgentWorkflowUiConfig | null) {
|
||||
return config?.inputOptions?.priceBands || defaultAgentWorkflowUiConfig.inputOptions?.priceBands || [];
|
||||
}
|
||||
@ -749,6 +862,24 @@ export function getAgentWorkflowBlockedOptionKeys(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function collectAgentWorkflowOptionBranchKeys(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
targetKey: string
|
||||
): string[] {
|
||||
for (const option of options) {
|
||||
if (option.key === targetKey) {
|
||||
return flattenAgentWorkflowOptionTree([option]).map(({ option: currentOption }) => currentOption.key);
|
||||
}
|
||||
|
||||
const childKeys = collectAgentWorkflowOptionBranchKeys(option.children || [], targetKey);
|
||||
if (childKeys.length > 0) {
|
||||
return childKeys;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||||
config?: AgentWorkflowUiConfig | null;
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
@ -759,7 +890,7 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||||
params.selections
|
||||
).map(({ step }) => step);
|
||||
|
||||
return Array.from(
|
||||
const directlyBlockedKeys = Array.from(
|
||||
new Set(
|
||||
visibleSteps.flatMap((step) =>
|
||||
getAgentWorkflowBlockedOptionKeys({
|
||||
@ -769,6 +900,18 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return Array.from(
|
||||
new Set(
|
||||
directlyBlockedKeys.flatMap((blockedKey) => {
|
||||
const branchKeys = collectAgentWorkflowOptionBranchKeys(
|
||||
visibleSteps.flatMap((step) => step.options || []),
|
||||
blockedKey
|
||||
);
|
||||
return branchKeys.length > 0 ? branchKeys : [blockedKey];
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isAgentWorkflowStepSelectionComplete(params: {
|
||||
|
||||
@ -617,6 +617,8 @@ export const canvasProject = pgTable("CanvasProject", {
|
||||
secondarySku?: string;
|
||||
brand?: string;
|
||||
store?: string;
|
||||
thirdCategory?: string;
|
||||
priceBand?: string;
|
||||
};
|
||||
materials?: {
|
||||
primary?: {
|
||||
|
||||
@ -105,15 +105,23 @@ export interface AgentWorkflowUiConfig {
|
||||
inputOptions?: {
|
||||
brands: string[];
|
||||
stores: string[];
|
||||
thirdCategories?: string[];
|
||||
priceBands?: string[];
|
||||
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
|
||||
};
|
||||
steps: AgentWorkflowStepUiConfig[];
|
||||
}
|
||||
|
||||
export interface AgentWorkflowStoreEntry {
|
||||
storeName: string;
|
||||
thirdCategories: string[];
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface AgentWorkflowBrandStoreEntry {
|
||||
brandName: string;
|
||||
stores: string[];
|
||||
storeEntries?: AgentWorkflowStoreEntry[];
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
@ -249,6 +257,7 @@ export interface AgentWorkflowComposerContext {
|
||||
secondarySku?: string;
|
||||
brand?: string;
|
||||
store?: string;
|
||||
thirdCategory?: string;
|
||||
priceBand?: string;
|
||||
selections: AgentWorkflowComposerSelection[];
|
||||
referenceImages: AgentWorkflowReferenceImage[];
|
||||
@ -382,6 +391,7 @@ export interface AgentWorkflowPersistedState {
|
||||
secondarySku: string;
|
||||
brand: string;
|
||||
store: string;
|
||||
thirdCategory: string;
|
||||
priceBand: string;
|
||||
};
|
||||
materials?: AgentWorkflowMaterialSelections;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user