From f6d8a4b8eacdf20086b59a94809cb1534f1744bd Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Apr 2026 14:30:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81PSD=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=93=81=E7=B1=BB=E6=A0=87=E7=AD=BE=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../psd-templates-settings-client.tsx | 288 +++++++++++++++++- app/api/admin/psd-templates/[id]/route.ts | 5 + app/api/admin/psd-templates/route.ts | 5 + app/api/psd-templates/route.ts | 34 ++- app/api/site-settings/route.ts | 106 +++++++ .../agents/AgentGenerationWorkspaceDarkV2.tsx | 14 +- .../agents/AgentPsdTemplateEditorDialog.tsx | 207 +++++++++++-- .../psd-template-category-tag-config.ts | 89 ++++++ lib/agents/psd-template-config.ts | 9 + 9 files changed, 730 insertions(+), 27 deletions(-) create mode 100644 lib/agents/psd-template-category-tag-config.ts diff --git a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx index 89c91a714..1f70f44c5 100644 --- a/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/psd-templates/psd-templates-settings-client.tsx @@ -44,6 +44,12 @@ import { getAgentWorkflowCategoryLabels, getAgentWorkflowCategoryOptionsByBrand, } 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 { AgentPsdFont, AgentPsdFontListResponse, @@ -98,6 +104,30 @@ function normalizeManagedBrandName(value?: string | null) { 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) { return template ? (JSON.parse(JSON.stringify(template)) as AgentPsdTemplate) : null; } @@ -178,8 +208,11 @@ export function PsdTemplatesSettingsClient({ backLabel: string; }) { const { data, mutate } = useSWR("/api/admin/psd-templates", fetcher); - const { data: siteSettingsData } = useSWR<{ + const { data: siteSettingsData, mutate: mutateSiteSettings } = useSWR<{ agentWorkflowUiConfig?: AgentWorkflowUiConfig; + agentPsdTemplateCategoryTagConfig?: { + tags?: string[]; + }; }>("/api/site-settings", fetcher); const { data: fontsData, mutate: mutateFonts } = useSWR( "/api/admin/psd-fonts", @@ -218,6 +251,7 @@ export function PsdTemplatesSettingsClient({ const [uploadBrandName, setUploadBrandName] = useState(""); const [uploadMode, setUploadMode] = useState("main"); const [uploadSecondaryCategory, setUploadSecondaryCategory] = useState(""); + const [uploadCategoryTags, setUploadCategoryTags] = useState([]); const [uploadDescription, setUploadDescription] = useState(""); const [uploadFile, setUploadFile] = useState(null); const [fontUploadName, setFontUploadName] = useState(""); @@ -235,6 +269,8 @@ export function PsdTemplatesSettingsClient({ const [deletingFontId, setDeletingFontId] = useState(null); const [savingTemplateBrandId, setSavingTemplateBrandId] = useState(null); const [savingFontBrandId, setSavingFontBrandId] = useState(null); + const [categoryTagDraft, setCategoryTagDraft] = useState(""); + const [isSavingCategoryTags, setIsSavingCategoryTags] = useState(false); const [isFontListExpanded, setIsFontListExpanded] = useState(false); const [templateBrandDrafts, setTemplateBrandDrafts] = useState>({}); const [fontBrandDrafts, setFontBrandDrafts] = useState>({}); @@ -249,6 +285,25 @@ export function PsdTemplatesSettingsClient({ () => getAgentWorkflowCategoryLabels(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( () => [ { @@ -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 }) { const parseAfterUpload = options?.parseAfterUpload === true; if (!uploadFile) { @@ -462,6 +592,9 @@ export function PsdTemplatesSettingsClient({ formData.set("brandName", uploadBrandName.trim()); formData.set("mode", uploadMode); formData.set("secondaryCategory", uploadSecondaryCategory.trim()); + uploadCategoryTags.forEach((tag) => { + formData.append("categoryTags", tag); + }); formData.set("description", uploadDescription.trim()); formData.set("status", "disabled"); formData.set("file", uploadFile); @@ -528,6 +661,7 @@ export function PsdTemplatesSettingsClient({ setUploadName(""); setUploadBrandName(""); setUploadSecondaryCategory(""); + setUploadCategoryTags([]); setUploadDescription(""); setUploadFile(null); await mutate(); @@ -676,6 +810,7 @@ export function PsdTemplatesSettingsClient({ brandName: normalizedBrandName, mode: template.mode, secondaryCategory: template.secondaryCategory || "", + categoryTags: template.categoryTags || [], description: template.description || "", status: template.status, layers: template.layers, @@ -721,6 +856,7 @@ export function PsdTemplatesSettingsClient({ brandName: draftTemplate.brandName, mode: draftTemplate.mode, secondaryCategory: draftTemplate.secondaryCategory || "", + categoryTags: draftTemplate.categoryTags || [], description: draftTemplate.description || "", status: draftTemplate.status, layers: draftTemplate.layers, @@ -942,6 +1078,73 @@ export function PsdTemplatesSettingsClient({
+ + + 品类标签 + + 管理 PSD 模板可绑定的品类标签。前端会根据用户当前选择的产品品类筛选模板。 + + + +
+ +
+ setCategoryTagDraft(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + void handleAddCategoryTag(); + } + }} + placeholder="例如:椅子类 / 沙发类 / 餐边柜类" + disabled={isSavingCategoryTags} + /> + +
+
+ +
+ +
+ {AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG} + {categoryTagConfig.tags.map((tag) => ( +
+ {tag} + +
+ ))} +
+

+ “适合所有”是固定标签,会让模板在所有品类下都显示;自定义标签删除后,不会再出现在新模板选择项中。 +

+
+
+
+ 上传新模板 @@ -1062,6 +1265,35 @@ export function PsdTemplatesSettingsClient({ 会优先读取当前品牌下已配置的分类层级作为选项;开启前台模板二级分类后,会按这里的分类分组展示模板。

+
+ +
+ {availableCategoryTagOptions.map((tag) => { + const isSelected = hasTemplateCategoryTag(uploadCategoryTags, tag); + return ( + + ); + })} +
+

+ 可多选。前端会根据用户当前选择的产品品类筛选模板;勾选“适合所有”后,这个模板会在所有品类下都显示。 +

+