feat: add brand import and brand-aware psd settings
This commit is contained in:
parent
ea6a9a9d76
commit
ab0dc8bce3
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { useEffect, useMemo, useRef, useState, type ChangeEvent, type ReactNode } from "react";
|
||||
import useSWR from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
@ -11,6 +11,7 @@ import {
|
||||
ArrowUp,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Download,
|
||||
Image as ImageIcon,
|
||||
Layers,
|
||||
Loader2,
|
||||
@ -67,12 +68,18 @@ import {
|
||||
import {
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
resolveAgentPsdTextColorConfigForBrand,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
type AgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||
import {
|
||||
AGENT_WORKFLOW_BRAND_IMPORT_ACCEPT,
|
||||
downloadAgentWorkflowBrandImportTemplate,
|
||||
parseAgentWorkflowBrandImportFile,
|
||||
} from "@/lib/agents/workflow-brand-import";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -85,10 +92,12 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
AgentPresetLibraryCategoryNode,
|
||||
AgentPresetLibraryConfig,
|
||||
AgentPsdTextColorBrandConfig,
|
||||
AgentPresetLibrarySection,
|
||||
AgentWorkflowBrandModelConfig,
|
||||
AgentWorkflowBrandStoreEntry,
|
||||
@ -685,12 +694,15 @@ export function AgentWorkflowSettingsClient({
|
||||
const [activeTab, setActiveTab] = useState("showcase");
|
||||
const [brandDraft, setBrandDraft] = useState("");
|
||||
const [priceBandDraft, setPriceBandDraft] = useState("");
|
||||
const [selectedPsdTextColorBrand, setSelectedPsdTextColorBrand] = useState("");
|
||||
const [storeDrafts, setStoreDrafts] = useState<Record<string, string>>({});
|
||||
const [thirdCategoryDrafts, setThirdCategoryDrafts] = useState<Record<string, string>>({});
|
||||
const [fourthCategoryDrafts, setFourthCategoryDrafts] = useState<Record<string, string>>({});
|
||||
const [fifthCategoryDrafts, setFifthCategoryDrafts] = useState<Record<string, string>>({});
|
||||
const [blockedOptionSearches, setBlockedOptionSearches] = useState<Record<string, string>>({});
|
||||
const [isImportingBrandSheet, setIsImportingBrandSheet] = useState(false);
|
||||
const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
|
||||
const brandImportInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
@ -760,6 +772,36 @@ export function AgentWorkflowSettingsClient({
|
||||
])
|
||||
).sort((left, right) => left.localeCompare(right, "zh-CN"));
|
||||
}, [brandModelConfig.entries, configuredBrands]);
|
||||
const psdTextColorBrandOptions = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
new Set([
|
||||
...configuredBrands,
|
||||
...(psdTextColorConfig.brandConfigs || []).map((entry) => entry.brandName),
|
||||
])
|
||||
).sort((left, right) => left.localeCompare(right, "zh-CN")),
|
||||
[configuredBrands, psdTextColorConfig.brandConfigs]
|
||||
);
|
||||
const selectedBrandPsdTextColorConfig = useMemo(
|
||||
() => resolveAgentPsdTextColorConfigForBrand(psdTextColorConfig, selectedPsdTextColorBrand),
|
||||
[psdTextColorConfig, selectedPsdTextColorBrand]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (psdTextColorBrandOptions.length === 0) {
|
||||
if (selectedPsdTextColorBrand) {
|
||||
setSelectedPsdTextColorBrand("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCurrentSelection = psdTextColorBrandOptions.some(
|
||||
(brandName) => brandName === selectedPsdTextColorBrand
|
||||
);
|
||||
if (!hasCurrentSelection) {
|
||||
setSelectedPsdTextColorBrand(psdTextColorBrandOptions[0] || "");
|
||||
}
|
||||
}, [psdTextColorBrandOptions, selectedPsdTextColorBrand]);
|
||||
|
||||
const sortedSteps = useMemo(
|
||||
() => [...config.steps].sort((left, right) => left.sortOrder - right.sortOrder),
|
||||
@ -1670,13 +1712,67 @@ export function AgentWorkflowSettingsClient({
|
||||
}
|
||||
};
|
||||
|
||||
const updatePsdTextColor = (index: 0 | 1, value: string) => {
|
||||
const normalized = normalizeHexColor(value, psdTextColorConfig.colors[index] || "#111111");
|
||||
const updateSelectedBrandPsdTextColorConfig = (
|
||||
updater: (current: AgentPsdTextColorBrandConfig | AgentPsdTextColorConfig) => {
|
||||
enabled: boolean;
|
||||
colors: [string, string];
|
||||
}
|
||||
) => {
|
||||
if (!selectedPsdTextColorBrand) {
|
||||
setPsdTextColorConfig((prev) => {
|
||||
const next = updater(prev);
|
||||
return {
|
||||
...prev,
|
||||
enabled: next.enabled,
|
||||
colors: next.colors,
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setPsdTextColorConfig((prev) => {
|
||||
const nextColors = [...prev.colors] as [string, string];
|
||||
nextColors[index] = normalized;
|
||||
const nextBrandConfigs = [...(prev.brandConfigs || [])];
|
||||
const matchedIndex = nextBrandConfigs.findIndex(
|
||||
(entry) =>
|
||||
entry.brandName.trim().toLowerCase() ===
|
||||
selectedPsdTextColorBrand.trim().toLowerCase()
|
||||
);
|
||||
const currentBrandConfig =
|
||||
matchedIndex >= 0
|
||||
? nextBrandConfigs[matchedIndex]
|
||||
: resolveAgentPsdTextColorConfigForBrand(prev, selectedPsdTextColorBrand);
|
||||
const next = updater(currentBrandConfig);
|
||||
const nextEntry: AgentPsdTextColorBrandConfig = {
|
||||
brandName: selectedPsdTextColorBrand,
|
||||
enabled: next.enabled,
|
||||
colors: next.colors,
|
||||
};
|
||||
|
||||
if (matchedIndex >= 0) {
|
||||
nextBrandConfigs[matchedIndex] = nextEntry;
|
||||
} else {
|
||||
nextBrandConfigs.push(nextEntry);
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
brandConfigs: nextBrandConfigs.sort((left, right) =>
|
||||
left.brandName.localeCompare(right.brandName, "zh-CN")
|
||||
),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const updatePsdTextColor = (index: 0 | 1, value: string) => {
|
||||
const normalized = normalizeHexColor(
|
||||
value,
|
||||
selectedBrandPsdTextColorConfig.colors[index] || "#111111"
|
||||
);
|
||||
updateSelectedBrandPsdTextColorConfig((current) => {
|
||||
const nextColors = [...current.colors] as [string, string];
|
||||
nextColors[index] = normalized;
|
||||
return {
|
||||
enabled: current.enabled,
|
||||
colors: nextColors,
|
||||
};
|
||||
});
|
||||
@ -1713,6 +1809,34 @@ export function AgentWorkflowSettingsClient({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadBrandTemplate = async () => {
|
||||
try {
|
||||
await downloadAgentWorkflowBrandImportTemplate();
|
||||
toast.success("品牌导入模板已开始下载");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "下载模板失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleBrandSheetImport = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsImportingBrandSheet(true);
|
||||
const { entries, rowCount } = await parseAgentWorkflowBrandImportFile(file);
|
||||
updateInputOptions([...brandStoreEntries, ...entries]);
|
||||
toast.success(`已从表格导入 ${rowCount} 行,品牌树已自动合并去重`);
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "导入表格失败");
|
||||
} finally {
|
||||
setIsImportingBrandSheet(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPresetLibraryNodeCard = (
|
||||
sectionId: string,
|
||||
node: AgentPresetLibraryCategoryNode,
|
||||
@ -3158,11 +3282,48 @@ export function AgentWorkflowSettingsClient({
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs">品牌树</Label>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{brandStoreEntries.length} 个品牌
|
||||
</span>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">品牌树</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
支持一键下载模板并导入品牌、店铺、三级、四级、五级分类表格
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{brandStoreEntries.length} 个品牌
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleDownloadBrandTemplate}
|
||||
className="h-8"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
下载模板
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => brandImportInputRef.current?.click()}
|
||||
className="h-8"
|
||||
disabled={isImportingBrandSheet}
|
||||
>
|
||||
{isImportingBrandSheet ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
导入表格
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref={brandImportInputRef}
|
||||
type="file"
|
||||
accept={AGENT_WORKFLOW_BRAND_IMPORT_ACCEPT}
|
||||
className="hidden"
|
||||
onChange={handleBrandSheetImport}
|
||||
/>
|
||||
<div className="rounded-xl border bg-muted/10 p-3">
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
@ -3647,29 +3808,57 @@ export function AgentWorkflowSettingsClient({
|
||||
<div>
|
||||
<CardTitle className="text-base">PSD 文字颜色选项</CardTitle>
|
||||
<CardDescription>
|
||||
控制套用 PSD 模板时,前台是否允许用户在两种预设文字颜色之间切换。
|
||||
按品牌配置套用 PSD 模板时可选的两组文字颜色,前台会跟随当前品牌读取。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{psdTextColorBrandOptions.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">选择品牌</Label>
|
||||
<SearchableSelect
|
||||
value={selectedPsdTextColorBrand}
|
||||
options={psdTextColorBrandOptions}
|
||||
placeholder="选择一个品牌后配置颜色"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
onValueChange={setSelectedPsdTextColorBrand}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
未单独配置的品牌会回退系统默认颜色;当前编辑品牌:{selectedPsdTextColorBrand}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/10 px-4 py-3 text-sm text-muted-foreground">
|
||||
请先在上方“品牌树”里配置品牌,再按品牌设置 PSD 文字颜色。
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">启用 PSD 文字双色切换</div>
|
||||
<div className="text-sm font-medium">
|
||||
{selectedPsdTextColorBrand
|
||||
? `启用“${selectedPsdTextColorBrand}”PSD 文字双色切换`
|
||||
: "启用 PSD 文字双色切换"}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
开启后,用户在 PSD 模板编辑弹窗里可为文字图层选择两种系统预设颜色。
|
||||
开启后,用户在 PSD 模板编辑弹窗里可为当前品牌的文字图层选择两种预设颜色。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={psdTextColorConfig.enabled}
|
||||
checked={selectedBrandPsdTextColorConfig.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setPsdTextColorConfig((prev) => ({ ...prev, enabled: checked }))
|
||||
updateSelectedBrandPsdTextColorConfig((current) => ({
|
||||
enabled: checked,
|
||||
colors: current.colors,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{psdTextColorConfig.colors.map((color, index) => (
|
||||
{selectedBrandPsdTextColorConfig.colors.map((color, index) => (
|
||||
<div
|
||||
key={`psd-text-color-${index + 1}`}
|
||||
className="rounded-xl border bg-muted/20 p-4"
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
resolveAgentPsdTextColorConfigForBrand,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||
@ -141,9 +142,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
templates,
|
||||
textColorConfig: textColorSetting
|
||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
textColorConfig: resolveAgentPsdTextColorConfigForBrand(
|
||||
textColorSetting
|
||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
requestedBrand
|
||||
),
|
||||
secondaryCategoryConfig: secondaryCategorySetting
|
||||
? normalizeAgentPsdTemplateSecondaryCategoryConfig(secondaryCategorySetting.value)
|
||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
|
||||
@ -872,6 +872,16 @@ const updateSchema = z.object({
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
colors: z.array(z.string().trim().max(32)).length(2).optional(),
|
||||
brandConfigs: z
|
||||
.array(
|
||||
z.object({
|
||||
brandName: z.string().trim().min(1).max(64),
|
||||
enabled: z.boolean().optional(),
|
||||
colors: z.array(z.string().trim().max(32)).length(2).optional(),
|
||||
})
|
||||
)
|
||||
.max(300)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
agentPsdTemplateSecondaryCategoryConfig: z
|
||||
|
||||
@ -326,8 +326,85 @@ export function AgentWorkflowBrandStorePanel({
|
||||
() => getAgentWorkflowPriceBandOptions(config),
|
||||
[config]
|
||||
);
|
||||
const hasThirdCategoryLevel = useMemo(
|
||||
() =>
|
||||
brandStoreEntries.some((entry) =>
|
||||
(entry.storeEntries || []).some(
|
||||
(storeEntry) =>
|
||||
(storeEntry.thirdCategoryEntries || []).length > 0 ||
|
||||
(storeEntry.thirdCategories || []).length > 0
|
||||
)
|
||||
) || Boolean(config?.inputOptions?.thirdCategories?.length),
|
||||
[brandStoreEntries, config?.inputOptions?.thirdCategories]
|
||||
);
|
||||
const hasFourthCategoryLevel = useMemo(
|
||||
() =>
|
||||
hasThirdCategoryLevel &&
|
||||
(brandStoreEntries.some((entry) =>
|
||||
(entry.storeEntries || []).some((storeEntry) =>
|
||||
(storeEntry.thirdCategoryEntries || []).some(
|
||||
(thirdEntry) =>
|
||||
(thirdEntry.fourthCategoryEntries || []).length > 0 ||
|
||||
(thirdEntry.fourthCategories || []).length > 0
|
||||
)
|
||||
)
|
||||
) || Boolean(config?.inputOptions?.fourthCategories?.length)),
|
||||
[brandStoreEntries, config?.inputOptions?.fourthCategories, hasThirdCategoryLevel]
|
||||
);
|
||||
const hasFifthCategoryLevel = useMemo(
|
||||
() =>
|
||||
hasFourthCategoryLevel &&
|
||||
(brandStoreEntries.some((entry) =>
|
||||
(entry.storeEntries || []).some((storeEntry) =>
|
||||
(storeEntry.thirdCategoryEntries || []).some((thirdEntry) =>
|
||||
(thirdEntry.fourthCategoryEntries || []).some(
|
||||
(fourthEntry) => (fourthEntry.fifthCategories || []).length > 0
|
||||
)
|
||||
)
|
||||
)
|
||||
) || Boolean(config?.inputOptions?.fifthCategories?.length)),
|
||||
[brandStoreEntries, config?.inputOptions?.fifthCategories, hasFourthCategoryLevel]
|
||||
);
|
||||
const isDark = theme === "dark";
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasThirdCategoryLevel) {
|
||||
if (inputs.thirdCategory || inputs.fourthCategory || inputs.fifthCategory) {
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
thirdCategory: "",
|
||||
fourthCategory: "",
|
||||
fifthCategory: "",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasFourthCategoryLevel) {
|
||||
if (inputs.fourthCategory || inputs.fifthCategory) {
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
fourthCategory: "",
|
||||
fifthCategory: "",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasFifthCategoryLevel && inputs.fifthCategory) {
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
fifthCategory: "",
|
||||
});
|
||||
}
|
||||
}, [
|
||||
hasFifthCategoryLevel,
|
||||
hasFourthCategoryLevel,
|
||||
hasThirdCategoryLevel,
|
||||
inputs,
|
||||
onInputsChange,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -389,55 +466,61 @@ export function AgentWorkflowBrandStorePanel({
|
||||
})
|
||||
}
|
||||
/>
|
||||
<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,
|
||||
fourthCategory:
|
||||
value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase()
|
||||
? inputs.fourthCategory
|
||||
: "",
|
||||
fifthCategory: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.fourthCategory}
|
||||
options={fourthCategoryOptions}
|
||||
placeholder="选择四级分类"
|
||||
searchPlaceholder="输入四级分类筛选"
|
||||
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
|
||||
theme={theme}
|
||||
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
fourthCategory: value,
|
||||
fifthCategory:
|
||||
value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase()
|
||||
? inputs.fifthCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.fifthCategory}
|
||||
options={fifthCategoryOptions}
|
||||
placeholder="选择五级分类"
|
||||
searchPlaceholder="输入五级分类筛选"
|
||||
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
|
||||
theme={theme}
|
||||
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
|
||||
/>
|
||||
{hasThirdCategoryLevel ? (
|
||||
<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,
|
||||
fourthCategory:
|
||||
value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase()
|
||||
? inputs.fourthCategory
|
||||
: "",
|
||||
fifthCategory: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{hasFourthCategoryLevel ? (
|
||||
<SearchableSelect
|
||||
value={inputs.fourthCategory}
|
||||
options={fourthCategoryOptions}
|
||||
placeholder="选择四级分类"
|
||||
searchPlaceholder="输入四级分类筛选"
|
||||
emptyText={inputs.thirdCategory ? "当前三级分类下暂无四级分类" : "请先选择三级分类"}
|
||||
theme={theme}
|
||||
disabled={!inputs.thirdCategory || fourthCategoryOptions.length === 0}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
fourthCategory: value,
|
||||
fifthCategory:
|
||||
value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase()
|
||||
? inputs.fifthCategory
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{hasFifthCategoryLevel ? (
|
||||
<SearchableSelect
|
||||
value={inputs.fifthCategory}
|
||||
options={fifthCategoryOptions}
|
||||
placeholder="选择五级分类"
|
||||
searchPlaceholder="输入五级分类筛选"
|
||||
emptyText={inputs.fourthCategory ? "当前四级分类下暂无五级分类" : "请先选择四级分类"}
|
||||
theme={theme}
|
||||
disabled={!inputs.fourthCategory || fifthCategoryOptions.length === 0}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, fifthCategory: value })}
|
||||
/>
|
||||
) : null}
|
||||
<SearchableSelect
|
||||
value={inputs.priceBand}
|
||||
options={priceBandOptions}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { AgentPsdTextColorConfig } from "@/types/agent";
|
||||
import type { AgentPsdTextColorBrandConfig, AgentPsdTextColorConfig } from "@/types/agent";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
|
||||
export const AGENT_PSD_TEXT_COLOR_CONFIG_KEY = "agent_psd_text_color_config";
|
||||
@ -6,8 +6,27 @@ export const AGENT_PSD_TEXT_COLOR_CONFIG_KEY = "agent_psd_text_color_config";
|
||||
export const defaultAgentPsdTextColorConfig: AgentPsdTextColorConfig = {
|
||||
enabled: false,
|
||||
colors: ["#ffffff", "#111111"],
|
||||
brandConfigs: [],
|
||||
};
|
||||
|
||||
function normalizeBrandName(value: unknown) {
|
||||
return String(value || "").trim().slice(0, 64);
|
||||
}
|
||||
|
||||
function normalizeColors(value: unknown, fallback: [string, string]) {
|
||||
const colors = Array.isArray(value)
|
||||
? value
|
||||
.map((item) => normalizeHexColor(String(item || ""), ""))
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
: [];
|
||||
|
||||
return [
|
||||
colors[0] || fallback[0],
|
||||
colors[1] || colors[0] || fallback[1],
|
||||
] as [string, string];
|
||||
}
|
||||
|
||||
export function normalizeAgentPsdTextColorConfig(
|
||||
value: unknown
|
||||
): AgentPsdTextColorConfig {
|
||||
@ -16,20 +35,56 @@ export function normalizeAgentPsdTextColorConfig(
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const colors = Array.isArray(raw.colors)
|
||||
? raw.colors
|
||||
.map((item) => normalizeHexColor(String(item || ""), ""))
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
: [];
|
||||
const baseColors = normalizeColors(raw.colors, defaultAgentPsdTextColorConfig.colors);
|
||||
const uniqueBrandConfigs = new Map<string, AgentPsdTextColorBrandConfig>();
|
||||
if (Array.isArray(raw.brandConfigs)) {
|
||||
raw.brandConfigs.forEach((item) => {
|
||||
const record =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
? (item as Record<string, unknown>)
|
||||
: {};
|
||||
const brandName = normalizeBrandName(record.brandName);
|
||||
if (!brandName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [firstColor, secondColor] = [
|
||||
colors[0] || defaultAgentPsdTextColorConfig.colors[0],
|
||||
colors[1] || colors[0] || defaultAgentPsdTextColorConfig.colors[1],
|
||||
];
|
||||
uniqueBrandConfigs.set(brandName.toLowerCase(), {
|
||||
brandName,
|
||||
enabled: record.enabled === true,
|
||||
colors: normalizeColors(record.colors, baseColors),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: raw.enabled === true,
|
||||
colors: [firstColor, secondColor],
|
||||
colors: baseColors,
|
||||
brandConfigs: Array.from(uniqueBrandConfigs.values()).sort((left, right) =>
|
||||
left.brandName.localeCompare(right.brandName, "zh-CN")
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAgentPsdTextColorConfigForBrand(
|
||||
config: AgentPsdTextColorConfig | null | undefined,
|
||||
brandName?: string | null
|
||||
): AgentPsdTextColorConfig {
|
||||
const normalized = normalizeAgentPsdTextColorConfig(config);
|
||||
const requestedBrand = normalizeBrandName(brandName);
|
||||
if (!requestedBrand) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const matchedBrandConfig = (normalized.brandConfigs || []).find(
|
||||
(entry) => entry.brandName.toLowerCase() === requestedBrand.toLowerCase()
|
||||
);
|
||||
if (!matchedBrandConfig) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return {
|
||||
...normalized,
|
||||
enabled: matchedBrandConfig.enabled,
|
||||
colors: matchedBrandConfig.colors,
|
||||
};
|
||||
}
|
||||
|
||||
417
lib/agents/workflow-brand-import.ts
Normal file
417
lib/agents/workflow-brand-import.ts
Normal file
@ -0,0 +1,417 @@
|
||||
import Papa from "papaparse";
|
||||
import type {
|
||||
AgentWorkflowBrandStoreEntry,
|
||||
AgentWorkflowFourthCategoryEntry,
|
||||
AgentWorkflowStoreEntry,
|
||||
AgentWorkflowThirdCategoryEntry,
|
||||
} from "@/types/agent";
|
||||
|
||||
type SpreadsheetRecord = Record<string, unknown>;
|
||||
|
||||
export const AGENT_WORKFLOW_BRAND_IMPORT_ACCEPT = ".xlsx,.xls,.csv";
|
||||
|
||||
const BRAND_SHEET_NAME = "品牌树模板";
|
||||
const INSTRUCTION_SHEET_NAME = "填写说明";
|
||||
const TEMPLATE_FILE_NAME = "智能体品牌树导入模板";
|
||||
|
||||
const TEMPLATE_HEADERS = {
|
||||
brandName: "品牌",
|
||||
storeName: "店铺",
|
||||
thirdCategory: "三级分类",
|
||||
fourthCategory: "四级分类",
|
||||
fifthCategory: "五级分类",
|
||||
} as const;
|
||||
|
||||
const HEADER_ALIASES = {
|
||||
brandName: ["品牌", "品牌名称", "brand", "brandname"],
|
||||
storeName: ["店铺", "店铺名称", "shop", "shopname", "store", "storename"],
|
||||
thirdCategory: ["三级分类", "三级类目", "thirdcategory", "third"],
|
||||
fourthCategory: ["四级分类", "四级类目", "fourthcategory", "fourth"],
|
||||
fifthCategory: ["五级分类", "五级类目", "fifthcategory", "fifth"],
|
||||
} as const;
|
||||
|
||||
const TEMPLATE_SAMPLE_ROWS = [
|
||||
{
|
||||
brandName: "朴美",
|
||||
storeName: "天猫朴美家居旗舰店",
|
||||
thirdCategory: "桌类",
|
||||
fourthCategory: "餐桌",
|
||||
fifthCategory: "岩板餐桌",
|
||||
},
|
||||
{
|
||||
brandName: "朴美",
|
||||
storeName: "天猫朴美家居旗舰店",
|
||||
thirdCategory: "桌类",
|
||||
fourthCategory: "茶几",
|
||||
fifthCategory: "圆形茶几",
|
||||
},
|
||||
{
|
||||
brandName: "朴美",
|
||||
storeName: "天猫朴美家居旗舰店",
|
||||
thirdCategory: "椅类",
|
||||
fourthCategory: "餐椅",
|
||||
fifthCategory: "软包餐椅",
|
||||
},
|
||||
{
|
||||
brandName: "朴美",
|
||||
storeName: "京东朴美家居旗舰店",
|
||||
thirdCategory: "柜类",
|
||||
fourthCategory: "电视柜",
|
||||
fifthCategory: "落地电视柜",
|
||||
},
|
||||
];
|
||||
|
||||
function normalizeCell(value: unknown, limit = 64) {
|
||||
const normalized = String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
return normalized.slice(0, limit);
|
||||
}
|
||||
|
||||
function normalizeHeader(value: unknown) {
|
||||
return String(value ?? "")
|
||||
.replace(/\s+/g, "")
|
||||
.replace(/[_-]+/g, "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function triggerBlobDownload(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = fileName;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function matchColumn(record: SpreadsheetRecord, aliases: readonly string[]) {
|
||||
const aliasSet = new Set(aliases.map((alias) => normalizeHeader(alias)));
|
||||
return Object.keys(record).find((key) => aliasSet.has(normalizeHeader(key))) || null;
|
||||
}
|
||||
|
||||
async function readSpreadsheetRecords(file: File) {
|
||||
const extension = file.name.toLowerCase().split(".").pop() || "";
|
||||
if (extension === "csv") {
|
||||
const text = await file.text();
|
||||
const parsed = Papa.parse<SpreadsheetRecord>(text, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transformHeader: (header) => normalizeCell(header, 128),
|
||||
});
|
||||
|
||||
if (parsed.errors.length > 0) {
|
||||
throw new Error(`CSV 解析失败:${parsed.errors[0]?.message || "未知错误"}`);
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
const { read, utils } = await import("xlsx");
|
||||
const buffer = await file.arrayBuffer();
|
||||
const workbook = read(buffer, { type: "array" });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
if (!sheetName) {
|
||||
throw new Error("表格中没有可读取的工作表");
|
||||
}
|
||||
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
return utils.sheet_to_json<SpreadsheetRecord>(worksheet, {
|
||||
defval: "",
|
||||
});
|
||||
}
|
||||
|
||||
function createBrandEntry(brandName: string, sortOrder: number): AgentWorkflowBrandStoreEntry {
|
||||
return {
|
||||
brandName,
|
||||
stores: [],
|
||||
storeEntries: [],
|
||||
sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
function createStoreEntry(storeName: string, sortOrder: number): AgentWorkflowStoreEntry {
|
||||
return {
|
||||
storeName,
|
||||
thirdCategories: [],
|
||||
thirdCategoryEntries: [],
|
||||
sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
function createThirdCategoryEntry(
|
||||
categoryName: string,
|
||||
sortOrder: number
|
||||
): AgentWorkflowThirdCategoryEntry {
|
||||
return {
|
||||
categoryName,
|
||||
fourthCategories: [],
|
||||
fourthCategoryEntries: [],
|
||||
sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
function createFourthCategoryEntry(
|
||||
categoryName: string,
|
||||
sortOrder: number
|
||||
): AgentWorkflowFourthCategoryEntry {
|
||||
return {
|
||||
categoryName,
|
||||
fifthCategories: [],
|
||||
sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ParsedAgentWorkflowBrandImportResult {
|
||||
entries: AgentWorkflowBrandStoreEntry[];
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
export async function downloadAgentWorkflowBrandImportTemplate() {
|
||||
const { Workbook } = await import("exceljs");
|
||||
|
||||
const workbook = new Workbook();
|
||||
const sheet = workbook.addWorksheet(BRAND_SHEET_NAME);
|
||||
sheet.columns = [
|
||||
{ header: TEMPLATE_HEADERS.brandName, key: "brandName", width: 20 },
|
||||
{ header: TEMPLATE_HEADERS.storeName, key: "storeName", width: 28 },
|
||||
{ header: TEMPLATE_HEADERS.thirdCategory, key: "thirdCategory", width: 18 },
|
||||
{ header: TEMPLATE_HEADERS.fourthCategory, key: "fourthCategory", width: 18 },
|
||||
{ header: TEMPLATE_HEADERS.fifthCategory, key: "fifthCategory", width: 18 },
|
||||
];
|
||||
sheet.getRow(1).font = { bold: true };
|
||||
sheet.views = [{ state: "frozen", ySplit: 1 }];
|
||||
|
||||
TEMPLATE_SAMPLE_ROWS.forEach((row) => {
|
||||
sheet.addRow(row);
|
||||
});
|
||||
|
||||
const instructionSheet = workbook.addWorksheet(INSTRUCTION_SHEET_NAME);
|
||||
instructionSheet.columns = [
|
||||
{ header: "字段", key: "field", width: 18 },
|
||||
{ header: "是否必填", key: "required", width: 12 },
|
||||
{ header: "说明", key: "description", width: 54 },
|
||||
{ header: "示例", key: "example", width: 28 },
|
||||
];
|
||||
instructionSheet.getRow(1).font = { bold: true };
|
||||
instructionSheet.views = [{ state: "frozen", ySplit: 1 }];
|
||||
instructionSheet.addRows([
|
||||
{
|
||||
field: "品牌",
|
||||
required: "是",
|
||||
description: "每一行都必须填写品牌名称;相同品牌会自动合并。",
|
||||
example: "朴美",
|
||||
},
|
||||
{
|
||||
field: "店铺",
|
||||
required: "否",
|
||||
description: "不填时只导入品牌;填写后会挂到对应品牌下。",
|
||||
example: "天猫朴美家居旗舰店",
|
||||
},
|
||||
{
|
||||
field: "三级分类",
|
||||
required: "否",
|
||||
description: "填写后会挂到对应店铺下;如果不填,四级/五级也必须留空。",
|
||||
example: "桌类",
|
||||
},
|
||||
{
|
||||
field: "四级分类",
|
||||
required: "否",
|
||||
description: "填写后会挂到对应三级分类下;如果不填,五级也必须留空。",
|
||||
example: "餐桌",
|
||||
},
|
||||
{
|
||||
field: "五级分类",
|
||||
required: "否",
|
||||
description: "填写后会挂到对应四级分类下。",
|
||||
example: "岩板餐桌",
|
||||
},
|
||||
]);
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer();
|
||||
const blob = new Blob([buffer], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
});
|
||||
triggerBlobDownload(blob, `${TEMPLATE_FILE_NAME}.xlsx`);
|
||||
}
|
||||
|
||||
export async function parseAgentWorkflowBrandImportFile(
|
||||
file: File
|
||||
): Promise<ParsedAgentWorkflowBrandImportResult> {
|
||||
const extension = file.name.toLowerCase().split(".").pop() || "";
|
||||
if (!["xls", "xlsx", "csv"].includes(extension)) {
|
||||
throw new Error("仅支持 xls、xlsx、csv 文件");
|
||||
}
|
||||
|
||||
const records = await readSpreadsheetRecords(file);
|
||||
if (records.length === 0) {
|
||||
throw new Error("表格中没有有效数据");
|
||||
}
|
||||
|
||||
const firstRecord = records[0];
|
||||
const brandColumn =
|
||||
matchColumn(firstRecord, HEADER_ALIASES.brandName) ||
|
||||
Object.keys(firstRecord).find((key) => normalizeHeader(key) === normalizeHeader(TEMPLATE_HEADERS.brandName));
|
||||
if (!brandColumn) {
|
||||
throw new Error("未找到品牌列,请使用模板文件填写");
|
||||
}
|
||||
|
||||
const storeColumn = matchColumn(firstRecord, HEADER_ALIASES.storeName);
|
||||
const thirdColumn = matchColumn(firstRecord, HEADER_ALIASES.thirdCategory);
|
||||
const fourthColumn = matchColumn(firstRecord, HEADER_ALIASES.fourthCategory);
|
||||
const fifthColumn = matchColumn(firstRecord, HEADER_ALIASES.fifthCategory);
|
||||
|
||||
const brandEntries: AgentWorkflowBrandStoreEntry[] = [];
|
||||
const brandLookup = new Map<string, AgentWorkflowBrandStoreEntry>();
|
||||
let effectiveRowCount = 0;
|
||||
|
||||
const getOrCreateBrand = (brandName: string) => {
|
||||
const lookupKey = brandName.toLowerCase();
|
||||
const existing = brandLookup.get(lookupKey);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const nextEntry = createBrandEntry(brandName, brandEntries.length);
|
||||
brandEntries.push(nextEntry);
|
||||
brandLookup.set(lookupKey, nextEntry);
|
||||
return nextEntry;
|
||||
};
|
||||
|
||||
const getOrCreateStore = (brandEntry: AgentWorkflowBrandStoreEntry, storeName: string) => {
|
||||
const storeEntries = brandEntry.storeEntries || [];
|
||||
const existing = storeEntries.find(
|
||||
(entry) => entry.storeName.toLowerCase() === storeName.toLowerCase()
|
||||
);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const nextEntry = createStoreEntry(storeName, storeEntries.length);
|
||||
brandEntry.storeEntries = [...storeEntries, nextEntry];
|
||||
brandEntry.stores = brandEntry.storeEntries.map((entry) => entry.storeName);
|
||||
return nextEntry;
|
||||
};
|
||||
|
||||
const getOrCreateThirdCategory = (storeEntry: AgentWorkflowStoreEntry, categoryName: string) => {
|
||||
const thirdEntries = storeEntry.thirdCategoryEntries || [];
|
||||
const existing = thirdEntries.find(
|
||||
(entry) => entry.categoryName.toLowerCase() === categoryName.toLowerCase()
|
||||
);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const nextEntry = createThirdCategoryEntry(categoryName, thirdEntries.length);
|
||||
storeEntry.thirdCategoryEntries = [...thirdEntries, nextEntry];
|
||||
storeEntry.thirdCategories = storeEntry.thirdCategoryEntries.map((entry) => entry.categoryName);
|
||||
return nextEntry;
|
||||
};
|
||||
|
||||
const getOrCreateFourthCategory = (
|
||||
thirdEntry: AgentWorkflowThirdCategoryEntry,
|
||||
categoryName: string
|
||||
) => {
|
||||
const fourthEntries = thirdEntry.fourthCategoryEntries || [];
|
||||
const existing = fourthEntries.find(
|
||||
(entry) => entry.categoryName.toLowerCase() === categoryName.toLowerCase()
|
||||
);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const nextEntry = createFourthCategoryEntry(categoryName, fourthEntries.length);
|
||||
thirdEntry.fourthCategoryEntries = [...fourthEntries, nextEntry];
|
||||
thirdEntry.fourthCategories = thirdEntry.fourthCategoryEntries.map((entry) => entry.categoryName);
|
||||
return nextEntry;
|
||||
};
|
||||
|
||||
records.forEach((record, index) => {
|
||||
const lineNumber = index + 2;
|
||||
const brandName = normalizeCell(record[brandColumn]);
|
||||
const storeName = storeColumn ? normalizeCell(record[storeColumn]) : "";
|
||||
const thirdCategory = thirdColumn ? normalizeCell(record[thirdColumn]) : "";
|
||||
const fourthCategory = fourthColumn ? normalizeCell(record[fourthColumn]) : "";
|
||||
const fifthCategory = fifthColumn ? normalizeCell(record[fifthColumn]) : "";
|
||||
|
||||
const hasAnyValue = Boolean(brandName || storeName || thirdCategory || fourthCategory || fifthCategory);
|
||||
if (!hasAnyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!brandName) {
|
||||
throw new Error(`第 ${lineNumber} 行缺少品牌名称`);
|
||||
}
|
||||
if (!storeName && (thirdCategory || fourthCategory || fifthCategory)) {
|
||||
throw new Error(`第 ${lineNumber} 行缺少店铺名称,不能直接填写分类`);
|
||||
}
|
||||
if (!thirdCategory && (fourthCategory || fifthCategory)) {
|
||||
throw new Error(`第 ${lineNumber} 行缺少三级分类,不能直接填写四级或五级分类`);
|
||||
}
|
||||
if (!fourthCategory && fifthCategory) {
|
||||
throw new Error(`第 ${lineNumber} 行缺少四级分类,不能直接填写五级分类`);
|
||||
}
|
||||
|
||||
effectiveRowCount += 1;
|
||||
const brandEntry = getOrCreateBrand(brandName);
|
||||
if (!storeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storeEntry = getOrCreateStore(brandEntry, storeName);
|
||||
if (!thirdCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
const thirdEntry = getOrCreateThirdCategory(storeEntry, thirdCategory);
|
||||
if (!fourthCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fourthEntry = getOrCreateFourthCategory(thirdEntry, fourthCategory);
|
||||
if (!fifthCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!fourthEntry.fifthCategories.some(
|
||||
(item) => item.toLowerCase() === fifthCategory.toLowerCase()
|
||||
)
|
||||
) {
|
||||
fourthEntry.fifthCategories = [...fourthEntry.fifthCategories, fifthCategory];
|
||||
}
|
||||
});
|
||||
|
||||
if (effectiveRowCount === 0) {
|
||||
throw new Error("表格中没有可导入的有效行");
|
||||
}
|
||||
|
||||
return {
|
||||
entries: brandEntries.map((brandEntry, brandIndex) => ({
|
||||
...brandEntry,
|
||||
stores: (brandEntry.storeEntries || []).map((storeEntry) => storeEntry.storeName),
|
||||
storeEntries: (brandEntry.storeEntries || []).map((storeEntry, storeIndex) => ({
|
||||
...storeEntry,
|
||||
thirdCategories: (storeEntry.thirdCategoryEntries || []).map((thirdEntry) => thirdEntry.categoryName),
|
||||
thirdCategoryEntries: (storeEntry.thirdCategoryEntries || []).map((thirdEntry, thirdIndex) => ({
|
||||
...thirdEntry,
|
||||
fourthCategories: (thirdEntry.fourthCategoryEntries || []).map(
|
||||
(fourthEntry) => fourthEntry.categoryName
|
||||
),
|
||||
fourthCategoryEntries: (thirdEntry.fourthCategoryEntries || []).map(
|
||||
(fourthEntry, fourthIndex) => ({
|
||||
...fourthEntry,
|
||||
fifthCategories: Array.from(new Set((fourthEntry.fifthCategories || []).filter(Boolean))),
|
||||
sortOrder: fourthIndex,
|
||||
})
|
||||
),
|
||||
sortOrder: thirdIndex,
|
||||
})),
|
||||
sortOrder: storeIndex,
|
||||
})),
|
||||
sortOrder: brandIndex,
|
||||
})),
|
||||
rowCount: effectiveRowCount,
|
||||
};
|
||||
}
|
||||
@ -441,9 +441,16 @@ export interface AgentGenerationProject {
|
||||
updatedAt: Date | string;
|
||||
}
|
||||
|
||||
export interface AgentPsdTextColorBrandConfig {
|
||||
brandName: string;
|
||||
enabled: boolean;
|
||||
colors: [string, string];
|
||||
}
|
||||
|
||||
export interface AgentPsdTextColorConfig {
|
||||
enabled: boolean;
|
||||
colors: [string, string];
|
||||
brandConfigs?: AgentPsdTextColorBrandConfig[];
|
||||
}
|
||||
|
||||
export interface AgentCategoryTreeConfig {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user