feat: 支持PSD模板品类标签筛选
This commit is contained in:
parent
5037a1a1eb
commit
f6d8a4b8ea
@ -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<AgentPsdTemplateListResponse>("/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<AgentPsdFontListResponse>(
|
||||
"/api/admin/psd-fonts",
|
||||
@ -218,6 +251,7 @@ export function PsdTemplatesSettingsClient({
|
||||
const [uploadBrandName, setUploadBrandName] = useState("");
|
||||
const [uploadMode, setUploadMode] = useState<AgentPsdTemplateMode>("main");
|
||||
const [uploadSecondaryCategory, setUploadSecondaryCategory] = useState("");
|
||||
const [uploadCategoryTags, setUploadCategoryTags] = useState<string[]>([]);
|
||||
const [uploadDescription, setUploadDescription] = useState("");
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [fontUploadName, setFontUploadName] = useState("");
|
||||
@ -235,6 +269,8 @@ export function PsdTemplatesSettingsClient({
|
||||
const [deletingFontId, setDeletingFontId] = useState<string | null>(null);
|
||||
const [savingTemplateBrandId, setSavingTemplateBrandId] = 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 [templateBrandDrafts, setTemplateBrandDrafts] = useState<Record<string, string>>({});
|
||||
const [fontBrandDrafts, setFontBrandDrafts] = useState<Record<string, string>>({});
|
||||
@ -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({
|
||||
|
||||
<div className="container mx-auto grid gap-6 px-6 py-6 lg:grid-cols-[360px,minmax(0,1fr)]">
|
||||
<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>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">上传新模板</CardTitle>
|
||||
@ -1062,6 +1265,35 @@ export function PsdTemplatesSettingsClient({
|
||||
会优先读取当前品牌下已配置的分类层级作为选项;开启前台模板二级分类后,会按这里的分类分组展示模板。
|
||||
</p>
|
||||
</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">
|
||||
<Label htmlFor="psdTemplateDescription">模板说明</Label>
|
||||
<Textarea
|
||||
@ -1413,6 +1645,15 @@ export function PsdTemplatesSettingsClient({
|
||||
二级分类:{template.secondaryCategory}
|
||||
</div>
|
||||
) : 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 ? (
|
||||
<div className="mt-2 line-clamp-2 text-xs text-muted-foreground">
|
||||
{template.description}
|
||||
@ -1820,6 +2061,46 @@ export function PsdTemplatesSettingsClient({
|
||||
/>
|
||||
)}
|
||||
</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 className="grid gap-4 md:grid-cols-2">
|
||||
@ -1875,6 +2156,11 @@ export function PsdTemplatesSettingsClient({
|
||||
{draftTemplate.secondaryCategory ? (
|
||||
<Badge variant="secondary">分类:{draftTemplate.secondaryCategory}</Badge>
|
||||
) : null}
|
||||
{(draftTemplate.categoryTags || []).map((tag) => (
|
||||
<Badge key={`draft-template-tag-${tag}`} variant="secondary">
|
||||
品类:{tag}
|
||||
</Badge>
|
||||
))}
|
||||
<Badge variant="outline">文件:{draftTemplate.fileName}</Badge>
|
||||
<Badge variant={getTemplateParseBadgeVariant(draftTemplate)}>
|
||||
{getTemplateParseLabel(draftTemplate)}
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
parseAgentPsdTemplateLayers,
|
||||
type AgentPsdTemplateMutationResponse,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
import { normalizeAgentPsdTemplateCategoryTags } from "@/lib/agents/psd-template-category-tag-config";
|
||||
import {
|
||||
preparePsdTemplateRenderAssets,
|
||||
} from "@/lib/agents/psd-template-parser";
|
||||
@ -31,6 +32,7 @@ const updateTemplateSchema = z.object({
|
||||
brandName: z.string().trim().min(1).max(64),
|
||||
mode: z.string().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(),
|
||||
status: z.string().optional(),
|
||||
layers: z.array(z.any()).default([]),
|
||||
@ -108,6 +110,7 @@ export async function PUT(
|
||||
|
||||
const nextStatus = normalizeAgentPsdTemplateStatus(body.status);
|
||||
const nextMode = normalizeAgentPsdTemplateMode(body.mode);
|
||||
const nextCategoryTags = normalizeAgentPsdTemplateCategoryTags(body.categoryTags);
|
||||
let nextLayers = parseAgentPsdTemplateLayers(body.layers);
|
||||
if (nextLayers.length === 0) {
|
||||
nextLayers = currentTemplate.layers;
|
||||
@ -124,6 +127,7 @@ export async function PUT(
|
||||
brandName: body.brandName.trim(),
|
||||
mode: nextMode,
|
||||
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
||||
categoryTags: nextCategoryTags.length > 0 ? nextCategoryTags : undefined,
|
||||
description: String(body.description || "").trim() || undefined,
|
||||
status: nextStatus,
|
||||
updatedAt: new Date().toISOString(),
|
||||
@ -214,6 +218,7 @@ export async function PUT(
|
||||
brandName: body.brandName.trim(),
|
||||
mode: nextMode,
|
||||
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
||||
categoryTags: nextCategoryTags.length > 0 ? nextCategoryTags : undefined,
|
||||
description: String(body.description || "").trim() || undefined,
|
||||
status: nextStatus,
|
||||
renderBaseUrl: uploadedRenderBase.url,
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
type AgentPsdTemplateListResponse,
|
||||
type AgentPsdTemplateMutationResponse,
|
||||
} 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 { 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 mode = normalizeAgentPsdTemplateMode(formData.get("mode"));
|
||||
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 status = normalizeAgentPsdTemplateStatus(formData.get("status"));
|
||||
|
||||
@ -173,6 +177,7 @@ export async function POST(request: NextRequest) {
|
||||
brandName,
|
||||
mode,
|
||||
secondaryCategory: secondaryCategory || undefined,
|
||||
categoryTags: categoryTags.length > 0 ? categoryTags : undefined,
|
||||
description: description || undefined,
|
||||
status,
|
||||
parseStatus: "pending",
|
||||
|
||||
@ -23,6 +23,11 @@ import {
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} 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 { repairAgentPsdTemplateAssetsIfNeeded } from "@/lib/agents/psd-template-repair";
|
||||
import { resolveAgentPsdTemplateTextStyles } from "@/lib/agents/psd-template-style-resolver";
|
||||
@ -53,7 +58,8 @@ export async function GET(request: NextRequest) {
|
||||
requestedModeRaw === "main" || requestedModeRaw === "sku"
|
||||
? normalizeAgentPsdTemplateMode(requestedModeRaw)
|
||||
: null;
|
||||
const [allTemplates, textColorSetting, secondaryCategorySetting] = await Promise.all([
|
||||
const [allTemplates, textColorSetting, secondaryCategorySetting, categoryTagSetting] =
|
||||
await Promise.all([
|
||||
readAgentPsdTemplates(),
|
||||
db
|
||||
.select()
|
||||
@ -69,6 +75,13 @@ export async function GET(request: NextRequest) {
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
.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<
|
||||
string,
|
||||
@ -147,18 +160,25 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
);
|
||||
|
||||
const normalizedTextColorConfig = textColorSetting
|
||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||
: defaultAgentPsdTextColorConfig;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
templates,
|
||||
textColorConfig: resolveAgentPsdTextColorConfigForBrand(
|
||||
textColorSetting
|
||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
requestedBrand
|
||||
),
|
||||
textColorConfig: requestedBrand
|
||||
? resolveAgentPsdTextColorConfigForBrand(
|
||||
normalizedTextColorConfig,
|
||||
requestedBrand
|
||||
)
|
||||
: normalizedTextColorConfig,
|
||||
secondaryCategoryConfig: secondaryCategorySetting
|
||||
? normalizeAgentPsdTemplateSecondaryCategoryConfig(secondaryCategorySetting.value)
|
||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
categoryTagConfig: categoryTagSetting
|
||||
? normalizeAgentPsdTemplateCategoryTagConfig(categoryTagSetting.value)
|
||||
: defaultAgentPsdTemplateCategoryTagConfig,
|
||||
} satisfies AgentPsdTemplateListResponse);
|
||||
} catch (error) {
|
||||
console.error("[PSD Templates] list failed:", error);
|
||||
|
||||
@ -67,6 +67,11 @@ import {
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} 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 {
|
||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
defaultAgentPresetLibraryConfig,
|
||||
@ -278,6 +283,7 @@ export async function GET() {
|
||||
agentWorkflowShowcaseConfigSetting,
|
||||
agentPsdTextColorConfigSetting,
|
||||
agentPsdTemplateSecondaryCategoryConfigSetting,
|
||||
agentPsdTemplateCategoryTagConfigSetting,
|
||||
] = await Promise.all([
|
||||
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
||||
db
|
||||
@ -442,6 +448,12 @@ export async function GET() {
|
||||
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.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: "" };
|
||||
@ -515,6 +527,10 @@ export async function GET() {
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
agentPsdTemplateSecondaryCategoryConfigSetting?.value
|
||||
);
|
||||
const agentPsdTemplateCategoryTagConfig =
|
||||
normalizeAgentPsdTemplateCategoryTagConfig(
|
||||
agentPsdTemplateCategoryTagConfigSetting?.value
|
||||
);
|
||||
|
||||
// 如果没有设置记录,返回默认值
|
||||
const baseSettings = settings || {
|
||||
@ -553,6 +569,8 @@ export async function GET() {
|
||||
agentPsdTextColorConfig: defaultAgentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig:
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
agentPsdTemplateCategoryTagConfig:
|
||||
defaultAgentPsdTemplateCategoryTagConfig,
|
||||
realtimeCanvasEnabled: true,
|
||||
realtimeCanvasServiceEnabled: true,
|
||||
storageFailoverConfig,
|
||||
@ -605,6 +623,7 @@ export async function GET() {
|
||||
agentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig,
|
||||
agentPsdTemplateCategoryTagConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -926,6 +945,11 @@ const updateSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
agentPsdTemplateCategoryTagConfig: z
|
||||
.object({
|
||||
tags: z.array(z.string().trim().min(1).max(64)).max(200).optional(),
|
||||
})
|
||||
.optional(),
|
||||
realtimeCanvasEnabled: z.boolean().optional(),
|
||||
aiDetailGenerationEnabled: z.boolean().optional(),
|
||||
canvasDefaultModelId: z.string().nullable().optional(),
|
||||
@ -1015,6 +1039,7 @@ export async function PUT(request: NextRequest) {
|
||||
"agentWorkflowShowcaseConfig",
|
||||
"agentPsdTextColorConfig",
|
||||
"agentPsdTemplateSecondaryCategoryConfig",
|
||||
"agentPsdTemplateCategoryTagConfig",
|
||||
]);
|
||||
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
|
||||
if (hasRestrictedKeys) {
|
||||
@ -1040,6 +1065,7 @@ export async function PUT(request: NextRequest) {
|
||||
agentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig,
|
||||
agentPsdTemplateCategoryTagConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
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) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -1682,6 +1752,10 @@ export async function PUT(request: NextRequest) {
|
||||
agentPsdTemplateSecondaryCategoryConfig
|
||||
)
|
||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
agentPsdTemplateCategoryTagConfig:
|
||||
agentPsdTemplateCategoryTagConfig !== undefined
|
||||
? normalizeAgentPsdTemplateCategoryTagConfig(agentPsdTemplateCategoryTagConfig)
|
||||
: defaultAgentPsdTemplateCategoryTagConfig,
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? 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) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -2282,6 +2378,16 @@ export async function PUT(request: NextRequest) {
|
||||
agentPsdTextColorConfig !== undefined
|
||||
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig:
|
||||
agentPsdTemplateSecondaryCategoryConfig !== undefined
|
||||
? normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
agentPsdTemplateSecondaryCategoryConfig
|
||||
)
|
||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
agentPsdTemplateCategoryTagConfig:
|
||||
agentPsdTemplateCategoryTagConfig !== undefined
|
||||
? normalizeAgentPsdTemplateCategoryTagConfig(agentPsdTemplateCategoryTagConfig)
|
||||
: defaultAgentPsdTemplateCategoryTagConfig,
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
|
||||
@ -5989,7 +5989,14 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
) : null}
|
||||
|
||||
<ClientErrorBoundary
|
||||
resetKeys={[psdTemplateDialogSourceUrl || "", workflowContext?.brand || "", psdTemplateDialogMode]}
|
||||
resetKeys={[
|
||||
psdTemplateDialogSourceUrl || "",
|
||||
workflowContext?.brand || "",
|
||||
workflowContext?.thirdCategory || "",
|
||||
workflowContext?.fourthCategory || "",
|
||||
workflowContext?.fifthCategory || "",
|
||||
psdTemplateDialogMode,
|
||||
]}
|
||||
fallback={
|
||||
<Dialog
|
||||
open={Boolean(psdTemplateDialogSourceUrl)}
|
||||
@ -6028,6 +6035,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}}
|
||||
sourceImageUrl={psdTemplateDialogSourceUrl}
|
||||
brandName={workflowContext?.brand || null}
|
||||
selectedCategoryNames={[
|
||||
workflowContext?.fifthCategory || "",
|
||||
workflowContext?.fourthCategory || "",
|
||||
workflowContext?.thirdCategory || "",
|
||||
].filter(Boolean)}
|
||||
sizeInfoText={workflowContext?.summaryText || null}
|
||||
templateMode={psdTemplateDialogMode}
|
||||
/>
|
||||
|
||||
@ -62,9 +62,16 @@ import {
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} 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 {
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
resolveAgentPsdTextColorConfigForBrand,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
normalizeHexColor,
|
||||
@ -90,6 +97,7 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
templates: [],
|
||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
categoryTagConfig: defaultAgentPsdTemplateCategoryTagConfig,
|
||||
error:
|
||||
typeof payload?.error === "string"
|
||||
? payload.error
|
||||
@ -106,6 +114,9 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
secondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
payload.secondaryCategoryConfig
|
||||
),
|
||||
categoryTagConfig: normalizeAgentPsdTemplateCategoryTagConfig(
|
||||
payload.categoryTagConfig
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
@ -113,6 +124,7 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
templates: [],
|
||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
categoryTagConfig: defaultAgentPsdTemplateCategoryTagConfig,
|
||||
error: "PSD 模板加载失败",
|
||||
};
|
||||
}
|
||||
@ -123,12 +135,86 @@ const PREVIEW_STAGE_TARGET_WIDTH = 760;
|
||||
const PREVIEW_STAGE_TARGET_HEIGHT = 760;
|
||||
const MIN_IMAGE_SCALE = 0.4;
|
||||
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(
|
||||
template: AgentPsdTemplate,
|
||||
showSecondaryCategory: boolean
|
||||
showSecondaryCategory: boolean,
|
||||
showBrand = false
|
||||
) {
|
||||
const metaParts = [
|
||||
showBrand
|
||||
? normalizeManagedBrandName(template.brandName) || UNASSIGNED_BRAND_LABEL
|
||||
: "",
|
||||
`${template.width} x ${template.height}`,
|
||||
showSecondaryCategory
|
||||
? String(template.secondaryCategory || "").trim()
|
||||
@ -141,12 +227,14 @@ function renderTemplateSelectContent(
|
||||
template: AgentPsdTemplate,
|
||||
params?: {
|
||||
showSecondaryCategory?: boolean;
|
||||
showBrand?: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
) {
|
||||
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
||||
const showBrand = params?.showBrand === true;
|
||||
const compact = params?.compact === true;
|
||||
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory);
|
||||
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory, showBrand);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -199,12 +287,14 @@ function renderTemplatePickerCardContent(
|
||||
template: AgentPsdTemplate,
|
||||
params?: {
|
||||
showSecondaryCategory?: boolean;
|
||||
showBrand?: boolean;
|
||||
emphasize?: boolean;
|
||||
}
|
||||
) {
|
||||
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
||||
const showBrand = params?.showBrand === true;
|
||||
const emphasize = params?.emphasize === true;
|
||||
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory);
|
||||
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory, showBrand);
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
@ -564,6 +654,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
onOpenChange,
|
||||
sourceImageUrl,
|
||||
brandName,
|
||||
selectedCategoryNames,
|
||||
sizeInfoText,
|
||||
templateMode,
|
||||
}: {
|
||||
@ -571,30 +662,43 @@ export function AgentPsdTemplateEditorDialog({
|
||||
onOpenChange: (open: boolean) => void;
|
||||
sourceImageUrl: string | null;
|
||||
brandName?: string | null;
|
||||
selectedCategoryNames?: string[] | null;
|
||||
sizeInfoText?: string | null;
|
||||
templateMode?: AgentPsdTemplateMode | null;
|
||||
}) {
|
||||
const normalizedBrandName = String(brandName || "").trim();
|
||||
const normalizedSelectedCategoryNames = useMemo(
|
||||
() =>
|
||||
normalizeAgentPsdTemplateCategoryTags(selectedCategoryNames).filter(
|
||||
(tag) => tag !== AGENT_PSD_TEMPLATE_ALL_CATEGORY_TAG
|
||||
),
|
||||
[selectedCategoryNames]
|
||||
);
|
||||
const normalizedTemplateMode =
|
||||
templateMode === "sku" ? "sku" : templateMode === "main" ? "main" : "";
|
||||
const templateListRequestUrl = useMemo(() => {
|
||||
if (!open || !normalizedBrandName) {
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set("brand", normalizedBrandName);
|
||||
return `/api/psd-templates?${searchParams.toString()}`;
|
||||
if (normalizedBrandName) {
|
||||
searchParams.set("brand", normalizedBrandName);
|
||||
}
|
||||
|
||||
return searchParams.size > 0
|
||||
? `/api/psd-templates?${searchParams.toString()}`
|
||||
: "/api/psd-templates";
|
||||
}, [open, normalizedBrandName]);
|
||||
const { data, isLoading } = useSWR<AgentPsdTemplateListResponse>(
|
||||
templateListRequestUrl,
|
||||
fetcher
|
||||
);
|
||||
const templates = useMemo(
|
||||
const allTemplates = useMemo(
|
||||
() => (data?.success ? data.templates : []),
|
||||
[data]
|
||||
);
|
||||
const textColorConfig = useMemo(
|
||||
const rawTextColorConfig = useMemo(
|
||||
() => normalizeAgentPsdTextColorConfig(data?.textColorConfig),
|
||||
[data?.textColorConfig]
|
||||
);
|
||||
@ -605,6 +709,25 @@ export function AgentPsdTemplateEditorDialog({
|
||||
),
|
||||
[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 [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
||||
@ -647,6 +770,28 @@ export function AgentPsdTemplateEditorDialog({
|
||||
templates.find((template) => template.id === selectedTemplateId) || null,
|
||||
[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(
|
||||
() =>
|
||||
secondaryCategoryConfig.enabled &&
|
||||
@ -1428,9 +1573,13 @@ export function AgentPsdTemplateEditorDialog({
|
||||
nextMode === "sku" ? skuModeTemplates : mainModeTemplates;
|
||||
if (nextTemplates.length === 0) {
|
||||
toast.error(
|
||||
nextMode === "sku"
|
||||
? "当前品牌下暂无 SKU 图模式模板"
|
||||
: "当前品牌下暂无主图模式模板"
|
||||
selectedCategorySummaryText
|
||||
? nextMode === "sku"
|
||||
? `当前品类“${selectedCategorySummaryText}”下暂无 SKU 图模式模板`
|
||||
: `当前品类“${selectedCategorySummaryText}”下暂无主图模式模板`
|
||||
: nextMode === "sku"
|
||||
? "当前品牌下暂无 SKU 图模式模板"
|
||||
: "当前品牌下暂无主图模式模板"
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -1475,6 +1624,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<Label>
|
||||
{selectedTemplate ? "选择模板" : "主图模式模板"}
|
||||
</Label>
|
||||
{selectedCategorySummaryText ? (
|
||||
<div className="text-white/45 text-xs">
|
||||
当前按品类“{selectedCategorySummaryText}”筛选模板,同时保留“适合所有”的模板。
|
||||
</div>
|
||||
) : null}
|
||||
{selectedTemplate && hasTemplateOptions ? (
|
||||
<div className="space-y-2.5">
|
||||
<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, {
|
||||
showSecondaryCategory:
|
||||
shouldGroupTemplatesBySecondaryCategory,
|
||||
showBrand: shouldShowTemplateBrand,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
@ -1550,6 +1705,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
{renderTemplateSelectContent(template, {
|
||||
showSecondaryCategory:
|
||||
shouldGroupTemplatesBySecondaryCategory,
|
||||
showBrand: shouldShowTemplateBrand,
|
||||
compact: true,
|
||||
})}
|
||||
</SelectItem>
|
||||
@ -1563,6 +1719,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
value={template.id}
|
||||
>
|
||||
{renderTemplateSelectContent(template, {
|
||||
showBrand: shouldShowTemplateBrand,
|
||||
compact: true,
|
||||
})}
|
||||
</SelectItem>
|
||||
@ -1598,15 +1755,18 @@ export function AgentPsdTemplateEditorDialog({
|
||||
{renderTemplatePickerCardContent(template, {
|
||||
showSecondaryCategory:
|
||||
shouldGroupTemplatesBySecondaryCategory,
|
||||
showBrand: shouldShowTemplateBrand,
|
||||
emphasize: true,
|
||||
})}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="rounded-xl border border-white/15 border-dashed bg-white/[0.02] px-3 py-3 text-muted-foreground text-sm">
|
||||
{normalizedBrandName
|
||||
? `当前品牌“${normalizedBrandName}”下暂无主图模式模板`
|
||||
: "请先在工作流中选择品牌后再打开 PSD 模板。"}
|
||||
{selectedCategorySummaryText
|
||||
? `当前品类“${selectedCategorySummaryText}”下暂无主图模式模板`
|
||||
: normalizedBrandName
|
||||
? `当前品牌“${normalizedBrandName}”下暂无主图模式模板`
|
||||
: "当前暂无可用的主图模式模板,请先去后台配置。"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -1650,6 +1810,10 @@ export function AgentPsdTemplateEditorDialog({
|
||||
? "正在加载可用模板..."
|
||||
: hasTemplateOptions
|
||||
? "请从左侧主图模式模板或右侧 SKU 图模式模板中选择一个模板开始编辑。"
|
||||
: selectedCategorySummaryText && normalizedBrandName
|
||||
? `当前品牌“${normalizedBrandName}”下没有命中品类“${selectedCategorySummaryText}”的 PSD 模板,请先去后台配置。`
|
||||
: selectedCategorySummaryText
|
||||
? `当前品类“${selectedCategorySummaryText}”下还没有可用 PSD 模板,请先去后台配置。`
|
||||
: normalizedBrandName
|
||||
? `当前品牌“${normalizedBrandName}”下还没有可用 PSD 模板,请先去后台配置。`
|
||||
: "当前还没有启用的 PSD 模板,请先去后台配置。"}
|
||||
@ -2071,7 +2235,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<div className="mt-1 text-white/45 text-xs">
|
||||
{normalizedTemplateMode === "sku"
|
||||
? "当前结果属于 SKU 图模式,可直接从这里选择并进入编辑。"
|
||||
: "这里集中展示当前品牌下的 SKU 图模式模板。"}
|
||||
: selectedCategorySummaryText
|
||||
? `当前按品类“${selectedCategorySummaryText}”筛选,同时展示“适合所有”的 SKU 图模式模板。`
|
||||
: normalizedBrandName
|
||||
? "这里集中展示当前品牌下的 SKU 图模式模板。"
|
||||
: "当前未选择品牌,这里展示全部品牌下的 SKU 图模式模板。"}
|
||||
</div>
|
||||
</div>
|
||||
<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, {
|
||||
showSecondaryCategory:
|
||||
shouldGroupTemplatesBySecondaryCategory,
|
||||
showBrand: shouldShowTemplateBrand,
|
||||
emphasize: true,
|
||||
})}
|
||||
</button>
|
||||
@ -2113,9 +2282,11 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center px-6 text-center text-muted-foreground text-sm">
|
||||
{normalizedBrandName
|
||||
? `当前品牌“${normalizedBrandName}”下暂无 SKU 图模式模板`
|
||||
: "请先返回工作流区域选择品牌,再打开 PSD 模板。"}
|
||||
{selectedCategorySummaryText
|
||||
? `当前品类“${selectedCategorySummaryText}”下暂无 SKU 图模式模板`
|
||||
: normalizedBrandName
|
||||
? `当前品牌“${normalizedBrandName}”下暂无 SKU 图模式模板`
|
||||
: "当前暂无可用的 SKU 图模式模板,请先去后台配置。"}
|
||||
</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 { 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_TEMPLATE_ASSET_VERSION = 6;
|
||||
@ -86,6 +88,7 @@ export interface AgentPsdTemplate {
|
||||
brandName: string;
|
||||
mode: AgentPsdTemplateMode;
|
||||
secondaryCategory?: string;
|
||||
categoryTags?: string[];
|
||||
description?: string;
|
||||
status: AgentPsdTemplateStatus;
|
||||
parseStatus: AgentPsdTemplateParseStatus;
|
||||
@ -114,6 +117,7 @@ export interface AgentPsdTemplateListResponse {
|
||||
templates: AgentPsdTemplate[];
|
||||
textColorConfig?: AgentPsdTextColorConfig;
|
||||
secondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
|
||||
categoryTagConfig?: AgentPsdTemplateCategoryTagConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -444,12 +448,17 @@ export function parseAgentPsdTemplates(value: unknown) {
|
||||
const parseStatus = normalizeAgentPsdTemplateParseStatus(record.parseStatus);
|
||||
const rawParseProgress = normalizePositiveInteger(record.parseProgress);
|
||||
|
||||
const categoryTags = normalizeAgentPsdTemplateCategoryTags(
|
||||
record.categoryTags ?? record.productCategoryTags
|
||||
);
|
||||
|
||||
templates.push({
|
||||
id,
|
||||
name,
|
||||
brandName,
|
||||
mode: normalizeAgentPsdTemplateMode(record.mode),
|
||||
secondaryCategory: normalizeText(record.secondaryCategory) || undefined,
|
||||
categoryTags: categoryTags.length > 0 ? categoryTags : undefined,
|
||||
description: normalizeText(record.description) || undefined,
|
||||
status: normalizeAgentPsdTemplateStatus(record.status),
|
||||
parseStatus,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user