From 3ee9701a508bd60792c46105445b44d21878c17c Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Apr 2026 19:41:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=81=94=E5=8A=A8PSD=E5=93=81=E7=B1=BB?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E5=B9=B6=E6=94=AF=E6=8C=81=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=AD=97=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../psd-templates-settings-client.tsx | 66 +++- app/api/admin/psd-fonts/route.ts | 93 +++-- app/api/site-settings/route.ts | 12 + app/canvas/[id]/page.tsx | 4 + .../agents/AgentGenerationWorkspaceDarkV2.tsx | 8 +- .../agents/AgentPsdTemplateEditorDialog.tsx | 2 +- .../agents/AgentWorkflowPanelDarkV2.tsx | 322 ++++++++++-------- lib/agents/psd-font-config.ts | 1 + .../psd-template-category-tag-config.ts | 144 ++++++++ 9 files changed, 459 insertions(+), 193 deletions(-) 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 1f70f44c5..d6bfe6677 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 @@ -257,7 +257,7 @@ export function PsdTemplatesSettingsClient({ const [fontUploadName, setFontUploadName] = useState(""); const [fontFamily, setFontFamily] = useState(""); const [fontUploadBrandName, setFontUploadBrandName] = useState(""); - const [fontUploadFile, setFontUploadFile] = useState(null); + const [fontUploadFiles, setFontUploadFiles] = useState([]); const [isCreating, setIsCreating] = useState(false); const [isCreatingAndParsing, setIsCreatingAndParsing] = useState(false); const [templateUploadPercent, setTemplateUploadPercent] = useState(0); @@ -486,15 +486,15 @@ export function PsdTemplatesSettingsClient({ } function handleFontFileChange(event: ChangeEvent) { - const nextFile = event.target.files?.[0] || null; - setFontUploadFile(nextFile); + const nextFiles = Array.from(event.target.files || []); + setFontUploadFiles(nextFiles); - if (nextFile) { - const normalizedName = nextFile.name.replace(/\.[^.]+$/, ""); - if (!fontUploadName.trim()) { + if (nextFiles.length === 1) { + const normalizedName = nextFiles[0]?.name.replace(/\.[^.]+$/, "") || ""; + if (normalizedName && !fontUploadName.trim()) { setFontUploadName(normalizedName); } - if (!fontFamily.trim()) { + if (normalizedName && !fontFamily.trim()) { setFontFamily(normalizedName); } } @@ -693,16 +693,25 @@ export function PsdTemplatesSettingsClient({ } async function handleUploadFont() { - if (!fontUploadFile) { + if (fontUploadFiles.length === 0) { toast.error("请先选择字体文件"); return; } const formData = new FormData(); - formData.set("name", fontUploadName.trim() || fontUploadFile.name.replace(/\.[^.]+$/, "")); - formData.set("family", fontFamily.trim() || fontUploadName.trim() || "CustomFont"); + if (fontUploadFiles.length === 1) { + formData.set( + "name", + fontUploadName.trim() || + fontUploadFiles[0]?.name.replace(/\.[^.]+$/, "") || + "CustomFont" + ); + } + formData.set("family", fontFamily.trim()); formData.set("brandName", fontUploadBrandName.trim()); - formData.set("file", fontUploadFile); + for (const file of fontUploadFiles) { + formData.append("files", file); + } try { setIsUploadingFont(true); @@ -711,15 +720,20 @@ export function PsdTemplatesSettingsClient({ body: formData, }); const payload = (await response.json()) as AgentPsdFontMutationResponse; - if (!response.ok || !payload.success || !payload.font) { + const uploadedFonts = payload.fonts || (payload.font ? [payload.font] : []); + if (!response.ok || !payload.success || uploadedFonts.length === 0) { throw new Error(payload.error || "字体上传失败"); } - toast.success("字体已上传到字体库"); + toast.success( + uploadedFonts.length > 1 + ? `已批量上传 ${uploadedFonts.length} 个字体` + : "字体已上传到字体库" + ); setFontUploadName(""); setFontFamily(""); setFontUploadBrandName(""); - setFontUploadFile(null); + setFontUploadFiles([]); await mutateFonts(); } catch (error) { toast.error(error instanceof Error ? error.message : "字体上传失败"); @@ -1371,8 +1385,14 @@ export function PsdTemplatesSettingsClient({ id="psdFontName" value={fontUploadName} onChange={(event) => setFontUploadName(event.target.value)} + disabled={fontUploadFiles.length > 1} placeholder="例如:思源黑体-中黑" /> + {fontUploadFiles.length > 1 ? ( +

+ 批量上传时会自动使用每个文件名作为字体名称,这一项不再单独生效。 +

+ ) : null}
@@ -1418,12 +1438,22 @@ export function PsdTemplatesSettingsClient({ id="psdFontFile" type="file" accept=".ttf,.otf,.woff,.woff2" + multiple onChange={handleFontFileChange} />

- {fontUploadFile - ? `已选择:${fontUploadFile.name}` - : "建议按实际字重分别上传多个字体文件"} + {fontUploadFiles.length > 0 + ? fontUploadFiles.length === 1 + ? `已选择:${fontUploadFiles[0]?.name || ""}` + : `已选择 ${fontUploadFiles.length} 个字体文件:${fontUploadFiles + .map((file) => file.name) + .join("、")}` + : "支持一次选择多个字体文件批量上传,建议按实际字重分别整理后再导入"} +

+

+ {fontUploadFiles.length > 1 + ? "批量上传时会使用每个文件名作为字体名称;如果填写了 CSS 字体族,会批量应用到这一批字体。" + : "单个上传时可自定义字体名称和 CSS 字体族。"}

diff --git a/app/api/admin/psd-fonts/route.ts b/app/api/admin/psd-fonts/route.ts index bae32e844..d6f9991ba 100644 --- a/app/api/admin/psd-fonts/route.ts +++ b/app/api/admin/psd-fonts/route.ts @@ -42,6 +42,16 @@ function getDefaultFontName(fileName: string) { return fileName.replace(/\.[^.]+$/, "").trim() || "未命名字体"; } +function getUploadFontFiles(formData: FormData) { + const multipleFiles = formData.getAll("files").filter((item): item is File => item instanceof File); + if (multipleFiles.length > 0) { + return multipleFiles; + } + + const singleFile = formData.get("file"); + return singleFile instanceof File ? [singleFile] : []; +} + export async function GET() { const session = await requireAgentWorkflowAdmin().catch(() => null); if (!session?.user?.id) { @@ -73,14 +83,12 @@ export async function POST(request: NextRequest) { return jsonMutationError(400, "上传表单解析失败"); } - const file = formData.get("file"); - const name = - String(formData.get("name") || "").trim() || - (file instanceof File ? getDefaultFontName(file.name) : ""); - const family = String(formData.get("family") || "").trim() || name; + const files = getUploadFontFiles(formData); + const customName = String(formData.get("name") || "").trim(); + const customFamily = String(formData.get("family") || "").trim(); const brandName = String(formData.get("brandName") || "").trim(); - if (!(file instanceof File)) { + if (files.length === 0) { return jsonMutationError(400, "缺少字体文件"); } @@ -88,39 +96,57 @@ export async function POST(request: NextRequest) { return jsonMutationError(400, "字体品牌不能超过 64 个字符"); } - const extension = getFileExtension(file.name); - if (!ALLOWED_FONT_EXTENSIONS.has(extension)) { - return jsonMutationError(400, "字体文件仅支持 ttf / otf / woff / woff2"); - } + for (const file of files) { + const extension = getFileExtension(file.name); + if (!ALLOWED_FONT_EXTENSIONS.has(extension)) { + return jsonMutationError(400, "字体文件仅支持 ttf / otf / woff / woff2"); + } - if (file.size <= 0) { - return jsonMutationError(400, "字体文件不能为空"); - } + if (file.size <= 0) { + return jsonMutationError(400, "字体文件不能为空"); + } - if (file.size > MAX_FONT_FILE_SIZE) { - return jsonMutationError(400, "字体文件不能超过 20MB"); + if (file.size > MAX_FONT_FILE_SIZE) { + return jsonMutationError(400, "字体文件不能超过 20MB"); + } } try { - const fileBuffer = await file.arrayBuffer(); - const uploaded = await uploadFile(file.name, fileBuffer); const now = new Date().toISOString(); - const nextFont: AgentPsdFont = { - id: randomUUID(), - name, - family, - brandName: brandName || undefined, - fileName: file.name, - fileUrl: uploaded.url, - filePath: uploaded.pathname, - contentType: uploaded.contentType || file.type || undefined, - createdAt: now, - updatedAt: now, - uploadedById: session.user.id, - }; - const currentFonts = await readAgentPsdFonts(); - const fonts = upsertAgentPsdFont(currentFonts, nextFont); + const createdFonts = await Promise.all( + files.map(async (file, index) => { + const fileBuffer = await file.arrayBuffer(); + const uploaded = await uploadFile(file.name, fileBuffer); + const defaultName = getDefaultFontName(file.name); + const resolvedName = + files.length === 1 ? customName || defaultName : defaultName; + const resolvedFamily = + customFamily || + (files.length === 1 ? customName || defaultName : defaultName); + + const nextFont: AgentPsdFont = { + id: randomUUID(), + name: resolvedName, + family: resolvedFamily, + brandName: brandName || undefined, + fileName: file.name, + fileUrl: uploaded.url, + filePath: uploaded.pathname, + contentType: uploaded.contentType || file.type || undefined, + createdAt: now, + updatedAt: new Date(Date.parse(now) + index).toISOString(), + uploadedById: session.user.id, + }; + + return nextFont; + }) + ); + + const fonts = createdFonts.reduce( + (collection, font) => upsertAgentPsdFont(collection, font), + currentFonts + ); await upsertSystemSetting({ key: AGENT_PSD_FONTS_KEY, @@ -130,7 +156,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true, - font: nextFont, + font: createdFonts[0], + fonts: createdFonts, } satisfies AgentPsdFontMutationResponse); } catch (error) { console.error("[PSD Fonts][Admin] create failed:", error); diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 0342c476e..224a761e6 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -69,9 +69,12 @@ import { } from "@/lib/agents/psd-template-secondary-category-config"; import { AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY, + buildAgentPsdTemplateCategoryTagIndex, defaultAgentPsdTemplateCategoryTagConfig, + defaultAgentPsdTemplateCategoryTagIndex, normalizeAgentPsdTemplateCategoryTagConfig, } from "@/lib/agents/psd-template-category-tag-config"; +import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings"; import { AGENT_PRESET_LIBRARY_CONFIG_KEY, defaultAgentPresetLibraryConfig, @@ -284,6 +287,7 @@ export async function GET() { agentPsdTextColorConfigSetting, agentPsdTemplateSecondaryCategoryConfigSetting, agentPsdTemplateCategoryTagConfigSetting, + agentPsdTemplates, ] = await Promise.all([ db.select().from(siteSettings).limit(1).then(rows => rows[0]), db @@ -454,6 +458,7 @@ export async function GET() { .where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY)) .limit(1) .then(rows => rows[0]), + readAgentPsdTemplates().catch(() => []), ]); const notification = notificationSetting?.value || { enabled: false, title: "", content: "" }; @@ -531,6 +536,10 @@ export async function GET() { normalizeAgentPsdTemplateCategoryTagConfig( agentPsdTemplateCategoryTagConfigSetting?.value ); + const agentPsdTemplateCategoryTagIndex = buildAgentPsdTemplateCategoryTagIndex({ + templates: agentPsdTemplates, + config: agentPsdTemplateCategoryTagConfig, + }); // 如果没有设置记录,返回默认值 const baseSettings = settings || { @@ -571,6 +580,8 @@ export async function GET() { defaultAgentPsdTemplateSecondaryCategoryConfig, agentPsdTemplateCategoryTagConfig: defaultAgentPsdTemplateCategoryTagConfig, + agentPsdTemplateCategoryTagIndex: + defaultAgentPsdTemplateCategoryTagIndex, realtimeCanvasEnabled: true, realtimeCanvasServiceEnabled: true, storageFailoverConfig, @@ -624,6 +635,7 @@ export async function GET() { agentPsdTextColorConfig, agentPsdTemplateSecondaryCategoryConfig, agentPsdTemplateCategoryTagConfig, + agentPsdTemplateCategoryTagIndex, membershipConfig, membershipUserOverrides, pointsSystemConfig, diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 8054fc596..35edf26e6 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -112,6 +112,7 @@ import { normalizeAgentGenerationProjectPersistedState, } from "@/lib/agents/generation-project-state"; import { normalizeAgentPresetModeConfig } from "@/lib/agents/preset-mode-config"; +import type { AgentPsdTemplateCategoryTagIndex } from "@/lib/agents/psd-template-category-tag-config"; import { agentProjectService } from "@/services/agent-projects"; import type { AgentPreset, @@ -384,6 +385,7 @@ export default function CanvasEditorPage({ params }: PageProps) { realtimeCanvasServiceEnabled?: boolean; agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig; agentWorkflowBrandModelConfig?: unknown; + agentPsdTemplateCategoryTagIndex?: AgentPsdTemplateCategoryTagIndex; agentWorkflowUiConfig?: { steps?: Array<{ key: AgentWorkflowStepKey; @@ -2130,6 +2132,7 @@ export default function CanvasEditorPage({ params }: PageProps) { config={resolvedWorkflowUiConfig} inputs={workflowInputs} onInputsChange={handleWorkflowInputsChange} + psdTemplateCategoryTagIndex={siteSettingsData?.agentPsdTemplateCategoryTagIndex} theme="dark" /> } @@ -2139,6 +2142,7 @@ export default function CanvasEditorPage({ params }: PageProps) { config={resolvedWorkflowUiConfig} inputs={workflowInputs} onInputsChange={handleWorkflowInputsChange} + psdTemplateCategoryTagIndex={siteSettingsData?.agentPsdTemplateCategoryTagIndex} materials={workflowMaterials} onMaterialsChange={handleWorkflowMaterialsChange} selections={sanitizedWorkflowSelections} diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index f89b21224..c35cdca5b 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -5992,8 +5992,6 @@ export function AgentGenerationWorkspaceDarkV2({ resetKeys={[ psdTemplateDialogSourceUrl || "", workflowContext?.brand || "", - workflowContext?.thirdCategory || "", - workflowContext?.fourthCategory || "", workflowContext?.fifthCategory || "", psdTemplateDialogMode, ]} @@ -6035,11 +6033,7 @@ export function AgentGenerationWorkspaceDarkV2({ }} sourceImageUrl={psdTemplateDialogSourceUrl} brandName={workflowContext?.brand || null} - selectedCategoryNames={[ - workflowContext?.fifthCategory || "", - workflowContext?.fourthCategory || "", - workflowContext?.thirdCategory || "", - ].filter(Boolean)} + selectedCategoryNames={[workflowContext?.fifthCategory || ""].filter(Boolean)} sizeInfoText={workflowContext?.summaryText || null} templateMode={psdTemplateDialogMode} /> diff --git a/components/agents/AgentPsdTemplateEditorDialog.tsx b/components/agents/AgentPsdTemplateEditorDialog.tsx index b8bcf57a4..7cd2aae38 100644 --- a/components/agents/AgentPsdTemplateEditorDialog.tsx +++ b/components/agents/AgentPsdTemplateEditorDialog.tsx @@ -183,7 +183,7 @@ function matchesTemplateSelectedCategories( template.categoryTags ); if (rawTemplateCategoryTags.length === 0) { - return true; + return false; } const templateCategoryTags = getEffectiveTemplateCategoryTags( diff --git a/components/agents/AgentWorkflowPanelDarkV2.tsx b/components/agents/AgentWorkflowPanelDarkV2.tsx index bec8e2407..18af6367b 100644 --- a/components/agents/AgentWorkflowPanelDarkV2.tsx +++ b/components/agents/AgentWorkflowPanelDarkV2.tsx @@ -15,7 +15,6 @@ import { flattenVisibleAgentWorkflowSteps, getAgentWorkflowBlockedOptionKeysForSelections, getAgentWorkflowBrandStoreEntries, - getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth, getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory, getAgentWorkflowPriceBandOptions, getAgentWorkflowThirdCategoryOptionsByBrandStore, @@ -34,6 +33,11 @@ import { buildAgentWorkflowComposerContext, getAgentWorkflowProgress, } from "@/lib/agents/workflow-composer"; +import { + getAgentPsdTemplateCategoryTagsByBrand, + normalizeAgentPsdTemplateCategoryTagIndex, + type AgentPsdTemplateCategoryTagIndex, +} from "@/lib/agents/psd-template-category-tag-config"; import type { AgentWorkflowComposerSelection, AgentWorkflowMaterialSelections, @@ -57,6 +61,7 @@ interface AgentWorkflowPanelDarkV2Props { config?: AgentWorkflowUiConfig | null; inputs: AgentWorkflowInputs; onInputsChange: (next: AgentWorkflowInputs) => void; + psdTemplateCategoryTagIndex?: AgentPsdTemplateCategoryTagIndex | null; materials: AgentWorkflowMaterialSelections; onMaterialsChange: (next: AgentWorkflowMaterialSelections) => void; selections: Partial>; @@ -77,6 +82,7 @@ interface AgentWorkflowBrandStorePanelProps { config?: AgentWorkflowUiConfig | null; inputs: AgentWorkflowInputs; onInputsChange: (next: AgentWorkflowInputs) => void; + psdTemplateCategoryTagIndex?: AgentPsdTemplateCategoryTagIndex | null; theme?: "light" | "dark"; title?: string; description?: string; @@ -281,6 +287,7 @@ export function AgentWorkflowBrandStorePanel({ config, inputs, onInputsChange, + psdTemplateCategoryTagIndex, theme = "dark", title = "品牌层级选择", description = "先选择品牌、店铺、分类层级和价格带,后续品牌模特与风格步骤会按这里的选择联动。", @@ -331,16 +338,18 @@ export function AgentWorkflowBrandStorePanel({ }), [config, inputs.brand, inputs.store, inputs.thirdCategory] ); + const normalizedPsdTemplateCategoryTagIndex = useMemo( + () => + normalizeAgentPsdTemplateCategoryTagIndex(psdTemplateCategoryTagIndex), + [psdTemplateCategoryTagIndex] + ); const fifthCategoryOptions = useMemo( () => - getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({ - config, - brand: inputs.brand, - store: inputs.store, - thirdCategory: inputs.thirdCategory, - fourthCategory: inputs.fourthCategory, - }), - [config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory] + getAgentPsdTemplateCategoryTagsByBrand( + normalizedPsdTemplateCategoryTagIndex, + inputs.brand + ), + [inputs.brand, normalizedPsdTemplateCategoryTagIndex] ); const priceBandOptions = useMemo( () => getAgentWorkflowPriceBandOptions(config), @@ -371,56 +380,52 @@ export function AgentWorkflowBrandStorePanel({ ) || 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(() => { + let nextInputs = inputs; + let changed = false; + if (!hasThirdCategoryLevel) { - if (inputs.thirdCategory || inputs.fourthCategory || inputs.fifthCategory) { - onInputsChange({ - ...inputs, + if (nextInputs.thirdCategory || nextInputs.fourthCategory) { + nextInputs = { + ...nextInputs, thirdCategory: "", fourthCategory: "", - fifthCategory: "", - }); + }; + changed = true; } - return; - } - - if (!hasFourthCategoryLevel) { - if (inputs.fourthCategory || inputs.fifthCategory) { - onInputsChange({ - ...inputs, + } else if (!hasFourthCategoryLevel) { + if (nextInputs.fourthCategory) { + nextInputs = { + ...nextInputs, fourthCategory: "", - fifthCategory: "", - }); + }; + changed = true; } - return; } - if (!hasFifthCategoryLevel && inputs.fifthCategory) { - onInputsChange({ - ...inputs, + if ( + nextInputs.fifthCategory && + !fifthCategoryOptions.some( + (option) => + option.trim().toLowerCase() === nextInputs.fifthCategory.trim().toLowerCase() + ) + ) { + nextInputs = { + ...nextInputs, fifthCategory: "", - }); + }; + changed = true; + } + + if (changed) { + onInputsChange(nextInputs); } }, [ - hasFifthCategoryLevel, - hasFourthCategoryLevel, + fifthCategoryOptions, hasThirdCategoryLevel, + hasFourthCategoryLevel, inputs, onInputsChange, ]); @@ -486,7 +491,6 @@ export function AgentWorkflowBrandStorePanel({ ? inputs.thirdCategory : "", fourthCategory: "", - fifthCategory: "", }) } /> @@ -511,7 +515,6 @@ export function AgentWorkflowBrandStorePanel({ value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase() ? inputs.fourthCategory : "", - fifthCategory: "", }) } /> @@ -533,30 +536,24 @@ export function AgentWorkflowBrandStorePanel({ onInputsChange({ ...inputs, fourthCategory: value, - fifthCategory: - value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase() - ? inputs.fifthCategory - : "", }) } /> ) : null} - {hasFifthCategoryLevel ? ( - onInputsChange({ ...inputs, fifthCategory: value })} - /> - ) : null} + onInputsChange({ ...inputs, fifthCategory: value })} + /> + activeEmbeddedStepEntry + ? getAgentWorkflowSelectedOptionPath({ + step: activeEmbeddedStepEntry.step, + selectionValue: selections[activeEmbeddedStepEntry.step.key], + }) + : [], + [activeEmbeddedStepEntry, selections] + ); const activeEmbeddedOptionLevel = activeEmbeddedOptionLevels.find((level) => level.depth === activeEmbeddedOptionDepth) || activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1] || @@ -938,6 +946,7 @@ export function AgentWorkflowPanelDarkV2({ config={config} inputs={inputs} onInputsChange={onInputsChange} + psdTemplateCategoryTagIndex={psdTemplateCategoryTagIndex} theme="dark" /> @@ -958,7 +967,7 @@ export function AgentWorkflowPanelDarkV2({ >
-
+
{flattenedResolvedSteps.map(({ step }, stepIndex) => { const completed = isAgentWorkflowStepSelectionComplete({ step, @@ -969,16 +978,17 @@ export function AgentWorkflowPanelDarkV2({ step, selectionValue: selections[step.key], }); - const stepOptionLevels = getWorkflowStepOptionLevels({ - step, - selectionValue: selections[step.key], - }); + const primarySelectedOption = stepSelectedPath[0] || null; + const selectedCountLabel = + stepSelectedPath.length > 0 + ? `${stepSelectedPath.length} 项已选` + : "待选择"; return (
-
+
-
- {stepSelectedPath.length > 0 ? ( -
- {stepSelectedPath.map((option, optionIndex) => { - const optionDepth = optionIndex + 1; - const optionLevelActive = - active && - (activeEmbeddedOptionLevel?.depth || 1) === optionDepth; - - return ( - - ); - })} -
- ) : ( - - 待选择 - - )} -
); })}
+ +
+
+
+ + 当前步骤路径 + + + {activeEmbeddedStepEntry.step.title} + +
+ + {activeEmbeddedSelectedPath.length > 0 + ? `已选 ${activeEmbeddedSelectedPath.length} 项` + : "还未选择主卡片"} + +
+ + {activeEmbeddedSelectedPath.length > 0 ? ( +
+ {activeEmbeddedSelectedPath.map((option, optionIndex) => { + const optionDepth = optionIndex + 1; + const optionLevelActive = + (activeEmbeddedOptionLevel?.depth || 1) === optionDepth; + + return ( + + ); + })} +
+ ) : ( +
+ 先选择当前步骤的主卡片,完整子节点路径会显示在这里,顶部步骤条只保留紧凑摘要。 +
+ )} +
diff --git a/lib/agents/psd-font-config.ts b/lib/agents/psd-font-config.ts index b18f55313..7ec2f3e61 100644 --- a/lib/agents/psd-font-config.ts +++ b/lib/agents/psd-font-config.ts @@ -23,6 +23,7 @@ export interface AgentPsdFontListResponse { export interface AgentPsdFontMutationResponse { success: boolean; font?: AgentPsdFont; + fonts?: AgentPsdFont[]; error?: string; } diff --git a/lib/agents/psd-template-category-tag-config.ts b/lib/agents/psd-template-category-tag-config.ts index b8e9d9b6f..37c368597 100644 --- a/lib/agents/psd-template-category-tag-config.ts +++ b/lib/agents/psd-template-category-tag-config.ts @@ -1,7 +1,18 @@ +import type { AgentPsdTemplate } from "@/lib/agents/psd-template-config"; + export interface AgentPsdTemplateCategoryTagConfig { tags: string[]; } +export interface AgentPsdTemplateCategoryTagIndexEntry { + brandName: string; + tags: string[]; +} + +export interface AgentPsdTemplateCategoryTagIndex { + entries: AgentPsdTemplateCategoryTagIndexEntry[]; +} + export const AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY = "agent_psd_template_category_tag_config"; export const AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG = "适合所有"; @@ -57,6 +68,14 @@ export const defaultAgentPsdTemplateCategoryTagConfig: AgentPsdTemplateCategoryT tags: [], }; +export const defaultAgentPsdTemplateCategoryTagIndex: AgentPsdTemplateCategoryTagIndex = { + entries: [], +}; + +function normalizeLookupText(value: unknown) { + return String(value || "").trim().toLowerCase(); +} + export function normalizeAgentPsdTemplateCategoryTagConfig( value: unknown ): AgentPsdTemplateCategoryTagConfig { @@ -72,6 +91,131 @@ export function normalizeAgentPsdTemplateCategoryTagConfig( }; } +export function normalizeAgentPsdTemplateCategoryTagIndex( + value: unknown +): AgentPsdTemplateCategoryTagIndex { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return defaultAgentPsdTemplateCategoryTagIndex; + } + + const rawEntries = Array.isArray((value as Record).entries) + ? ((value as Record).entries as unknown[]) + : []; + const entries: AgentPsdTemplateCategoryTagIndexEntry[] = []; + const seenBrands = new Set(); + + for (const item of rawEntries) { + if (!item || typeof item !== "object" || Array.isArray(item)) { + continue; + } + + const record = item as Record; + const brandName = String(record.brandName || "").trim(); + const tags = normalizeAgentPsdTemplateCategoryTags(record.tags).filter( + (tag) => tag !== AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG + ); + const brandKey = normalizeLookupText(brandName); + + if (!brandName || !brandKey || seenBrands.has(brandKey)) { + continue; + } + + seenBrands.add(brandKey); + entries.push({ + brandName, + tags, + }); + } + + return { entries }; +} + +export function buildAgentPsdTemplateCategoryTagIndex(params: { + templates: AgentPsdTemplate[]; + config?: AgentPsdTemplateCategoryTagConfig | null; +}) { + const normalizedConfig = normalizeAgentPsdTemplateCategoryTagConfig(params.config); + const activeTags = normalizedConfig.tags; + const activeTagKeySet = new Set(activeTags.map((tag) => normalizeLookupText(tag))); + const builders = new Map< + string, + { + brandName: string; + tagKeys: Set; + hasUniversalTemplate: boolean; + } + >(); + + for (const template of params.templates || []) { + if ( + template.status !== "active" || + template.parseStatus !== "completed" || + !template.layers.some((layer) => layer.isEditable) + ) { + continue; + } + + const brandName = String(template.brandName || "").trim(); + const brandKey = normalizeLookupText(brandName); + if (!brandName || !brandKey) { + continue; + } + + const categoryTags = normalizeAgentPsdTemplateCategoryTags(template.categoryTags); + if (categoryTags.length === 0) { + continue; + } + + const currentBuilder = + builders.get(brandKey) || + { + brandName, + tagKeys: new Set(), + hasUniversalTemplate: false, + }; + + if (categoryTags.includes(AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG)) { + currentBuilder.hasUniversalTemplate = true; + } + + for (const tag of categoryTags) { + const tagKey = normalizeLookupText(tag); + if (!tagKey || !activeTagKeySet.has(tagKey)) { + continue; + } + currentBuilder.tagKeys.add(tagKey); + } + + builders.set(brandKey, currentBuilder); + } + + return { + entries: Array.from(builders.values()).map((entry) => ({ + brandName: entry.brandName, + tags: entry.hasUniversalTemplate + ? activeTags + : activeTags.filter((tag) => entry.tagKeys.has(normalizeLookupText(tag))), + })), + } satisfies AgentPsdTemplateCategoryTagIndex; +} + +export function getAgentPsdTemplateCategoryTagsByBrand( + index?: AgentPsdTemplateCategoryTagIndex | null, + brandName?: string | null +) { + const normalizedIndex = normalizeAgentPsdTemplateCategoryTagIndex(index); + const brandKey = normalizeLookupText(brandName); + if (!brandKey) { + return []; + } + + return ( + normalizedIndex.entries.find( + (entry) => normalizeLookupText(entry.brandName) === brandKey + )?.tags || [] + ); +} + export function getAgentPsdTemplateCategoryTagOptions( config?: AgentPsdTemplateCategoryTagConfig | null, extraTags?: unknown