feat: filter PSD templates by brand
This commit is contained in:
parent
954d708e30
commit
305e1e5161
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -5831,6 +5831,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}
|
||||
}}
|
||||
sourceImageUrl={psdTemplateDialogSourceUrl}
|
||||
brandName={workflowContext?.brand || null}
|
||||
/>
|
||||
|
||||
{/* Fullscreen Preview */}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user