feat: 支持PSD模板品类标签筛选
This commit is contained in:
parent
5037a1a1eb
commit
f6d8a4b8ea
@ -44,6 +44,12 @@ import {
|
|||||||
getAgentWorkflowCategoryLabels,
|
getAgentWorkflowCategoryLabels,
|
||||||
getAgentWorkflowCategoryOptionsByBrand,
|
getAgentWorkflowCategoryOptionsByBrand,
|
||||||
} from "@/lib/agents/workflow-ui-config";
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG,
|
||||||
|
getAgentPsdTemplateCategoryTagOptions,
|
||||||
|
normalizeAgentPsdTemplateCategoryTagConfig,
|
||||||
|
normalizeAgentPsdTemplateCategoryTags,
|
||||||
|
} from "@/lib/agents/psd-template-category-tag-config";
|
||||||
import type {
|
import type {
|
||||||
AgentPsdFont,
|
AgentPsdFont,
|
||||||
AgentPsdFontListResponse,
|
AgentPsdFontListResponse,
|
||||||
@ -98,6 +104,30 @@ function normalizeManagedBrandName(value?: string | null) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasTemplateCategoryTag(tags: string[] | undefined, target: string) {
|
||||||
|
const normalizedTarget = String(target || "").trim().toLowerCase();
|
||||||
|
if (!normalizedTarget) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (tags || []).some((tag) => tag.trim().toLowerCase() === normalizedTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTemplateCategoryTag(tags: string[] | undefined, target: string) {
|
||||||
|
const normalizedTags = normalizeAgentPsdTemplateCategoryTags(tags);
|
||||||
|
const normalizedTarget = normalizeAgentPsdTemplateCategoryTags([target])[0];
|
||||||
|
|
||||||
|
if (!normalizedTarget) {
|
||||||
|
return normalizedTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTemplateCategoryTag(normalizedTags, normalizedTarget)) {
|
||||||
|
return normalizedTags.filter((tag) => tag.trim().toLowerCase() !== normalizedTarget.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeAgentPsdTemplateCategoryTags([...normalizedTags, normalizedTarget]);
|
||||||
|
}
|
||||||
|
|
||||||
function cloneTemplate(template: AgentPsdTemplate | null) {
|
function cloneTemplate(template: AgentPsdTemplate | null) {
|
||||||
return template ? (JSON.parse(JSON.stringify(template)) as AgentPsdTemplate) : null;
|
return template ? (JSON.parse(JSON.stringify(template)) as AgentPsdTemplate) : null;
|
||||||
}
|
}
|
||||||
@ -178,8 +208,11 @@ export function PsdTemplatesSettingsClient({
|
|||||||
backLabel: string;
|
backLabel: string;
|
||||||
}) {
|
}) {
|
||||||
const { data, mutate } = useSWR<AgentPsdTemplateListResponse>("/api/admin/psd-templates", fetcher);
|
const { data, mutate } = useSWR<AgentPsdTemplateListResponse>("/api/admin/psd-templates", fetcher);
|
||||||
const { data: siteSettingsData } = useSWR<{
|
const { data: siteSettingsData, mutate: mutateSiteSettings } = useSWR<{
|
||||||
agentWorkflowUiConfig?: AgentWorkflowUiConfig;
|
agentWorkflowUiConfig?: AgentWorkflowUiConfig;
|
||||||
|
agentPsdTemplateCategoryTagConfig?: {
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
}>("/api/site-settings", fetcher);
|
}>("/api/site-settings", fetcher);
|
||||||
const { data: fontsData, mutate: mutateFonts } = useSWR<AgentPsdFontListResponse>(
|
const { data: fontsData, mutate: mutateFonts } = useSWR<AgentPsdFontListResponse>(
|
||||||
"/api/admin/psd-fonts",
|
"/api/admin/psd-fonts",
|
||||||
@ -218,6 +251,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
const [uploadBrandName, setUploadBrandName] = useState("");
|
const [uploadBrandName, setUploadBrandName] = useState("");
|
||||||
const [uploadMode, setUploadMode] = useState<AgentPsdTemplateMode>("main");
|
const [uploadMode, setUploadMode] = useState<AgentPsdTemplateMode>("main");
|
||||||
const [uploadSecondaryCategory, setUploadSecondaryCategory] = useState("");
|
const [uploadSecondaryCategory, setUploadSecondaryCategory] = useState("");
|
||||||
|
const [uploadCategoryTags, setUploadCategoryTags] = useState<string[]>([]);
|
||||||
const [uploadDescription, setUploadDescription] = useState("");
|
const [uploadDescription, setUploadDescription] = useState("");
|
||||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||||
const [fontUploadName, setFontUploadName] = useState("");
|
const [fontUploadName, setFontUploadName] = useState("");
|
||||||
@ -235,6 +269,8 @@ export function PsdTemplatesSettingsClient({
|
|||||||
const [deletingFontId, setDeletingFontId] = useState<string | null>(null);
|
const [deletingFontId, setDeletingFontId] = useState<string | null>(null);
|
||||||
const [savingTemplateBrandId, setSavingTemplateBrandId] = useState<string | null>(null);
|
const [savingTemplateBrandId, setSavingTemplateBrandId] = useState<string | null>(null);
|
||||||
const [savingFontBrandId, setSavingFontBrandId] = useState<string | null>(null);
|
const [savingFontBrandId, setSavingFontBrandId] = useState<string | null>(null);
|
||||||
|
const [categoryTagDraft, setCategoryTagDraft] = useState("");
|
||||||
|
const [isSavingCategoryTags, setIsSavingCategoryTags] = useState(false);
|
||||||
const [isFontListExpanded, setIsFontListExpanded] = useState(false);
|
const [isFontListExpanded, setIsFontListExpanded] = useState(false);
|
||||||
const [templateBrandDrafts, setTemplateBrandDrafts] = useState<Record<string, string>>({});
|
const [templateBrandDrafts, setTemplateBrandDrafts] = useState<Record<string, string>>({});
|
||||||
const [fontBrandDrafts, setFontBrandDrafts] = useState<Record<string, string>>({});
|
const [fontBrandDrafts, setFontBrandDrafts] = useState<Record<string, string>>({});
|
||||||
@ -249,6 +285,25 @@ export function PsdTemplatesSettingsClient({
|
|||||||
() => getAgentWorkflowCategoryLabels(workflowConfig),
|
() => getAgentWorkflowCategoryLabels(workflowConfig),
|
||||||
[workflowConfig]
|
[workflowConfig]
|
||||||
);
|
);
|
||||||
|
const categoryTagConfig = useMemo(
|
||||||
|
() =>
|
||||||
|
normalizeAgentPsdTemplateCategoryTagConfig(
|
||||||
|
siteSettingsData?.agentPsdTemplateCategoryTagConfig
|
||||||
|
),
|
||||||
|
[siteSettingsData?.agentPsdTemplateCategoryTagConfig]
|
||||||
|
);
|
||||||
|
const availableCategoryTagOptions = useMemo(
|
||||||
|
() => getAgentPsdTemplateCategoryTagOptions(categoryTagConfig),
|
||||||
|
[categoryTagConfig]
|
||||||
|
);
|
||||||
|
const draftTemplateCategoryTagOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
getAgentPsdTemplateCategoryTagOptions(
|
||||||
|
categoryTagConfig,
|
||||||
|
draftTemplate?.categoryTags || []
|
||||||
|
),
|
||||||
|
[categoryTagConfig, draftTemplate?.categoryTags]
|
||||||
|
);
|
||||||
const categoryLevelOptions = useMemo(
|
const categoryLevelOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -445,6 +500,81 @@ export function PsdTemplatesSettingsClient({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveCategoryTagConfig(nextTags: string[]) {
|
||||||
|
const normalized = normalizeAgentPsdTemplateCategoryTagConfig({
|
||||||
|
tags: nextTags,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSavingCategoryTags(true);
|
||||||
|
const response = await fetch("/api/site-settings", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
agentPsdTemplateCategoryTagConfig: normalized,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = (await response.json().catch(() => null)) as
|
||||||
|
| { error?: string }
|
||||||
|
| null;
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(payload?.error || "品类标签保存失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateSiteSettings();
|
||||||
|
return normalized.tags;
|
||||||
|
} finally {
|
||||||
|
setIsSavingCategoryTags(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAddCategoryTag() {
|
||||||
|
const nextTag = normalizeAgentPsdTemplateCategoryTags([categoryTagDraft])[0];
|
||||||
|
if (!nextTag) {
|
||||||
|
toast.error("请先输入品类标签");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextTag === AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG) {
|
||||||
|
toast.error("“适合所有”是固定标签,无需重复添加");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTemplateCategoryTag(categoryTagConfig.tags, nextTag)) {
|
||||||
|
toast.error("该品类标签已存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveCategoryTagConfig([...categoryTagConfig.tags, nextTag]);
|
||||||
|
setCategoryTagDraft("");
|
||||||
|
toast.success("品类标签已添加");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "品类标签保存失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRemoveCategoryTag(tag: string) {
|
||||||
|
const normalizedTag = normalizeAgentPsdTemplateCategoryTags([tag])[0];
|
||||||
|
if (!normalizedTag || normalizedTag === AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await saveCategoryTagConfig(
|
||||||
|
categoryTagConfig.tags.filter(
|
||||||
|
(item) => item.trim().toLowerCase() !== normalizedTag.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
toast.success("品类标签已移除");
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "品类标签保存失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleCreateTemplate(options?: { parseAfterUpload?: boolean }) {
|
async function handleCreateTemplate(options?: { parseAfterUpload?: boolean }) {
|
||||||
const parseAfterUpload = options?.parseAfterUpload === true;
|
const parseAfterUpload = options?.parseAfterUpload === true;
|
||||||
if (!uploadFile) {
|
if (!uploadFile) {
|
||||||
@ -462,6 +592,9 @@ export function PsdTemplatesSettingsClient({
|
|||||||
formData.set("brandName", uploadBrandName.trim());
|
formData.set("brandName", uploadBrandName.trim());
|
||||||
formData.set("mode", uploadMode);
|
formData.set("mode", uploadMode);
|
||||||
formData.set("secondaryCategory", uploadSecondaryCategory.trim());
|
formData.set("secondaryCategory", uploadSecondaryCategory.trim());
|
||||||
|
uploadCategoryTags.forEach((tag) => {
|
||||||
|
formData.append("categoryTags", tag);
|
||||||
|
});
|
||||||
formData.set("description", uploadDescription.trim());
|
formData.set("description", uploadDescription.trim());
|
||||||
formData.set("status", "disabled");
|
formData.set("status", "disabled");
|
||||||
formData.set("file", uploadFile);
|
formData.set("file", uploadFile);
|
||||||
@ -528,6 +661,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
setUploadName("");
|
setUploadName("");
|
||||||
setUploadBrandName("");
|
setUploadBrandName("");
|
||||||
setUploadSecondaryCategory("");
|
setUploadSecondaryCategory("");
|
||||||
|
setUploadCategoryTags([]);
|
||||||
setUploadDescription("");
|
setUploadDescription("");
|
||||||
setUploadFile(null);
|
setUploadFile(null);
|
||||||
await mutate();
|
await mutate();
|
||||||
@ -676,6 +810,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
brandName: normalizedBrandName,
|
brandName: normalizedBrandName,
|
||||||
mode: template.mode,
|
mode: template.mode,
|
||||||
secondaryCategory: template.secondaryCategory || "",
|
secondaryCategory: template.secondaryCategory || "",
|
||||||
|
categoryTags: template.categoryTags || [],
|
||||||
description: template.description || "",
|
description: template.description || "",
|
||||||
status: template.status,
|
status: template.status,
|
||||||
layers: template.layers,
|
layers: template.layers,
|
||||||
@ -721,6 +856,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
brandName: draftTemplate.brandName,
|
brandName: draftTemplate.brandName,
|
||||||
mode: draftTemplate.mode,
|
mode: draftTemplate.mode,
|
||||||
secondaryCategory: draftTemplate.secondaryCategory || "",
|
secondaryCategory: draftTemplate.secondaryCategory || "",
|
||||||
|
categoryTags: draftTemplate.categoryTags || [],
|
||||||
description: draftTemplate.description || "",
|
description: draftTemplate.description || "",
|
||||||
status: draftTemplate.status,
|
status: draftTemplate.status,
|
||||||
layers: draftTemplate.layers,
|
layers: draftTemplate.layers,
|
||||||
@ -942,6 +1078,73 @@ export function PsdTemplatesSettingsClient({
|
|||||||
|
|
||||||
<div className="container mx-auto grid gap-6 px-6 py-6 lg:grid-cols-[360px,minmax(0,1fr)]">
|
<div className="container mx-auto grid gap-6 px-6 py-6 lg:grid-cols-[360px,minmax(0,1fr)]">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">品类标签</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
管理 PSD 模板可绑定的品类标签。前端会根据用户当前选择的产品品类筛选模板。
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="psdCategoryTagDraft">新增标签</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="psdCategoryTagDraft"
|
||||||
|
value={categoryTagDraft}
|
||||||
|
onChange={(event) => setCategoryTagDraft(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleAddCategoryTag();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="例如:椅子类 / 沙发类 / 餐边柜类"
|
||||||
|
disabled={isSavingCategoryTags}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
void handleAddCategoryTag();
|
||||||
|
}}
|
||||||
|
disabled={isSavingCategoryTags || !categoryTagDraft.trim()}
|
||||||
|
>
|
||||||
|
添加标签
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>当前标签</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="default">{AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG}</Badge>
|
||||||
|
{categoryTagConfig.tags.map((tag) => (
|
||||||
|
<div
|
||||||
|
key={tag}
|
||||||
|
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1 text-sm"
|
||||||
|
>
|
||||||
|
<span>{tag}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-muted-foreground transition hover:text-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
void handleRemoveCategoryTag(tag);
|
||||||
|
}}
|
||||||
|
disabled={isSavingCategoryTags}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
“适合所有”是固定标签,会让模板在所有品类下都显示;自定义标签删除后,不会再出现在新模板选择项中。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">上传新模板</CardTitle>
|
<CardTitle className="text-base">上传新模板</CardTitle>
|
||||||
@ -1062,6 +1265,35 @@ export function PsdTemplatesSettingsClient({
|
|||||||
会优先读取当前品牌下已配置的分类层级作为选项;开启前台模板二级分类后,会按这里的分类分组展示模板。
|
会优先读取当前品牌下已配置的分类层级作为选项;开启前台模板二级分类后,会按这里的分类分组展示模板。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>适用品类标签</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{availableCategoryTagOptions.map((tag) => {
|
||||||
|
const isSelected = hasTemplateCategoryTag(uploadCategoryTags, tag);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={tag}
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setUploadCategoryTags((current) =>
|
||||||
|
toggleTemplateCategoryTag(current, tag)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={`rounded-full border px-3 py-1 text-sm transition ${
|
||||||
|
isSelected
|
||||||
|
? "border-orange-400 bg-orange-500/10 text-orange-600"
|
||||||
|
: "border-border bg-background text-muted-foreground hover:border-orange-300/50 hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
可多选。前端会根据用户当前选择的产品品类筛选模板;勾选“适合所有”后,这个模板会在所有品类下都显示。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="psdTemplateDescription">模板说明</Label>
|
<Label htmlFor="psdTemplateDescription">模板说明</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@ -1413,6 +1645,15 @@ export function PsdTemplatesSettingsClient({
|
|||||||
二级分类:{template.secondaryCategory}
|
二级分类:{template.secondaryCategory}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{template.categoryTags?.length ? (
|
||||||
|
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
{template.categoryTags.map((tag) => (
|
||||||
|
<Badge key={`${template.id}-${tag}`} variant="secondary">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{template.description ? (
|
{template.description ? (
|
||||||
<div className="mt-2 line-clamp-2 text-xs text-muted-foreground">
|
<div className="mt-2 line-clamp-2 text-xs text-muted-foreground">
|
||||||
{template.description}
|
{template.description}
|
||||||
@ -1820,6 +2061,46 @@ export function PsdTemplatesSettingsClient({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-2 md:col-span-2">
|
||||||
|
<Label>适用品类标签</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{draftTemplateCategoryTagOptions.map((tag) => {
|
||||||
|
const isSelected = hasTemplateCategoryTag(
|
||||||
|
draftTemplate.categoryTags,
|
||||||
|
tag
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`draft-category-tag-${tag}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setDraftTemplate((current) =>
|
||||||
|
current
|
||||||
|
? {
|
||||||
|
...current,
|
||||||
|
categoryTags: toggleTemplateCategoryTag(
|
||||||
|
current.categoryTags,
|
||||||
|
tag
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={`rounded-full border px-3 py-1 text-sm transition ${
|
||||||
|
isSelected
|
||||||
|
? "border-orange-400 bg-orange-500/10 text-orange-600"
|
||||||
|
: "border-border bg-background text-muted-foreground hover:border-orange-300/50 hover:text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
可多选。勾选“适合所有”后,模板会在所有品类下都显示;未设置标签的旧模板仍会保持可见,避免影响现有使用。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
@ -1875,6 +2156,11 @@ export function PsdTemplatesSettingsClient({
|
|||||||
{draftTemplate.secondaryCategory ? (
|
{draftTemplate.secondaryCategory ? (
|
||||||
<Badge variant="secondary">分类:{draftTemplate.secondaryCategory}</Badge>
|
<Badge variant="secondary">分类:{draftTemplate.secondaryCategory}</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
|
{(draftTemplate.categoryTags || []).map((tag) => (
|
||||||
|
<Badge key={`draft-template-tag-${tag}`} variant="secondary">
|
||||||
|
品类:{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
<Badge variant="outline">文件:{draftTemplate.fileName}</Badge>
|
<Badge variant="outline">文件:{draftTemplate.fileName}</Badge>
|
||||||
<Badge variant={getTemplateParseBadgeVariant(draftTemplate)}>
|
<Badge variant={getTemplateParseBadgeVariant(draftTemplate)}>
|
||||||
{getTemplateParseLabel(draftTemplate)}
|
{getTemplateParseLabel(draftTemplate)}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
parseAgentPsdTemplateLayers,
|
parseAgentPsdTemplateLayers,
|
||||||
type AgentPsdTemplateMutationResponse,
|
type AgentPsdTemplateMutationResponse,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
|
import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config";
|
||||||
import {
|
import {
|
||||||
preparePsdTemplateRenderAssets,
|
preparePsdTemplateRenderAssets,
|
||||||
} from "@/lib/agents/psd-template-parser";
|
} from "@/lib/agents/psd-template-parser";
|
||||||
@ -31,6 +32,7 @@ const updateTemplateSchema = z.object({
|
|||||||
brandName: z.string().trim().min(1).max(64),
|
brandName: z.string().trim().min(1).max(64),
|
||||||
mode: z.string().optional().nullable(),
|
mode: z.string().optional().nullable(),
|
||||||
secondaryCategory: z.string().trim().max(64).optional().nullable(),
|
secondaryCategory: z.string().trim().max(64).optional().nullable(),
|
||||||
|
categoryTags: z.array(z.string().trim().min(1).max(64)).max(30).optional().nullable(),
|
||||||
description: z.string().optional().nullable(),
|
description: z.string().optional().nullable(),
|
||||||
status: z.string().optional(),
|
status: z.string().optional(),
|
||||||
layers: z.array(z.any()).default([]),
|
layers: z.array(z.any()).default([]),
|
||||||
@ -108,6 +110,7 @@ export async function PUT(
|
|||||||
|
|
||||||
const nextStatus = normalizeAgentPsdTemplateStatus(body.status);
|
const nextStatus = normalizeAgentPsdTemplateStatus(body.status);
|
||||||
const nextMode = normalizeAgentPsdTemplateMode(body.mode);
|
const nextMode = normalizeAgentPsdTemplateMode(body.mode);
|
||||||
|
const nextCategoryTags = normalizeAgentPsdTemplateCategoryTags(body.categoryTags);
|
||||||
let nextLayers = parseAgentPsdTemplateLayers(body.layers);
|
let nextLayers = parseAgentPsdTemplateLayers(body.layers);
|
||||||
if (nextLayers.length === 0) {
|
if (nextLayers.length === 0) {
|
||||||
nextLayers = currentTemplate.layers;
|
nextLayers = currentTemplate.layers;
|
||||||
@ -124,6 +127,7 @@ export async function PUT(
|
|||||||
brandName: body.brandName.trim(),
|
brandName: body.brandName.trim(),
|
||||||
mode: nextMode,
|
mode: nextMode,
|
||||||
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
||||||
|
categoryTags: nextCategoryTags.length > 0 ? nextCategoryTags : undefined,
|
||||||
description: String(body.description || "").trim() || undefined,
|
description: String(body.description || "").trim() || undefined,
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
@ -214,6 +218,7 @@ export async function PUT(
|
|||||||
brandName: body.brandName.trim(),
|
brandName: body.brandName.trim(),
|
||||||
mode: nextMode,
|
mode: nextMode,
|
||||||
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
||||||
|
categoryTags: nextCategoryTags.length > 0 ? nextCategoryTags : undefined,
|
||||||
description: String(body.description || "").trim() || undefined,
|
description: String(body.description || "").trim() || undefined,
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
renderBaseUrl: uploadedRenderBase.url,
|
renderBaseUrl: uploadedRenderBase.url,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
type AgentPsdTemplateListResponse,
|
type AgentPsdTemplateListResponse,
|
||||||
type AgentPsdTemplateMutationResponse,
|
type AgentPsdTemplateMutationResponse,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
|
import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config";
|
||||||
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
|
|
||||||
@ -115,6 +116,9 @@ export async function POST(request: NextRequest) {
|
|||||||
const brandName = String(formData.get("brandName") || "").trim();
|
const brandName = String(formData.get("brandName") || "").trim();
|
||||||
const mode = normalizeAgentPsdTemplateMode(formData.get("mode"));
|
const mode = normalizeAgentPsdTemplateMode(formData.get("mode"));
|
||||||
const secondaryCategory = String(formData.get("secondaryCategory") || "").trim();
|
const secondaryCategory = String(formData.get("secondaryCategory") || "").trim();
|
||||||
|
const categoryTags = normalizeAgentPsdTemplateCategoryTags(
|
||||||
|
formData.getAll("categoryTags").map((item) => String(item || ""))
|
||||||
|
);
|
||||||
const description = String(formData.get("description") || "").trim();
|
const description = String(formData.get("description") || "").trim();
|
||||||
const status = normalizeAgentPsdTemplateStatus(formData.get("status"));
|
const status = normalizeAgentPsdTemplateStatus(formData.get("status"));
|
||||||
|
|
||||||
@ -173,6 +177,7 @@ export async function POST(request: NextRequest) {
|
|||||||
brandName,
|
brandName,
|
||||||
mode,
|
mode,
|
||||||
secondaryCategory: secondaryCategory || undefined,
|
secondaryCategory: secondaryCategory || undefined,
|
||||||
|
categoryTags: categoryTags.length > 0 ? categoryTags : undefined,
|
||||||
description: description || undefined,
|
description: description || undefined,
|
||||||
status,
|
status,
|
||||||
parseStatus: "pending",
|
parseStatus: "pending",
|
||||||
|
|||||||
@ -23,6 +23,11 @@ import {
|
|||||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY,
|
||||||
|
defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
|
normalizeAgentPsdTemplateCategoryTagConfig,
|
||||||
|
} from "@/lib/agents/psd-template-category-tag-config";
|
||||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
import { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||||
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
||||||
@ -53,7 +58,8 @@ export async function GET(request: NextRequest) {
|
|||||||
requestedModeRaw === "main" || requestedModeRaw === "sku"
|
requestedModeRaw === "main" || requestedModeRaw === "sku"
|
||||||
? normalizeAgentPsdTemplateMode(requestedModeRaw)
|
? normalizeAgentPsdTemplateMode(requestedModeRaw)
|
||||||
: null;
|
: null;
|
||||||
const [allTemplates, textColorSetting, secondaryCategorySetting] = await Promise.all([
|
const [allTemplates, textColorSetting, secondaryCategorySetting, categoryTagSetting] =
|
||||||
|
await Promise.all([
|
||||||
readAgentPsdTemplates(),
|
readAgentPsdTemplates(),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
@ -69,6 +75,13 @@ export async function GET(request: NextRequest) {
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(systemSettings)
|
||||||
|
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY))
|
||||||
|
.limit(1)
|
||||||
|
.then((rows) => rows[0])
|
||||||
|
.catch(() => null),
|
||||||
]);
|
]);
|
||||||
const repairedTemplateMap = new Map<
|
const repairedTemplateMap = new Map<
|
||||||
string,
|
string,
|
||||||
@ -147,18 +160,25 @@ export async function GET(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const normalizedTextColorConfig = textColorSetting
|
||||||
|
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||||
|
: defaultAgentPsdTextColorConfig;
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
templates,
|
templates,
|
||||||
textColorConfig: resolveAgentPsdTextColorConfigForBrand(
|
textColorConfig: requestedBrand
|
||||||
textColorSetting
|
? resolveAgentPsdTextColorConfigForBrand(
|
||||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
normalizedTextColorConfig,
|
||||||
: defaultAgentPsdTextColorConfig,
|
requestedBrand
|
||||||
requestedBrand
|
)
|
||||||
),
|
: normalizedTextColorConfig,
|
||||||
secondaryCategoryConfig: secondaryCategorySetting
|
secondaryCategoryConfig: secondaryCategorySetting
|
||||||
? normalizeAgentPsdTemplateSecondaryCategoryConfig(secondaryCategorySetting.value)
|
? normalizeAgentPsdTemplateSecondaryCategoryConfig(secondaryCategorySetting.value)
|
||||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
categoryTagConfig: categoryTagSetting
|
||||||
|
? normalizeAgentPsdTemplateCategoryTagConfig(categoryTagSetting.value)
|
||||||
|
: defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
} satisfies AgentPsdTemplateListResponse);
|
} satisfies AgentPsdTemplateListResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[PSD Templates] list failed:", error);
|
console.error("[PSD Templates] list failed:", error);
|
||||||
|
|||||||
@ -67,6 +67,11 @@ import {
|
|||||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY,
|
||||||
|
defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
|
normalizeAgentPsdTemplateCategoryTagConfig,
|
||||||
|
} from "@/lib/agents/psd-template-category-tag-config";
|
||||||
import {
|
import {
|
||||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||||
defaultAgentPresetLibraryConfig,
|
defaultAgentPresetLibraryConfig,
|
||||||
@ -278,6 +283,7 @@ export async function GET() {
|
|||||||
agentWorkflowShowcaseConfigSetting,
|
agentWorkflowShowcaseConfigSetting,
|
||||||
agentPsdTextColorConfigSetting,
|
agentPsdTextColorConfigSetting,
|
||||||
agentPsdTemplateSecondaryCategoryConfigSetting,
|
agentPsdTemplateSecondaryCategoryConfigSetting,
|
||||||
|
agentPsdTemplateCategoryTagConfigSetting,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
||||||
db
|
db
|
||||||
@ -442,6 +448,12 @@ export async function GET() {
|
|||||||
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY))
|
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then(rows => rows[0]),
|
.then(rows => rows[0]),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(systemSettings)
|
||||||
|
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY))
|
||||||
|
.limit(1)
|
||||||
|
.then(rows => rows[0]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
||||||
@ -515,6 +527,10 @@ export async function GET() {
|
|||||||
normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||||
agentPsdTemplateSecondaryCategoryConfigSetting?.value
|
agentPsdTemplateSecondaryCategoryConfigSetting?.value
|
||||||
);
|
);
|
||||||
|
const agentPsdTemplateCategoryTagConfig =
|
||||||
|
normalizeAgentPsdTemplateCategoryTagConfig(
|
||||||
|
agentPsdTemplateCategoryTagConfigSetting?.value
|
||||||
|
);
|
||||||
|
|
||||||
// 如果没有设置记录,返回默认值
|
// 如果没有设置记录,返回默认值
|
||||||
const baseSettings = settings || {
|
const baseSettings = settings || {
|
||||||
@ -553,6 +569,8 @@ export async function GET() {
|
|||||||
agentPsdTextColorConfig: defaultAgentPsdTextColorConfig,
|
agentPsdTextColorConfig: defaultAgentPsdTextColorConfig,
|
||||||
agentPsdTemplateSecondaryCategoryConfig:
|
agentPsdTemplateSecondaryCategoryConfig:
|
||||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
agentPsdTemplateCategoryTagConfig:
|
||||||
|
defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
realtimeCanvasEnabled: true,
|
realtimeCanvasEnabled: true,
|
||||||
realtimeCanvasServiceEnabled: true,
|
realtimeCanvasServiceEnabled: true,
|
||||||
storageFailoverConfig,
|
storageFailoverConfig,
|
||||||
@ -605,6 +623,7 @@ export async function GET() {
|
|||||||
agentWorkflowShowcaseConfig,
|
agentWorkflowShowcaseConfig,
|
||||||
agentPsdTextColorConfig,
|
agentPsdTextColorConfig,
|
||||||
agentPsdTemplateSecondaryCategoryConfig,
|
agentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
agentPsdTemplateCategoryTagConfig,
|
||||||
membershipConfig,
|
membershipConfig,
|
||||||
membershipUserOverrides,
|
membershipUserOverrides,
|
||||||
pointsSystemConfig,
|
pointsSystemConfig,
|
||||||
@ -926,6 +945,11 @@ const updateSchema = z.object({
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
agentPsdTemplateCategoryTagConfig: z
|
||||||
|
.object({
|
||||||
|
tags: z.array(z.string().trim().min(1).max(64)).max(200).optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
realtimeCanvasEnabled: z.boolean().optional(),
|
realtimeCanvasEnabled: z.boolean().optional(),
|
||||||
aiDetailGenerationEnabled: z.boolean().optional(),
|
aiDetailGenerationEnabled: z.boolean().optional(),
|
||||||
canvasDefaultModelId: z.string().nullable().optional(),
|
canvasDefaultModelId: z.string().nullable().optional(),
|
||||||
@ -1015,6 +1039,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
"agentWorkflowShowcaseConfig",
|
"agentWorkflowShowcaseConfig",
|
||||||
"agentPsdTextColorConfig",
|
"agentPsdTextColorConfig",
|
||||||
"agentPsdTemplateSecondaryCategoryConfig",
|
"agentPsdTemplateSecondaryCategoryConfig",
|
||||||
|
"agentPsdTemplateCategoryTagConfig",
|
||||||
]);
|
]);
|
||||||
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
|
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
|
||||||
if (hasRestrictedKeys) {
|
if (hasRestrictedKeys) {
|
||||||
@ -1040,6 +1065,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
agentWorkflowShowcaseConfig,
|
agentWorkflowShowcaseConfig,
|
||||||
agentPsdTextColorConfig,
|
agentPsdTextColorConfig,
|
||||||
agentPsdTemplateSecondaryCategoryConfig,
|
agentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
agentPsdTemplateCategoryTagConfig,
|
||||||
membershipConfig,
|
membershipConfig,
|
||||||
membershipUserOverrides,
|
membershipUserOverrides,
|
||||||
pointsSystemConfig,
|
pointsSystemConfig,
|
||||||
@ -1474,6 +1500,50 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (agentPsdTemplateSecondaryCategoryConfig !== undefined) {
|
||||||
|
const normalized = normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||||
|
agentPsdTemplateSecondaryCategoryConfig
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.insert(systemSettings)
|
||||||
|
.values({
|
||||||
|
key: AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||||
|
value: normalized,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: systemSettings.key,
|
||||||
|
set: {
|
||||||
|
value: normalized,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (agentPsdTemplateCategoryTagConfig !== undefined) {
|
||||||
|
const normalized = normalizeAgentPsdTemplateCategoryTagConfig(
|
||||||
|
agentPsdTemplateCategoryTagConfig
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.insert(systemSettings)
|
||||||
|
.values({
|
||||||
|
key: AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY,
|
||||||
|
value: normalized,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: systemSettings.key,
|
||||||
|
set: {
|
||||||
|
value: normalized,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (membershipConfig !== undefined) {
|
if (membershipConfig !== undefined) {
|
||||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||||
await db
|
await db
|
||||||
@ -1682,6 +1752,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
agentPsdTemplateSecondaryCategoryConfig
|
agentPsdTemplateSecondaryCategoryConfig
|
||||||
)
|
)
|
||||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
agentPsdTemplateCategoryTagConfig:
|
||||||
|
agentPsdTemplateCategoryTagConfig !== undefined
|
||||||
|
? normalizeAgentPsdTemplateCategoryTagConfig(agentPsdTemplateCategoryTagConfig)
|
||||||
|
: defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
membershipConfig:
|
membershipConfig:
|
||||||
membershipConfig !== undefined
|
membershipConfig !== undefined
|
||||||
? normalizeMembershipConfig(membershipConfig)
|
? normalizeMembershipConfig(membershipConfig)
|
||||||
@ -2082,6 +2156,28 @@ export async function PUT(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (agentPsdTemplateCategoryTagConfig !== undefined) {
|
||||||
|
const normalized = normalizeAgentPsdTemplateCategoryTagConfig(
|
||||||
|
agentPsdTemplateCategoryTagConfig
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.insert(systemSettings)
|
||||||
|
.values({
|
||||||
|
key: AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY,
|
||||||
|
value: normalized,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: systemSettings.key,
|
||||||
|
set: {
|
||||||
|
value: normalized,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (membershipConfig !== undefined) {
|
if (membershipConfig !== undefined) {
|
||||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||||
await db
|
await db
|
||||||
@ -2282,6 +2378,16 @@ export async function PUT(request: NextRequest) {
|
|||||||
agentPsdTextColorConfig !== undefined
|
agentPsdTextColorConfig !== undefined
|
||||||
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
||||||
: defaultAgentPsdTextColorConfig,
|
: defaultAgentPsdTextColorConfig,
|
||||||
|
agentPsdTemplateSecondaryCategoryConfig:
|
||||||
|
agentPsdTemplateSecondaryCategoryConfig !== undefined
|
||||||
|
? normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||||
|
agentPsdTemplateSecondaryCategoryConfig
|
||||||
|
)
|
||||||
|
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
agentPsdTemplateCategoryTagConfig:
|
||||||
|
agentPsdTemplateCategoryTagConfig !== undefined
|
||||||
|
? normalizeAgentPsdTemplateCategoryTagConfig(agentPsdTemplateCategoryTagConfig)
|
||||||
|
: defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
membershipConfig:
|
membershipConfig:
|
||||||
membershipConfig !== undefined
|
membershipConfig !== undefined
|
||||||
? normalizeMembershipConfig(membershipConfig)
|
? normalizeMembershipConfig(membershipConfig)
|
||||||
|
|||||||
@ -5989,7 +5989,14 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ClientErrorBoundary
|
<ClientErrorBoundary
|
||||||
resetKeys={[psdTemplateDialogSourceUrl || "", workflowContext?.brand || "", psdTemplateDialogMode]}
|
resetKeys={[
|
||||||
|
psdTemplateDialogSourceUrl || "",
|
||||||
|
workflowContext?.brand || "",
|
||||||
|
workflowContext?.thirdCategory || "",
|
||||||
|
workflowContext?.fourthCategory || "",
|
||||||
|
workflowContext?.fifthCategory || "",
|
||||||
|
psdTemplateDialogMode,
|
||||||
|
]}
|
||||||
fallback={
|
fallback={
|
||||||
<Dialog
|
<Dialog
|
||||||
open={Boolean(psdTemplateDialogSourceUrl)}
|
open={Boolean(psdTemplateDialogSourceUrl)}
|
||||||
@ -6028,6 +6035,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
}}
|
}}
|
||||||
sourceImageUrl={psdTemplateDialogSourceUrl}
|
sourceImageUrl={psdTemplateDialogSourceUrl}
|
||||||
brandName={workflowContext?.brand || null}
|
brandName={workflowContext?.brand || null}
|
||||||
|
selectedCategoryNames={[
|
||||||
|
workflowContext?.fifthCategory || "",
|
||||||
|
workflowContext?.fourthCategory || "",
|
||||||
|
workflowContext?.thirdCategory || "",
|
||||||
|
].filter(Boolean)}
|
||||||
sizeInfoText={workflowContext?.summaryText || null}
|
sizeInfoText={workflowContext?.summaryText || null}
|
||||||
templateMode={psdTemplateDialogMode}
|
templateMode={psdTemplateDialogMode}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -62,9 +62,16 @@ import {
|
|||||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG,
|
||||||
|
defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
|
normalizeAgentPsdTemplateCategoryTagConfig,
|
||||||
|
normalizeAgentPsdTemplateCategoryTags,
|
||||||
|
} from "@/lib/agents/psd-template-category-tag-config";
|
||||||
import {
|
import {
|
||||||
defaultAgentPsdTextColorConfig,
|
defaultAgentPsdTextColorConfig,
|
||||||
normalizeAgentPsdTextColorConfig,
|
normalizeAgentPsdTextColorConfig,
|
||||||
|
resolveAgentPsdTextColorConfigForBrand,
|
||||||
} from "@/lib/agents/psd-text-color-config";
|
} from "@/lib/agents/psd-text-color-config";
|
||||||
import {
|
import {
|
||||||
normalizeHexColor,
|
normalizeHexColor,
|
||||||
@ -90,6 +97,7 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
|||||||
templates: [],
|
templates: [],
|
||||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||||
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
categoryTagConfig: defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
error:
|
error:
|
||||||
typeof payload?.error === "string"
|
typeof payload?.error === "string"
|
||||||
? payload.error
|
? payload.error
|
||||||
@ -106,6 +114,9 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
|||||||
secondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
secondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||||
payload.secondaryCategoryConfig
|
payload.secondaryCategoryConfig
|
||||||
),
|
),
|
||||||
|
categoryTagConfig: normalizeAgentPsdTemplateCategoryTagConfig(
|
||||||
|
payload.categoryTagConfig
|
||||||
|
),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
@ -113,6 +124,7 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
|||||||
templates: [],
|
templates: [],
|
||||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||||
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
categoryTagConfig: defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
error: "PSD 模板加载失败",
|
error: "PSD 模板加载失败",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -123,12 +135,86 @@ const PREVIEW_STAGE_TARGET_WIDTH = 760;
|
|||||||
const PREVIEW_STAGE_TARGET_HEIGHT = 760;
|
const PREVIEW_STAGE_TARGET_HEIGHT = 760;
|
||||||
const MIN_IMAGE_SCALE = 0.4;
|
const MIN_IMAGE_SCALE = 0.4;
|
||||||
const MAX_IMAGE_SCALE = 3;
|
const MAX_IMAGE_SCALE = 3;
|
||||||
|
const UNASSIGNED_BRAND_LABEL = "未分类";
|
||||||
|
|
||||||
|
function normalizeManagedBrandName(value?: string | null) {
|
||||||
|
const normalized = String(value || "").trim();
|
||||||
|
if (!normalized || normalized === "未分配品牌") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveTemplateCategoryTags(
|
||||||
|
template: AgentPsdTemplate,
|
||||||
|
activeCategoryTags: string[]
|
||||||
|
) {
|
||||||
|
const templateCategoryTags = normalizeAgentPsdTemplateCategoryTags(
|
||||||
|
template.categoryTags
|
||||||
|
);
|
||||||
|
if (templateCategoryTags.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeCategoryTagSet = new Set(
|
||||||
|
activeCategoryTags.map((tag) => tag.trim().toLowerCase()).filter(Boolean)
|
||||||
|
);
|
||||||
|
if (activeCategoryTagSet.size === 0) {
|
||||||
|
return templateCategoryTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
return templateCategoryTags.filter(
|
||||||
|
(tag) =>
|
||||||
|
tag === AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG ||
|
||||||
|
activeCategoryTagSet.has(tag.trim().toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesTemplateSelectedCategories(
|
||||||
|
template: AgentPsdTemplate,
|
||||||
|
selectedCategoryNames: string[],
|
||||||
|
activeCategoryTags: string[]
|
||||||
|
) {
|
||||||
|
if (selectedCategoryNames.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawTemplateCategoryTags = normalizeAgentPsdTemplateCategoryTags(
|
||||||
|
template.categoryTags
|
||||||
|
);
|
||||||
|
if (rawTemplateCategoryTags.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateCategoryTags = getEffectiveTemplateCategoryTags(
|
||||||
|
template,
|
||||||
|
activeCategoryTags
|
||||||
|
);
|
||||||
|
if (templateCategoryTags.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateCategoryTags.includes(AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedCategorySet = new Set(
|
||||||
|
selectedCategoryNames.map((tag) => tag.trim().toLowerCase()).filter(Boolean)
|
||||||
|
);
|
||||||
|
return templateCategoryTags.some((tag) =>
|
||||||
|
selectedCategorySet.has(tag.trim().toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getTemplateMetaLabel(
|
function getTemplateMetaLabel(
|
||||||
template: AgentPsdTemplate,
|
template: AgentPsdTemplate,
|
||||||
showSecondaryCategory: boolean
|
showSecondaryCategory: boolean,
|
||||||
|
showBrand = false
|
||||||
) {
|
) {
|
||||||
const metaParts = [
|
const metaParts = [
|
||||||
|
showBrand
|
||||||
|
? normalizeManagedBrandName(template.brandName) || UNASSIGNED_BRAND_LABEL
|
||||||
|
: "",
|
||||||
`${template.width} x ${template.height}`,
|
`${template.width} x ${template.height}`,
|
||||||
showSecondaryCategory
|
showSecondaryCategory
|
||||||
? String(template.secondaryCategory || "").trim()
|
? String(template.secondaryCategory || "").trim()
|
||||||
@ -141,12 +227,14 @@ function renderTemplateSelectContent(
|
|||||||
template: AgentPsdTemplate,
|
template: AgentPsdTemplate,
|
||||||
params?: {
|
params?: {
|
||||||
showSecondaryCategory?: boolean;
|
showSecondaryCategory?: boolean;
|
||||||
|
showBrand?: boolean;
|
||||||
compact?: boolean;
|
compact?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
||||||
|
const showBrand = params?.showBrand === true;
|
||||||
const compact = params?.compact === true;
|
const compact = params?.compact === true;
|
||||||
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory);
|
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory, showBrand);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -199,12 +287,14 @@ function renderTemplatePickerCardContent(
|
|||||||
template: AgentPsdTemplate,
|
template: AgentPsdTemplate,
|
||||||
params?: {
|
params?: {
|
||||||
showSecondaryCategory?: boolean;
|
showSecondaryCategory?: boolean;
|
||||||
|
showBrand?: boolean;
|
||||||
emphasize?: boolean;
|
emphasize?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
||||||
|
const showBrand = params?.showBrand === true;
|
||||||
const emphasize = params?.emphasize === true;
|
const emphasize = params?.emphasize === true;
|
||||||
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory);
|
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory, showBrand);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 items-center gap-3">
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
@ -564,6 +654,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
sourceImageUrl,
|
sourceImageUrl,
|
||||||
brandName,
|
brandName,
|
||||||
|
selectedCategoryNames,
|
||||||
sizeInfoText,
|
sizeInfoText,
|
||||||
templateMode,
|
templateMode,
|
||||||
}: {
|
}: {
|
||||||
@ -571,30 +662,43 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
sourceImageUrl: string | null;
|
sourceImageUrl: string | null;
|
||||||
brandName?: string | null;
|
brandName?: string | null;
|
||||||
|
selectedCategoryNames?: string[] | null;
|
||||||
sizeInfoText?: string | null;
|
sizeInfoText?: string | null;
|
||||||
templateMode?: AgentPsdTemplateMode | null;
|
templateMode?: AgentPsdTemplateMode | null;
|
||||||
}) {
|
}) {
|
||||||
const normalizedBrandName = String(brandName || "").trim();
|
const normalizedBrandName = String(brandName || "").trim();
|
||||||
|
const normalizedSelectedCategoryNames = useMemo(
|
||||||
|
() =>
|
||||||
|
normalizeAgentPsdTemplateCategoryTags(selectedCategoryNames).filter(
|
||||||
|
(tag) => tag !== AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG
|
||||||
|
),
|
||||||
|
[selectedCategoryNames]
|
||||||
|
);
|
||||||
const normalizedTemplateMode =
|
const normalizedTemplateMode =
|
||||||
templateMode === "sku" ? "sku" : templateMode === "main" ? "main" : "";
|
templateMode === "sku" ? "sku" : templateMode === "main" ? "main" : "";
|
||||||
const templateListRequestUrl = useMemo(() => {
|
const templateListRequestUrl = useMemo(() => {
|
||||||
if (!open || !normalizedBrandName) {
|
if (!open) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
searchParams.set("brand", normalizedBrandName);
|
if (normalizedBrandName) {
|
||||||
return `/api/psd-templates?${searchParams.toString()}`;
|
searchParams.set("brand", normalizedBrandName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchParams.size > 0
|
||||||
|
? `/api/psd-templates?${searchParams.toString()}`
|
||||||
|
: "/api/psd-templates";
|
||||||
}, [open, normalizedBrandName]);
|
}, [open, normalizedBrandName]);
|
||||||
const { data, isLoading } = useSWR<AgentPsdTemplateListResponse>(
|
const { data, isLoading } = useSWR<AgentPsdTemplateListResponse>(
|
||||||
templateListRequestUrl,
|
templateListRequestUrl,
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
const templates = useMemo(
|
const allTemplates = useMemo(
|
||||||
() => (data?.success ? data.templates : []),
|
() => (data?.success ? data.templates : []),
|
||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
const textColorConfig = useMemo(
|
const rawTextColorConfig = useMemo(
|
||||||
() => normalizeAgentPsdTextColorConfig(data?.textColorConfig),
|
() => normalizeAgentPsdTextColorConfig(data?.textColorConfig),
|
||||||
[data?.textColorConfig]
|
[data?.textColorConfig]
|
||||||
);
|
);
|
||||||
@ -605,6 +709,25 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
),
|
),
|
||||||
[data?.secondaryCategoryConfig]
|
[data?.secondaryCategoryConfig]
|
||||||
);
|
);
|
||||||
|
const categoryTagConfig = useMemo(
|
||||||
|
() => normalizeAgentPsdTemplateCategoryTagConfig(data?.categoryTagConfig),
|
||||||
|
[data?.categoryTagConfig]
|
||||||
|
);
|
||||||
|
const activeCategoryTags = useMemo(
|
||||||
|
() => categoryTagConfig.tags,
|
||||||
|
[categoryTagConfig.tags]
|
||||||
|
);
|
||||||
|
const templates = useMemo(
|
||||||
|
() =>
|
||||||
|
allTemplates.filter((template) =>
|
||||||
|
matchesTemplateSelectedCategories(
|
||||||
|
template,
|
||||||
|
normalizedSelectedCategoryNames,
|
||||||
|
activeCategoryTags
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[activeCategoryTags, allTemplates, normalizedSelectedCategoryNames]
|
||||||
|
);
|
||||||
|
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
||||||
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
||||||
@ -647,6 +770,28 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
templates.find((template) => template.id === selectedTemplateId) || null,
|
templates.find((template) => template.id === selectedTemplateId) || null,
|
||||||
[selectedTemplateId, templates]
|
[selectedTemplateId, templates]
|
||||||
);
|
);
|
||||||
|
const effectiveTemplateBrandName = useMemo(
|
||||||
|
() => normalizedBrandName || String(selectedTemplate?.brandName || "").trim(),
|
||||||
|
[normalizedBrandName, selectedTemplate?.brandName]
|
||||||
|
);
|
||||||
|
const textColorConfig = useMemo(
|
||||||
|
() =>
|
||||||
|
resolveAgentPsdTextColorConfigForBrand(
|
||||||
|
rawTextColorConfig,
|
||||||
|
effectiveTemplateBrandName
|
||||||
|
),
|
||||||
|
[effectiveTemplateBrandName, rawTextColorConfig]
|
||||||
|
);
|
||||||
|
const shouldShowTemplateBrand = useMemo(
|
||||||
|
() =>
|
||||||
|
!normalizedBrandName &&
|
||||||
|
templates.some((template) => Boolean(normalizeManagedBrandName(template.brandName))),
|
||||||
|
[normalizedBrandName, templates]
|
||||||
|
);
|
||||||
|
const selectedCategorySummaryText = useMemo(
|
||||||
|
() => normalizedSelectedCategoryNames.join(" / "),
|
||||||
|
[normalizedSelectedCategoryNames]
|
||||||
|
);
|
||||||
const shouldGroupTemplatesBySecondaryCategory = useMemo(
|
const shouldGroupTemplatesBySecondaryCategory = useMemo(
|
||||||
() =>
|
() =>
|
||||||
secondaryCategoryConfig.enabled &&
|
secondaryCategoryConfig.enabled &&
|
||||||
@ -1428,9 +1573,13 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
nextMode === "sku" ? skuModeTemplates : mainModeTemplates;
|
nextMode === "sku" ? skuModeTemplates : mainModeTemplates;
|
||||||
if (nextTemplates.length === 0) {
|
if (nextTemplates.length === 0) {
|
||||||
toast.error(
|
toast.error(
|
||||||
nextMode === "sku"
|
selectedCategorySummaryText
|
||||||
? "当前品牌下暂无 SKU 图模式模板"
|
? nextMode === "sku"
|
||||||
: "当前品牌下暂无主图模式模板"
|
? `当前品类“${selectedCategorySummaryText}”下暂无 SKU 图模式模板`
|
||||||
|
: `当前品类“${selectedCategorySummaryText}”下暂无主图模式模板`
|
||||||
|
: nextMode === "sku"
|
||||||
|
? "当前品牌下暂无 SKU 图模式模板"
|
||||||
|
: "当前品牌下暂无主图模式模板"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1475,6 +1624,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
<Label>
|
<Label>
|
||||||
{selectedTemplate ? "选择模板" : "主图模式模板"}
|
{selectedTemplate ? "选择模板" : "主图模式模板"}
|
||||||
</Label>
|
</Label>
|
||||||
|
{selectedCategorySummaryText ? (
|
||||||
|
<div className="text-white/45 text-xs">
|
||||||
|
当前按品类“{selectedCategorySummaryText}”筛选模板,同时保留“适合所有”的模板。
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{selectedTemplate && hasTemplateOptions ? (
|
{selectedTemplate && hasTemplateOptions ? (
|
||||||
<div className="space-y-2.5">
|
<div className="space-y-2.5">
|
||||||
<div className="inline-flex rounded-full border border-white/10 bg-white/[0.03] p-1">
|
<div className="inline-flex rounded-full border border-white/10 bg-white/[0.03] p-1">
|
||||||
@ -1516,6 +1670,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
{renderTemplateSelectContent(selectedTemplate, {
|
{renderTemplateSelectContent(selectedTemplate, {
|
||||||
showSecondaryCategory:
|
showSecondaryCategory:
|
||||||
shouldGroupTemplatesBySecondaryCategory,
|
shouldGroupTemplatesBySecondaryCategory,
|
||||||
|
showBrand: shouldShowTemplateBrand,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@ -1550,6 +1705,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
{renderTemplateSelectContent(template, {
|
{renderTemplateSelectContent(template, {
|
||||||
showSecondaryCategory:
|
showSecondaryCategory:
|
||||||
shouldGroupTemplatesBySecondaryCategory,
|
shouldGroupTemplatesBySecondaryCategory,
|
||||||
|
showBrand: shouldShowTemplateBrand,
|
||||||
compact: true,
|
compact: true,
|
||||||
})}
|
})}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -1563,6 +1719,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
value={template.id}
|
value={template.id}
|
||||||
>
|
>
|
||||||
{renderTemplateSelectContent(template, {
|
{renderTemplateSelectContent(template, {
|
||||||
|
showBrand: shouldShowTemplateBrand,
|
||||||
compact: true,
|
compact: true,
|
||||||
})}
|
})}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -1598,15 +1755,18 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
{renderTemplatePickerCardContent(template, {
|
{renderTemplatePickerCardContent(template, {
|
||||||
showSecondaryCategory:
|
showSecondaryCategory:
|
||||||
shouldGroupTemplatesBySecondaryCategory,
|
shouldGroupTemplatesBySecondaryCategory,
|
||||||
|
showBrand: shouldShowTemplateBrand,
|
||||||
emphasize: true,
|
emphasize: true,
|
||||||
})}
|
})}
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-xl border border-white/15 border-dashed bg-white/[0.02] px-3 py-3 text-muted-foreground text-sm">
|
<div className="rounded-xl border border-white/15 border-dashed bg-white/[0.02] px-3 py-3 text-muted-foreground text-sm">
|
||||||
{normalizedBrandName
|
{selectedCategorySummaryText
|
||||||
? `当前品牌“${normalizedBrandName}”下暂无主图模式模板`
|
? `当前品类“${selectedCategorySummaryText}”下暂无主图模式模板`
|
||||||
: "请先在工作流中选择品牌后再打开 PSD 模板。"}
|
: normalizedBrandName
|
||||||
|
? `当前品牌“${normalizedBrandName}”下暂无主图模式模板`
|
||||||
|
: "当前暂无可用的主图模式模板,请先去后台配置。"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -1650,6 +1810,10 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
? "正在加载可用模板..."
|
? "正在加载可用模板..."
|
||||||
: hasTemplateOptions
|
: hasTemplateOptions
|
||||||
? "请从左侧主图模式模板或右侧 SKU 图模式模板中选择一个模板开始编辑。"
|
? "请从左侧主图模式模板或右侧 SKU 图模式模板中选择一个模板开始编辑。"
|
||||||
|
: selectedCategorySummaryText && normalizedBrandName
|
||||||
|
? `当前品牌“${normalizedBrandName}”下没有命中品类“${selectedCategorySummaryText}”的 PSD 模板,请先去后台配置。`
|
||||||
|
: selectedCategorySummaryText
|
||||||
|
? `当前品类“${selectedCategorySummaryText}”下还没有可用 PSD 模板,请先去后台配置。`
|
||||||
: normalizedBrandName
|
: normalizedBrandName
|
||||||
? `当前品牌“${normalizedBrandName}”下还没有可用 PSD 模板,请先去后台配置。`
|
? `当前品牌“${normalizedBrandName}”下还没有可用 PSD 模板,请先去后台配置。`
|
||||||
: "当前还没有启用的 PSD 模板,请先去后台配置。"}
|
: "当前还没有启用的 PSD 模板,请先去后台配置。"}
|
||||||
@ -2071,7 +2235,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
<div className="mt-1 text-white/45 text-xs">
|
<div className="mt-1 text-white/45 text-xs">
|
||||||
{normalizedTemplateMode === "sku"
|
{normalizedTemplateMode === "sku"
|
||||||
? "当前结果属于 SKU 图模式,可直接从这里选择并进入编辑。"
|
? "当前结果属于 SKU 图模式,可直接从这里选择并进入编辑。"
|
||||||
: "这里集中展示当前品牌下的 SKU 图模式模板。"}
|
: selectedCategorySummaryText
|
||||||
|
? `当前按品类“${selectedCategorySummaryText}”筛选,同时展示“适合所有”的 SKU 图模式模板。`
|
||||||
|
: normalizedBrandName
|
||||||
|
? "这里集中展示当前品牌下的 SKU 图模式模板。"
|
||||||
|
: "当前未选择品牌,这里展示全部品牌下的 SKU 图模式模板。"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-white/65 text-xs">
|
<div className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-white/65 text-xs">
|
||||||
@ -2106,6 +2274,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
{renderTemplatePickerCardContent(template, {
|
{renderTemplatePickerCardContent(template, {
|
||||||
showSecondaryCategory:
|
showSecondaryCategory:
|
||||||
shouldGroupTemplatesBySecondaryCategory,
|
shouldGroupTemplatesBySecondaryCategory,
|
||||||
|
showBrand: shouldShowTemplateBrand,
|
||||||
emphasize: true,
|
emphasize: true,
|
||||||
})}
|
})}
|
||||||
</button>
|
</button>
|
||||||
@ -2113,9 +2282,11 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center px-6 text-center text-muted-foreground text-sm">
|
<div className="flex h-full w-full items-center justify-center px-6 text-center text-muted-foreground text-sm">
|
||||||
{normalizedBrandName
|
{selectedCategorySummaryText
|
||||||
? `当前品牌“${normalizedBrandName}”下暂无 SKU 图模式模板`
|
? `当前品类“${selectedCategorySummaryText}”下暂无 SKU 图模式模板`
|
||||||
: "请先返回工作流区域选择品牌,再打开 PSD 模板。"}
|
: normalizedBrandName
|
||||||
|
? `当前品牌“${normalizedBrandName}”下暂无 SKU 图模式模板`
|
||||||
|
: "当前暂无可用的 SKU 图模式模板,请先去后台配置。"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
89
lib/agents/psd-template-category-tag-config.ts
Normal file
89
lib/agents/psd-template-category-tag-config.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
export interface AgentPsdTemplateCategoryTagConfig {
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY =
|
||||||
|
"agent_psd_template_category_tag_config";
|
||||||
|
export const AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG = "适合所有";
|
||||||
|
|
||||||
|
const MAX_CATEGORY_TAG_LENGTH = 64;
|
||||||
|
const MAX_CATEGORY_TAG_CONFIG_ITEMS = 200;
|
||||||
|
const MAX_TEMPLATE_CATEGORY_TAG_ITEMS = 30;
|
||||||
|
|
||||||
|
function normalizeCategoryTagText(value: unknown) {
|
||||||
|
return String(value || "").trim().slice(0, MAX_CATEGORY_TAG_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAgentPsdTemplateCategoryTags(value: unknown): string[] {
|
||||||
|
const source = Array.isArray(value)
|
||||||
|
? value
|
||||||
|
: typeof value === "string"
|
||||||
|
? value
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tags: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
for (const item of source) {
|
||||||
|
const normalized = normalizeCategoryTagText(item);
|
||||||
|
if (!normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalValue =
|
||||||
|
normalized === AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG
|
||||||
|
? AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG
|
||||||
|
: normalized;
|
||||||
|
const dedupeKey = finalValue.toLowerCase();
|
||||||
|
if (seen.has(dedupeKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(dedupeKey);
|
||||||
|
tags.push(finalValue);
|
||||||
|
|
||||||
|
if (tags.length >= MAX_TEMPLATE_CATEGORY_TAG_ITEMS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultAgentPsdTemplateCategoryTagConfig: AgentPsdTemplateCategoryTagConfig = {
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeAgentPsdTemplateCategoryTagConfig(
|
||||||
|
value: unknown
|
||||||
|
): AgentPsdTemplateCategoryTagConfig {
|
||||||
|
const raw =
|
||||||
|
value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tags: normalizeAgentPsdTemplateCategoryTags(raw.tags)
|
||||||
|
.filter((tag) => tag !== AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG)
|
||||||
|
.slice(0, MAX_CATEGORY_TAG_CONFIG_ITEMS),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgentPsdTemplateCategoryTagOptions(
|
||||||
|
config?: AgentPsdTemplateCategoryTagConfig | null,
|
||||||
|
extraTags?: unknown
|
||||||
|
) {
|
||||||
|
return [
|
||||||
|
AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG,
|
||||||
|
...normalizeAgentPsdTemplateCategoryTagConfig(config).tags,
|
||||||
|
...normalizeAgentPsdTemplateCategoryTags(extraTags).filter(
|
||||||
|
(tag) => tag !== AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG
|
||||||
|
),
|
||||||
|
].filter((tag, index, array) => {
|
||||||
|
const normalized = tag.toLowerCase();
|
||||||
|
return array.findIndex((candidate) => candidate.toLowerCase() === normalized) === index;
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import type { AgentPsdTextColorConfig } from "@/types/agent";
|
import type { AgentPsdTextColorConfig } from "@/types/agent";
|
||||||
import type { AgentPsdTemplateSecondaryCategoryConfig } from "@/lib/agents/psd-template-secondary-category-config";
|
import type { AgentPsdTemplateSecondaryCategoryConfig } from "@/lib/agents/psd-template-secondary-category-config";
|
||||||
|
import type { AgentPsdTemplateCategoryTagConfig } from "@/lib/agents/psd-template-category-tag-config";
|
||||||
|
import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config";
|
||||||
|
|
||||||
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
|
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
|
||||||
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 6;
|
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 6;
|
||||||
@ -86,6 +88,7 @@ export interface AgentPsdTemplate {
|
|||||||
brandName: string;
|
brandName: string;
|
||||||
mode: AgentPsdTemplateMode;
|
mode: AgentPsdTemplateMode;
|
||||||
secondaryCategory?: string;
|
secondaryCategory?: string;
|
||||||
|
categoryTags?: string[];
|
||||||
description?: string;
|
description?: string;
|
||||||
status: AgentPsdTemplateStatus;
|
status: AgentPsdTemplateStatus;
|
||||||
parseStatus: AgentPsdTemplateParseStatus;
|
parseStatus: AgentPsdTemplateParseStatus;
|
||||||
@ -114,6 +117,7 @@ export interface AgentPsdTemplateListResponse {
|
|||||||
templates: AgentPsdTemplate[];
|
templates: AgentPsdTemplate[];
|
||||||
textColorConfig?: AgentPsdTextColorConfig;
|
textColorConfig?: AgentPsdTextColorConfig;
|
||||||
secondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
|
secondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
|
||||||
|
categoryTagConfig?: AgentPsdTemplateCategoryTagConfig;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,12 +448,17 @@ export function parseAgentPsdTemplates(value: unknown) {
|
|||||||
const parseStatus = normalizeAgentPsdTemplateParseStatus(record.parseStatus);
|
const parseStatus = normalizeAgentPsdTemplateParseStatus(record.parseStatus);
|
||||||
const rawParseProgress = normalizePositiveInteger(record.parseProgress);
|
const rawParseProgress = normalizePositiveInteger(record.parseProgress);
|
||||||
|
|
||||||
|
const categoryTags = normalizeAgentPsdTemplateCategoryTags(
|
||||||
|
record.categoryTags ?? record.productCategoryTags
|
||||||
|
);
|
||||||
|
|
||||||
templates.push({
|
templates.push({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
brandName,
|
brandName,
|
||||||
mode: normalizeAgentPsdTemplateMode(record.mode),
|
mode: normalizeAgentPsdTemplateMode(record.mode),
|
||||||
secondaryCategory: normalizeText(record.secondaryCategory) || undefined,
|
secondaryCategory: normalizeText(record.secondaryCategory) || undefined,
|
||||||
|
categoryTags: categoryTags.length > 0 ? categoryTags : undefined,
|
||||||
description: normalizeText(record.description) || undefined,
|
description: normalizeText(record.description) || undefined,
|
||||||
status: normalizeAgentPsdTemplateStatus(record.status),
|
status: normalizeAgentPsdTemplateStatus(record.status),
|
||||||
parseStatus,
|
parseStatus,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user