feat: filter PSD templates by brand
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-27 16:46:56 +08:00
parent 954d708e30
commit 305e1e5161
7 changed files with 137 additions and 11 deletions

View File

@ -67,18 +67,40 @@ export function PsdTemplatesSettingsClient({
backLabel: string;
}) {
const { data, mutate } = useSWR<AgentPsdTemplateListResponse>("/api/admin/psd-templates", fetcher);
const { data: siteSettingsData } = useSWR<{
agentWorkflowUiConfig?: {
inputOptions?: {
brands?: string[];
};
};
}>("/api/site-settings", fetcher);
const { data: fontsData, mutate: mutateFonts } = useSWR<AgentPsdFontListResponse>(
"/api/admin/psd-fonts",
fetcher
);
const templates = useMemo(() => (data?.success ? data.templates : []), [data]);
const brandOptions = useMemo(
() =>
Array.from(
new Set(
[
...(siteSettingsData?.agentWorkflowUiConfig?.inputOptions?.brands || []),
...templates.map((template) => template.brandName),
]
.map((item) => String(item || "").trim())
.filter(Boolean)
)
),
[siteSettingsData, templates]
);
const fonts = useMemo(() => (fontsData?.success ? fontsData.fonts : []), [fontsData]);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
const [draftTemplate, setDraftTemplate] = useState<AgentPsdTemplate | null>(null);
const [layerKeyword, setLayerKeyword] = useState("");
const [uploadName, setUploadName] = useState("");
const [uploadBrandName, setUploadBrandName] = useState("");
const [uploadDescription, setUploadDescription] = useState("");
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [fontUploadName, setFontUploadName] = useState("");
@ -177,8 +199,14 @@ export function PsdTemplatesSettingsClient({
return;
}
if (!uploadBrandName.trim()) {
toast.error("请先选择模板品牌");
return;
}
const formData = new FormData();
formData.set("name", uploadName.trim() || uploadFile.name.replace(/\.[^.]+$/, ""));
formData.set("brandName", uploadBrandName.trim());
formData.set("description", uploadDescription.trim());
formData.set("status", "disabled");
formData.set("file", uploadFile);
@ -197,6 +225,7 @@ export function PsdTemplatesSettingsClient({
toast.success("PSD 模板已上传,请继续配置图层");
setUploadName("");
setUploadBrandName("");
setUploadDescription("");
setUploadFile(null);
await mutate();
@ -279,6 +308,7 @@ export function PsdTemplatesSettingsClient({
},
body: JSON.stringify({
name: draftTemplate.name,
brandName: draftTemplate.brandName,
description: draftTemplate.description || "",
status: draftTemplate.status,
layers: draftTemplate.layers,
@ -421,6 +451,29 @@ export function PsdTemplatesSettingsClient({
placeholder="例如:主图包装模板"
/>
</div>
<div className="space-y-2">
<Label>所属品牌</Label>
{brandOptions.length > 0 ? (
<Select value={uploadBrandName} onValueChange={setUploadBrandName}>
<SelectTrigger>
<SelectValue placeholder="请选择品牌" />
</SelectTrigger>
<SelectContent>
{brandOptions.map((brand) => (
<SelectItem key={brand} value={brand}>
{brand}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={uploadBrandName}
onChange={(event) => setUploadBrandName(event.target.value)}
placeholder="请输入品牌名称"
/>
)}
</div>
<div className="space-y-2">
<Label htmlFor="psdTemplateDescription">模板说明</Label>
<Textarea
@ -563,11 +616,14 @@ export function PsdTemplatesSettingsClient({
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-medium">{template.name}</div>
<div className="mt-1 text-xs text-muted-foreground">
{template.width} x {template.height} · {template.layerCount} 图层
</div>
<div className="truncate text-sm font-medium">{template.name}</div>
<div className="mt-1 text-xs text-muted-foreground">
{template.width} x {template.height} · {template.layerCount} 图层
</div>
<div className="mt-1 text-xs text-muted-foreground">
品牌:{template.brandName}
</div>
</div>
<Badge variant={template.status === "active" ? "default" : "outline"}>
{template.status === "active" ? "启用" : "停用"}
</Badge>
@ -634,6 +690,43 @@ export function PsdTemplatesSettingsClient({
}
/>
</div>
<div className="space-y-2">
<Label>所属品牌</Label>
{brandOptions.length > 0 ? (
<Select
value={draftTemplate.brandName}
onValueChange={(value) =>
setDraftTemplate((current) =>
current ? { ...current, brandName: value } : current
)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{brandOptions.map((brand) => (
<SelectItem key={brand} value={brand}>
{brand}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={draftTemplate.brandName}
onChange={(event) =>
setDraftTemplate((current) =>
current ? { ...current, brandName: event.target.value } : current
)
}
placeholder="请输入品牌名称"
/>
)}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>启用状态</Label>
<Select
@ -656,7 +749,7 @@ export function PsdTemplatesSettingsClient({
<SelectItem value="disabled">停用</SelectItem>
<SelectItem value="active">启用</SelectItem>
</SelectContent>
</Select>
</Select>
</div>
</div>
@ -676,6 +769,7 @@ export function PsdTemplatesSettingsClient({
</div>
<div className="flex flex-wrap items-center gap-3">
<Badge variant="secondary">品牌:{draftTemplate.brandName}</Badge>
<Badge variant="outline">文件:{draftTemplate.fileName}</Badge>
<Badge variant="secondary">上传图层数:{draftTemplate.layerCount}</Badge>
<Button

View File

@ -22,6 +22,7 @@ export const runtime = "nodejs";
const updateTemplateSchema = z.object({
name: z.string().trim().min(1).max(80),
brandName: z.string().trim().min(1).max(64),
description: z.string().optional().nullable(),
status: z.string().optional(),
layers: z.array(z.any()).default([]),
@ -142,6 +143,7 @@ export async function PUT(
const updatedTemplate = {
...currentTemplate,
name: body.name.trim(),
brandName: body.brandName.trim(),
description: String(body.description || "").trim() || undefined,
status: normalizeAgentPsdTemplateStatus(body.status),
renderBaseUrl: uploadedRenderBase.url,

View File

@ -80,6 +80,7 @@ export async function POST(request: NextRequest) {
const name =
String(formData.get("name") || "").trim() ||
(file instanceof File ? getDefaultTemplateName(file.name) : "");
const brandName = String(formData.get("brandName") || "").trim();
const description = String(formData.get("description") || "").trim();
const status = normalizeAgentPsdTemplateStatus(formData.get("status"));
@ -87,10 +88,18 @@ export async function POST(request: NextRequest) {
return jsonMutationError(400, "缺少模板名称");
}
if (!brandName) {
return jsonMutationError(400, "请选择模板品牌");
}
if (name.length > 80) {
return jsonMutationError(400, "模板名称不能超过 80 个字符");
}
if (brandName.length > 64) {
return jsonMutationError(400, "模板品牌不能超过 64 个字符");
}
if (!(file instanceof File)) {
return jsonMutationError(400, "缺少 PSD 模板文件");
}
@ -131,6 +140,7 @@ export async function POST(request: NextRequest) {
const nextTemplate: AgentPsdTemplate = {
id: randomUUID(),
name,
brandName,
description: description || undefined,
status,
fileName: file.name,

View File

@ -1,4 +1,4 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/(auth)/auth";
import {
getEditableAgentPsdTemplateLayers,
@ -18,15 +18,19 @@ function jsonError(status: number, error: string) {
return NextResponse.json(payload, { status });
}
export async function GET() {
export async function GET(request: NextRequest) {
const session = await auth().catch(() => null);
if (!session?.user?.id) {
return jsonError(401, "未登录");
}
try {
const requestedBrand = String(request.nextUrl.searchParams.get("brand") || "").trim();
const templates = (await readAgentPsdTemplates())
.filter((template) => template.status === "active")
.filter((template) =>
requestedBrand ? template.brandName === requestedBrand : true
)
.map((template) => ({
...template,
layers: getEditableAgentPsdTemplateLayers(template),

View File

@ -5831,6 +5831,7 @@ export function AgentGenerationWorkspaceDarkV2({
}
}}
sourceImageUrl={psdTemplateDialogSourceUrl}
brandName={workflowContext?.brand || null}
/>
{/* Fullscreen Preview */}

View File

@ -60,13 +60,18 @@ export function AgentPsdTemplateEditorDialog({
open,
onOpenChange,
sourceImageUrl,
brandName,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
sourceImageUrl: string | null;
brandName?: string | null;
}) {
const normalizedBrandName = String(brandName || "").trim();
const { data, isLoading } = useSWR<AgentPsdTemplateListResponse>(
open ? "/api/psd-templates" : null,
open && normalizedBrandName
? `/api/psd-templates?brand=${encodeURIComponent(normalizedBrandName)}`
: null,
fetcher
);
const templates = useMemo(() => (data?.success ? data.templates : []), [data]);
@ -212,6 +217,7 @@ export function AgentPsdTemplateEditorDialog({
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{selectedTemplate.brandName}</Badge>
<Badge variant="secondary">
{selectedTemplate.width} x {selectedTemplate.height}
</Badge>
@ -246,7 +252,11 @@ export function AgentPsdTemplateEditorDialog({
</>
) : (
<div className="rounded-2xl border border-dashed p-5 text-sm text-muted-foreground">
{isLoading ? "正在加载可用模板..." : "当前还没有启用的 PSD 模板,请先去后台配置。"}
{isLoading
? "正在加载可用模板..."
: normalizedBrandName
? `当前品牌“${normalizedBrandName}”下还没有可用 PSD 模板,请先去后台配置。`
: "当前还没有启用的 PSD 模板,请先去后台配置。"}
</div>
)}
</div>
@ -409,7 +419,7 @@ export function AgentPsdTemplateEditorDialog({
})
) : (
<div className="rounded-2xl border border-dashed p-5 text-sm text-muted-foreground">
暂无可编辑槽位。
{normalizedBrandName ? "暂无可编辑槽位。" : "请先在工作流中选择品牌后再使用 PSD 模板。"}
</div>
)}
</div>
@ -452,7 +462,9 @@ export function AgentPsdTemplateEditorDialog({
</div>
<div className="mt-4 text-sm font-medium">结果还没生成</div>
<div className="mt-2 text-xs leading-6 text-muted-foreground">
选好模板和槽位内容后点击“生成套版结果”,这里会显示导出的图片。
{normalizedBrandName
? "选好模板和槽位内容后点击“生成套版结果”,这里会显示导出的图片。"
: "请先返回工作流区域选择品牌,再打开 PSD 模板。"}
</div>
</div>
)}

View File

@ -55,6 +55,7 @@ export type AgentPsdTemplateLayerSlot =
export interface AgentPsdTemplate {
id: string;
name: string;
brandName: string;
description?: string;
status: AgentPsdTemplateStatus;
fileName: string;
@ -285,6 +286,7 @@ export function parseAgentPsdTemplates(value: unknown) {
const record = entry as Record<string, unknown>;
const id = normalizeText(record.id);
const name = normalizeText(record.name);
const brandName = normalizeText(record.brandName) || "未分配品牌";
const fileName = normalizeText(record.fileName);
const fileUrl = normalizeUrl(record.fileUrl);
const previewUrl = normalizeUrl(record.previewUrl);
@ -312,6 +314,7 @@ export function parseAgentPsdTemplates(value: unknown) {
templates.push({
id,
name,
brandName,
description: normalizeText(record.description) || undefined,
status: normalizeAgentPsdTemplateStatus(record.status),
fileName,