feat: enhance psd template editor workflow
This commit is contained in:
parent
e7cc0c9555
commit
305dd21dd5
@ -67,6 +67,11 @@ import {
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
type AgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||
import { normalizeHexColor } from "@/lib/agents/psd-text-layout";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -474,6 +479,7 @@ export function AgentWorkflowSettingsClient({
|
||||
agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig;
|
||||
agentWorkflowShowcaseConfig?: AgentWorkflowShowcaseConfig;
|
||||
agentPsdTextColorConfig?: AgentPsdTextColorConfig;
|
||||
agentPsdTemplateSecondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
|
||||
}>("/api/site-settings", fetcher);
|
||||
const [presetLibraryConfig, setPresetLibraryConfig] = useState<AgentPresetLibraryConfig>(
|
||||
defaultAgentPresetLibraryConfig
|
||||
@ -491,6 +497,10 @@ export function AgentWorkflowSettingsClient({
|
||||
const [psdTextColorConfig, setPsdTextColorConfig] = useState<AgentPsdTextColorConfig>(
|
||||
defaultAgentPsdTextColorConfig
|
||||
);
|
||||
const [psdTemplateSecondaryCategoryConfig, setPsdTemplateSecondaryCategoryConfig] =
|
||||
useState<AgentPsdTemplateSecondaryCategoryConfig>(
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig
|
||||
);
|
||||
const [brandModelConfig, setBrandModelConfig] = useState<AgentWorkflowBrandModelConfig>(
|
||||
defaultAgentWorkflowBrandModelConfig
|
||||
);
|
||||
@ -536,6 +546,11 @@ export function AgentWorkflowSettingsClient({
|
||||
setPsdTextColorConfig(
|
||||
normalizeAgentPsdTextColorConfig(data.agentPsdTextColorConfig)
|
||||
);
|
||||
setPsdTemplateSecondaryCategoryConfig(
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
data.agentPsdTemplateSecondaryCategoryConfig
|
||||
)
|
||||
);
|
||||
setBrandModelConfig(
|
||||
normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig)
|
||||
);
|
||||
@ -1062,6 +1077,7 @@ export function AgentWorkflowSettingsClient({
|
||||
agentWorkflowBrandModelConfig: brandModelConfig,
|
||||
agentWorkflowShowcaseConfig: showcaseConfig,
|
||||
agentPsdTextColorConfig: psdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig: psdTemplateSecondaryCategoryConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -2646,6 +2662,38 @@ export function AgentWorkflowSettingsClient({
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants} className="lg:col-span-2">
|
||||
<Card className="mb-6">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-500/10">
|
||||
<Layers className="h-4 w-4 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base">PSD 模板二级分类</CardTitle>
|
||||
<CardDescription>
|
||||
控制前台“选择模板”下拉是否按二级分类分组展示。
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">启用 PSD 模板二级分类</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
开启后,用户前台会按模板配置里的二级分类分组显示;未填写分类的模板会归入“未分组”。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={psdTemplateSecondaryCategoryConfig.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setPsdTemplateSecondaryCategoryConfig({ enabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@ -163,6 +163,7 @@ export function PsdTemplatesSettingsClient({
|
||||
const [layerKeyword, setLayerKeyword] = useState("");
|
||||
const [uploadName, setUploadName] = useState("");
|
||||
const [uploadBrandName, setUploadBrandName] = useState("");
|
||||
const [uploadSecondaryCategory, setUploadSecondaryCategory] = useState("");
|
||||
const [uploadDescription, setUploadDescription] = useState("");
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
const [fontUploadName, setFontUploadName] = useState("");
|
||||
@ -305,6 +306,7 @@ export function PsdTemplatesSettingsClient({
|
||||
const formData = new FormData();
|
||||
formData.set("name", uploadName.trim() || uploadFile.name.replace(/\.[^.]+$/, ""));
|
||||
formData.set("brandName", uploadBrandName.trim());
|
||||
formData.set("secondaryCategory", uploadSecondaryCategory.trim());
|
||||
formData.set("description", uploadDescription.trim());
|
||||
formData.set("status", "disabled");
|
||||
formData.set("file", uploadFile);
|
||||
@ -370,6 +372,7 @@ export function PsdTemplatesSettingsClient({
|
||||
);
|
||||
setUploadName("");
|
||||
setUploadBrandName("");
|
||||
setUploadSecondaryCategory("");
|
||||
setUploadDescription("");
|
||||
setUploadFile(null);
|
||||
await mutate();
|
||||
@ -472,6 +475,7 @@ export function PsdTemplatesSettingsClient({
|
||||
body: JSON.stringify({
|
||||
name: draftTemplate.name,
|
||||
brandName: draftTemplate.brandName,
|
||||
secondaryCategory: draftTemplate.secondaryCategory || "",
|
||||
description: draftTemplate.description || "",
|
||||
status: draftTemplate.status,
|
||||
layers: draftTemplate.layers,
|
||||
@ -731,6 +735,18 @@ export function PsdTemplatesSettingsClient({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="psdTemplateSecondaryCategory">二级分类</Label>
|
||||
<Input
|
||||
id="psdTemplateSecondaryCategory"
|
||||
value={uploadSecondaryCategory}
|
||||
onChange={(event) => setUploadSecondaryCategory(event.target.value)}
|
||||
placeholder="例如:凳子类 / 升降桌类"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
开启前台模板二级分类后,会按这里的分类分组展示模板。
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="psdTemplateDescription">模板说明</Label>
|
||||
<Textarea
|
||||
@ -924,6 +940,11 @@ export function PsdTemplatesSettingsClient({
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
品牌:{template.brandName}
|
||||
</div>
|
||||
{template.secondaryCategory ? (
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
二级分类:{template.secondaryCategory}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={template.status === "active" ? "default" : "outline"}>
|
||||
@ -1153,6 +1174,21 @@ export function PsdTemplatesSettingsClient({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="draftTemplateSecondaryCategory">二级分类</Label>
|
||||
<Input
|
||||
id="draftTemplateSecondaryCategory"
|
||||
value={draftTemplate.secondaryCategory || ""}
|
||||
onChange={(event) =>
|
||||
setDraftTemplate((current) =>
|
||||
current
|
||||
? { ...current, secondaryCategory: event.target.value }
|
||||
: current
|
||||
)
|
||||
}
|
||||
placeholder="例如:凳子类 / 升降桌类"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
@ -1204,6 +1240,9 @@ export function PsdTemplatesSettingsClient({
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Badge variant="secondary">品牌:{draftTemplate.brandName}</Badge>
|
||||
{draftTemplate.secondaryCategory ? (
|
||||
<Badge variant="secondary">分类:{draftTemplate.secondaryCategory}</Badge>
|
||||
) : null}
|
||||
<Badge variant="outline">文件:{draftTemplate.fileName}</Badge>
|
||||
<Badge variant={getTemplateParseBadgeVariant(draftTemplate)}>
|
||||
{getTemplateParseLabel(draftTemplate)}
|
||||
|
||||
@ -28,6 +28,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),
|
||||
secondaryCategory: z.string().trim().max(64).optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
status: z.string().optional(),
|
||||
layers: z.array(z.any()).default([]),
|
||||
@ -118,6 +119,7 @@ export async function PUT(
|
||||
...currentTemplate,
|
||||
name: body.name.trim(),
|
||||
brandName: body.brandName.trim(),
|
||||
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
||||
description: String(body.description || "").trim() || undefined,
|
||||
status: nextStatus,
|
||||
updatedAt: new Date().toISOString(),
|
||||
@ -206,6 +208,7 @@ export async function PUT(
|
||||
...currentTemplate,
|
||||
name: body.name.trim(),
|
||||
brandName: body.brandName.trim(),
|
||||
secondaryCategory: String(body.secondaryCategory || "").trim() || undefined,
|
||||
description: String(body.description || "").trim() || undefined,
|
||||
status: nextStatus,
|
||||
renderBaseUrl: uploadedRenderBase.url,
|
||||
|
||||
@ -112,6 +112,7 @@ export async function POST(request: NextRequest) {
|
||||
String(formData.get("name") || "").trim() ||
|
||||
(file instanceof File ? getDefaultTemplateName(file.name) : "");
|
||||
const brandName = String(formData.get("brandName") || "").trim();
|
||||
const secondaryCategory = String(formData.get("secondaryCategory") || "").trim();
|
||||
const description = String(formData.get("description") || "").trim();
|
||||
const status = normalizeAgentPsdTemplateStatus(formData.get("status"));
|
||||
|
||||
@ -131,6 +132,10 @@ export async function POST(request: NextRequest) {
|
||||
return jsonMutationError(400, "模板品牌不能超过 64 个字符");
|
||||
}
|
||||
|
||||
if (secondaryCategory.length > 64) {
|
||||
return jsonMutationError(400, "模板二级分类不能超过 64 个字符");
|
||||
}
|
||||
|
||||
if (!(file instanceof File)) {
|
||||
return jsonMutationError(400, "缺少 PSD 模板文件");
|
||||
}
|
||||
@ -164,6 +169,7 @@ export async function POST(request: NextRequest) {
|
||||
id: templateId,
|
||||
name,
|
||||
brandName,
|
||||
secondaryCategory: secondaryCategory || undefined,
|
||||
description: description || undefined,
|
||||
status,
|
||||
parseStatus: "pending",
|
||||
|
||||
@ -16,6 +16,11 @@ import {
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} from "@/lib/agents/psd-template-secondary-category-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";
|
||||
@ -41,7 +46,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const requestedBrand = String(request.nextUrl.searchParams.get("brand") || "").trim();
|
||||
const [allTemplates, textColorSetting] = await Promise.all([
|
||||
const [allTemplates, textColorSetting, secondaryCategorySetting] = await Promise.all([
|
||||
readAgentPsdTemplates(),
|
||||
db
|
||||
.select()
|
||||
@ -50,6 +55,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_SECONDARY_CATEGORY_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
.catch(() => null),
|
||||
]);
|
||||
const repairedTemplateMap = new Map<
|
||||
string,
|
||||
@ -132,6 +144,9 @@ export async function GET(request: NextRequest) {
|
||||
textColorConfig: textColorSetting
|
||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
secondaryCategoryConfig: secondaryCategorySetting
|
||||
? normalizeAgentPsdTemplateSecondaryCategoryConfig(secondaryCategorySetting.value)
|
||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} satisfies AgentPsdTemplateListResponse);
|
||||
} catch (error) {
|
||||
console.error("[PSD Templates] list failed:", error);
|
||||
|
||||
@ -62,6 +62,11 @@ import {
|
||||
defaultAgentPsdTextColorConfig,
|
||||
normalizeAgentPsdTextColorConfig,
|
||||
} from "@/lib/agents/psd-text-color-config";
|
||||
import {
|
||||
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||
import {
|
||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
defaultAgentPresetLibraryConfig,
|
||||
@ -272,6 +277,7 @@ export async function GET() {
|
||||
agentWorkflowBrandModelConfigSetting,
|
||||
agentWorkflowShowcaseConfigSetting,
|
||||
agentPsdTextColorConfigSetting,
|
||||
agentPsdTemplateSecondaryCategoryConfigSetting,
|
||||
] = await Promise.all([
|
||||
db.select().from(siteSettings).limit(1).then(rows => rows[0]),
|
||||
db
|
||||
@ -430,6 +436,12 @@ export async function GET() {
|
||||
.where(eq(systemSettings.key, AGENT_PSD_TEXT_COLOR_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
]);
|
||||
|
||||
const notification = notificationSetting?.value || { enabled: false, title: "", content: "" };
|
||||
@ -499,6 +511,10 @@ export async function GET() {
|
||||
const agentPsdTextColorConfig = normalizeAgentPsdTextColorConfig(
|
||||
agentPsdTextColorConfigSetting?.value
|
||||
);
|
||||
const agentPsdTemplateSecondaryCategoryConfig =
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
agentPsdTemplateSecondaryCategoryConfigSetting?.value
|
||||
);
|
||||
|
||||
// 如果没有设置记录,返回默认值
|
||||
const baseSettings = settings || {
|
||||
@ -535,6 +551,8 @@ export async function GET() {
|
||||
agentWorkflowBrandModelConfig: defaultAgentWorkflowBrandModelConfig,
|
||||
agentWorkflowShowcaseConfig: defaultAgentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig: defaultAgentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig:
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
realtimeCanvasEnabled: true,
|
||||
realtimeCanvasServiceEnabled: true,
|
||||
storageFailoverConfig,
|
||||
@ -586,6 +604,7 @@ export async function GET() {
|
||||
agentWorkflowBrandModelConfig,
|
||||
agentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -813,6 +832,11 @@ const updateSchema = z.object({
|
||||
colors: z.array(z.string().trim().max(32)).length(2).optional(),
|
||||
})
|
||||
.optional(),
|
||||
agentPsdTemplateSecondaryCategoryConfig: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
realtimeCanvasEnabled: z.boolean().optional(),
|
||||
aiDetailGenerationEnabled: z.boolean().optional(),
|
||||
canvasDefaultModelId: z.string().nullable().optional(),
|
||||
@ -901,6 +925,7 @@ export async function PUT(request: NextRequest) {
|
||||
"agentWorkflowBrandModelConfig",
|
||||
"agentWorkflowShowcaseConfig",
|
||||
"agentPsdTextColorConfig",
|
||||
"agentPsdTemplateSecondaryCategoryConfig",
|
||||
]);
|
||||
const hasRestrictedKeys = Object.keys(body || {}).some((key) => !allowedKeys.has(key));
|
||||
if (hasRestrictedKeys) {
|
||||
@ -925,6 +950,7 @@ export async function PUT(request: NextRequest) {
|
||||
agentWorkflowBrandModelConfig,
|
||||
agentWorkflowShowcaseConfig,
|
||||
agentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig,
|
||||
membershipConfig,
|
||||
membershipUserOverrides,
|
||||
pointsSystemConfig,
|
||||
@ -1337,6 +1363,28 @@ 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 (membershipConfig !== undefined) {
|
||||
const normalized = normalizeMembershipConfig(membershipConfig);
|
||||
await db
|
||||
@ -1539,6 +1587,12 @@ export async function PUT(request: NextRequest) {
|
||||
agentPsdTextColorConfig !== undefined
|
||||
? normalizeAgentPsdTextColorConfig(agentPsdTextColorConfig)
|
||||
: defaultAgentPsdTextColorConfig,
|
||||
agentPsdTemplateSecondaryCategoryConfig:
|
||||
agentPsdTemplateSecondaryCategoryConfig !== undefined
|
||||
? normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
agentPsdTemplateSecondaryCategoryConfig
|
||||
)
|
||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
membershipConfig:
|
||||
membershipConfig !== undefined
|
||||
? normalizeMembershipConfig(membershipConfig)
|
||||
|
||||
@ -29,7 +29,15 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
@ -54,6 +62,10 @@ import {
|
||||
type AgentPsdTemplateRenderResponse,
|
||||
type AgentPsdTemplateTextLayerSlot,
|
||||
} from "@/lib/agents/psd-template-config";
|
||||
import {
|
||||
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||
import { normalizeHexColor, resolveTextLayout } from "@/lib/agents/psd-text-layout";
|
||||
|
||||
const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
@ -68,6 +80,7 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
success: false,
|
||||
templates: [],
|
||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
error: typeof payload?.error === "string" ? payload.error : "PSD 模板加载失败",
|
||||
};
|
||||
}
|
||||
@ -76,12 +89,16 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||
success: true,
|
||||
templates: parseAgentPsdTemplates(payload.templates),
|
||||
textColorConfig: normalizeAgentPsdTextColorConfig(payload.textColorConfig),
|
||||
secondaryCategoryConfig: normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
payload.secondaryCategoryConfig
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
templates: [],
|
||||
textColorConfig: defaultAgentPsdTextColorConfig,
|
||||
secondaryCategoryConfig: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||
error: "PSD 模板加载失败",
|
||||
};
|
||||
}
|
||||
@ -342,7 +359,6 @@ function getPlacedAssignedImageStyle(params: {
|
||||
top: frame.top,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
...getAssignedImageMaskStyle(layer),
|
||||
};
|
||||
}
|
||||
|
||||
@ -410,6 +426,10 @@ export function AgentPsdTemplateEditorDialog({
|
||||
() => normalizeAgentPsdTextColorConfig(data?.textColorConfig),
|
||||
[data?.textColorConfig]
|
||||
);
|
||||
const secondaryCategoryConfig = useMemo(
|
||||
() => normalizeAgentPsdTemplateSecondaryCategoryConfig(data?.secondaryCategoryConfig),
|
||||
[data?.secondaryCategoryConfig]
|
||||
);
|
||||
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
||||
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
||||
@ -438,6 +458,30 @@ export function AgentPsdTemplateEditorDialog({
|
||||
() => templates.find((template) => template.id === selectedTemplateId) || null,
|
||||
[selectedTemplateId, templates]
|
||||
);
|
||||
const shouldGroupTemplatesBySecondaryCategory = useMemo(
|
||||
() =>
|
||||
secondaryCategoryConfig.enabled &&
|
||||
templates.some((template) => Boolean(String(template.secondaryCategory || "").trim())),
|
||||
[secondaryCategoryConfig.enabled, templates]
|
||||
);
|
||||
const groupedTemplates = useMemo(() => {
|
||||
if (!shouldGroupTemplatesBySecondaryCategory) {
|
||||
return [] as Array<{ label: string; templates: AgentPsdTemplate[] }>;
|
||||
}
|
||||
|
||||
const groups = new Map<string, AgentPsdTemplate[]>();
|
||||
for (const template of templates) {
|
||||
const groupLabel = String(template.secondaryCategory || "").trim() || "未分组";
|
||||
const currentGroup = groups.get(groupLabel) || [];
|
||||
currentGroup.push(template);
|
||||
groups.set(groupLabel, currentGroup);
|
||||
}
|
||||
|
||||
return Array.from(groups.entries()).map(([label, grouped]) => ({
|
||||
label,
|
||||
templates: grouped,
|
||||
}));
|
||||
}, [shouldGroupTemplatesBySecondaryCategory, templates]);
|
||||
const selectedTemplateLayers = useMemo(() => {
|
||||
if (!selectedTemplate || !Array.isArray(selectedTemplate.layers)) {
|
||||
return [] as AgentPsdTemplateLayerSlot[];
|
||||
@ -967,9 +1011,9 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
overlayClassName="bg-black/85 backdrop-blur-sm"
|
||||
className="flex h-[92vh] w-[96vw] max-w-[1700px] flex-col overflow-hidden border border-white/10 bg-[#09090b] p-0 text-white shadow-[0_30px_120px_rgba(0,0,0,0.65)]"
|
||||
className="flex h-[96vh] w-[96vw] max-w-[1700px] flex-col overflow-hidden border border-white/10 bg-[#09090b] p-0 text-white shadow-[0_30px_120px_rgba(0,0,0,0.65)]"
|
||||
>
|
||||
<DialogHeader className="border-b border-white/10 bg-[#09090b] px-6 py-5">
|
||||
<DialogHeader className="border-b border-white/10 bg-[#09090b] px-5 py-4">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5 text-orange-500" />
|
||||
套用 PSD 模板
|
||||
@ -989,15 +1033,79 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<Label>选择模板</Label>
|
||||
{hasTemplateOptions ? (
|
||||
<Select value={selectedTemplateSelectValue} onValueChange={setSelectedTemplateId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={isLoading ? "加载模板中..." : "请选择模板"} />
|
||||
<SelectTrigger className="h-[74px] items-center">
|
||||
{selectedTemplate ? (
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex h-12 w-12 shrink-0 overflow-hidden rounded-xl border border-white/10 bg-white/5">
|
||||
<img
|
||||
src={selectedTemplate.previewUrl}
|
||||
alt={selectedTemplate.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 text-left">
|
||||
{shouldGroupTemplatesBySecondaryCategory &&
|
||||
selectedTemplate.secondaryCategory ? (
|
||||
<span className="block truncate text-[11px] text-white/50">
|
||||
{selectedTemplate.secondaryCategory}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="block truncate text-base font-medium">
|
||||
{selectedTemplate.name}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<SelectValue placeholder={isLoading ? "加载模板中..." : "请选择模板"} />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.id}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
{shouldGroupTemplatesBySecondaryCategory
|
||||
? groupedTemplates.map((group) => (
|
||||
<SelectGroup key={`psd-template-group-${group.label}`}>
|
||||
<SelectLabel>{group.label}</SelectLabel>
|
||||
{group.templates.map((template) => (
|
||||
<SelectItem
|
||||
key={template.id}
|
||||
value={template.id}
|
||||
className="py-2.5 pr-3"
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="flex h-12 w-12 shrink-0 overflow-hidden rounded-lg border border-black/10 bg-black/5">
|
||||
<img
|
||||
src={template.previewUrl}
|
||||
alt={template.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-sm font-medium">
|
||||
{template.name}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))
|
||||
: templates.map((template) => (
|
||||
<SelectItem
|
||||
key={template.id}
|
||||
value={template.id}
|
||||
className="py-2.5 pr-3"
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="flex h-12 w-12 shrink-0 overflow-hidden rounded-lg border border-black/10 bg-black/5">
|
||||
<img
|
||||
src={template.previewUrl}
|
||||
alt={template.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-sm font-medium">
|
||||
{template.name}
|
||||
</span>
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
@ -1066,19 +1174,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-col border-b border-white/10 bg-[#09090b] p-5 xl:border-b-0 xl:border-r">
|
||||
<div className="mb-4 flex flex-wrap items-center gap-2">
|
||||
<div className="text-sm font-medium">模板画布编辑区</div>
|
||||
{selectedTemplate ? (
|
||||
<>
|
||||
<Badge variant="outline">{editableImageLayers.length} 个图片替换区域</Badge>
|
||||
<Badge variant="outline">{editableTextLayers.length} 个文字编辑区域</Badge>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-4 text-xs text-muted-foreground">
|
||||
点模板上的橙色框替换图片,点蓝色框直接修改文字。
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={previewViewportRef}
|
||||
className="psd-preview-viewport relative flex min-h-0 flex-1 items-center justify-center overflow-auto rounded-2xl border border-white/10 bg-[#111114] p-4"
|
||||
@ -1123,12 +1218,18 @@ export function AgentPsdTemplateEditorDialog({
|
||||
if (!layerImageUrl) {
|
||||
return null;
|
||||
}
|
||||
const assignedImageContainerStyle = assignedUrl
|
||||
? {
|
||||
...layerBoxStyle,
|
||||
...getAssignedImageMaskStyle(layer),
|
||||
}
|
||||
: layerBoxStyle;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={layer.id}
|
||||
className="absolute overflow-hidden"
|
||||
style={layerBoxStyle}
|
||||
style={assignedImageContainerStyle}
|
||||
>
|
||||
<img
|
||||
src={layerImageUrl}
|
||||
@ -1146,7 +1247,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: getImageFit(layer.replaceMode),
|
||||
...getAssignedImageMaskStyle(layer),
|
||||
}
|
||||
: {
|
||||
width: "100%",
|
||||
@ -1442,8 +1542,8 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 bg-[#09090b] p-5">
|
||||
<div className="mb-4 text-sm font-medium">当前选中图层</div>
|
||||
<div className="flex min-h-0 flex-col bg-[#09090b] p-4 xl:p-5">
|
||||
<div className="mb-3 text-sm font-medium">当前选中图层</div>
|
||||
|
||||
<Input
|
||||
ref={uploadInputRef}
|
||||
@ -1456,11 +1556,10 @@ export function AgentPsdTemplateEditorDialog({
|
||||
}}
|
||||
/>
|
||||
|
||||
<ScrollArea className="h-full pr-3">
|
||||
<div className="flex min-h-full flex-col gap-5">
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.02] p-4">
|
||||
<div className="grid min-h-0 flex-1 gap-4 xl:grid-rows-[minmax(0,1fr)_minmax(0,0.78fr)]">
|
||||
<div className="flex min-h-0 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
|
||||
{selectedImageLayer ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">可替换图片</Badge>
|
||||
<div className="text-sm font-medium">{selectedImageLayer.layerName}</div>
|
||||
@ -1470,7 +1569,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{selectedImageLayer.layerPath}</div>
|
||||
<div className="overflow-hidden rounded-xl border border-white/10 bg-white/5">
|
||||
<div className="h-[220px]">
|
||||
<div className="h-[150px] xl:h-[160px]">
|
||||
{effectiveImageAssignments[selectedImageLayer.id] ? (
|
||||
<img
|
||||
src={effectiveImageAssignments[selectedImageLayer.id]}
|
||||
@ -1496,7 +1595,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
缩放
|
||||
</div>
|
||||
<span className="rounded-full border border-orange-300/35 bg-orange-500/15 px-2 py-0.5 font-medium text-orange-100">
|
||||
<span className="rounded-full border border-emerald-300/35 bg-emerald-500/15 px-2 py-0.5 font-medium text-emerald-100">
|
||||
{Math.round(selectedImagePlacement.scale * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
@ -1518,8 +1617,8 @@ export function AgentPsdTemplateEditorDialog({
|
||||
value={[selectedImagePlacement.scale]}
|
||||
className="py-1"
|
||||
trackClassName="h-2.5 rounded-full border border-white/20 bg-[#0f1014]"
|
||||
rangeClassName="bg-gradient-to-r from-amber-300 via-orange-400 to-rose-400 shadow-[0_0_12px_rgba(251,146,60,0.45)]"
|
||||
thumbClassName="h-5 w-5 border-2 border-orange-100 bg-[#0a0a0d] shadow-[0_0_0_5px_rgba(251,146,60,0.22)]"
|
||||
rangeClassName="bg-gradient-to-r from-emerald-300 via-green-400 to-teal-400 shadow-[0_0_12px_rgba(16,185,129,0.38)]"
|
||||
thumbClassName="h-5 w-5 border-2 border-emerald-100 bg-[#0a0a0d] shadow-[0_0_0_5px_rgba(16,185,129,0.2)]"
|
||||
onValueChange={(value) => {
|
||||
const newScale = clampImageScale(value[0] || 1);
|
||||
// offset 本身就是相对中心位移,缩放时不应按比例放大,否则会导致画面偏移看起来像“变形”。
|
||||
@ -1541,7 +1640,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between px-0.5 text-[10px] text-white/45">
|
||||
<span>40%</span>
|
||||
<span className="text-orange-200">100%</span>
|
||||
<span className="text-emerald-200">100%</span>
|
||||
<span>300%</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -1628,7 +1727,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
) : selectedTextLayer ? (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="default">可编辑文字</Badge>
|
||||
<div className="text-sm font-medium">{selectedTextLayer.layerName}</div>
|
||||
@ -1638,9 +1737,10 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">{selectedTextLayer.layerPath}</div>
|
||||
<Textarea
|
||||
rows={5}
|
||||
rows={4}
|
||||
value={effectiveTextAssignments[selectedTextLayer.id] || ""}
|
||||
placeholder={selectedTextLayer.placeholder || "请输入文字"}
|
||||
className="min-h-[150px] resize-none xl:min-h-[132px]"
|
||||
onChange={(event) =>
|
||||
setTextAssignments((current) => ({
|
||||
...current,
|
||||
@ -1707,7 +1807,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[200px] flex-col items-center justify-center text-center">
|
||||
<div className="flex min-h-[160px] flex-1 flex-col items-center justify-center text-center">
|
||||
<MousePointerClick className="h-8 w-8 text-white/35" />
|
||||
<div className="mt-3 text-sm font-medium">点模板中的图层开始编辑</div>
|
||||
<div className="mt-2 text-xs leading-6 text-muted-foreground">
|
||||
@ -1715,13 +1815,13 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0 flex-1 rounded-2xl border border-white/10 bg-white/[0.02] p-4">
|
||||
<div className="flex min-h-0 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
|
||||
<div className="mb-3 text-sm font-medium">导出结果</div>
|
||||
{renderedResultUrl ? (
|
||||
<div className="flex h-full min-h-0 flex-col justify-between gap-4">
|
||||
<div className="mx-auto w-full max-w-[240px] overflow-hidden rounded-2xl border border-white/10 bg-white/5">
|
||||
<div className="mx-auto w-full max-w-[210px] overflow-hidden rounded-2xl border border-white/10 bg-white/5">
|
||||
<div
|
||||
style={{
|
||||
aspectRatio: selectedTemplate
|
||||
@ -1743,23 +1843,22 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full min-h-[220px] flex-col items-center justify-center rounded-2xl border border-dashed border-white/15 bg-white/[0.02] px-6 text-center">
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-white/5">
|
||||
<div className="flex h-full min-h-[160px] flex-col items-center justify-center rounded-2xl border border-dashed border-white/15 bg-white/[0.02] px-5 text-center">
|
||||
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/5">
|
||||
{selectedTextLayer ? (
|
||||
<Type className="h-8 w-8 text-muted-foreground" />
|
||||
<Type className="h-7 w-7 text-muted-foreground" />
|
||||
) : (
|
||||
<ImageIcon className="h-8 w-8 text-muted-foreground" />
|
||||
<ImageIcon className="h-7 w-7 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 text-sm font-medium">还未导出成品图</div>
|
||||
<div className="mt-3 text-sm font-medium">还未导出成品图</div>
|
||||
<div className="mt-2 text-xs leading-6 text-muted-foreground">
|
||||
在模板画布里完成替换后,点击“应用模板并导出”,这里会显示最终图片。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { AgentPsdTextColorConfig } from "@/types/agent";
|
||||
import type { AgentPsdTemplateSecondaryCategoryConfig } from "@/lib/agents/psd-template-secondary-category-config";
|
||||
|
||||
export const AGENT_PSD_TEMPLATES_KEY = "agent_psd_templates";
|
||||
export const AGENT_PSD_TEMPLATE_ASSET_VERSION = 3;
|
||||
@ -74,6 +75,7 @@ export interface AgentPsdTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
brandName: string;
|
||||
secondaryCategory?: string;
|
||||
description?: string;
|
||||
status: AgentPsdTemplateStatus;
|
||||
parseStatus: AgentPsdTemplateParseStatus;
|
||||
@ -101,6 +103,7 @@ export interface AgentPsdTemplateListResponse {
|
||||
success: boolean;
|
||||
templates: AgentPsdTemplate[];
|
||||
textColorConfig?: AgentPsdTextColorConfig;
|
||||
secondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@ -383,6 +386,7 @@ export function parseAgentPsdTemplates(value: unknown) {
|
||||
id,
|
||||
name,
|
||||
brandName,
|
||||
secondaryCategory: normalizeText(record.secondaryCategory) || undefined,
|
||||
description: normalizeText(record.description) || undefined,
|
||||
status: normalizeAgentPsdTemplateStatus(record.status),
|
||||
parseStatus,
|
||||
|
||||
@ -469,7 +469,7 @@ export async function renderAgentPsdTemplate(params: {
|
||||
placement: imagePlacements[layer.id],
|
||||
});
|
||||
|
||||
let overlayBuffer = await sharp(sourceBuffer)
|
||||
const resizedOverlayBuffer = await sharp(sourceBuffer)
|
||||
.rotate()
|
||||
.resize({
|
||||
width: Math.max(1, Math.round(placementFrame.width)),
|
||||
@ -485,9 +485,49 @@ export async function renderAgentPsdTemplate(params: {
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const slotOverlay = await fitOverlayBufferIntoCanvas({
|
||||
input: resizedOverlayBuffer,
|
||||
left: placementFrame.left,
|
||||
top: placementFrame.top,
|
||||
width: placementFrame.width,
|
||||
height: placementFrame.height,
|
||||
canvasWidth: layer.width,
|
||||
canvasHeight: layer.height,
|
||||
});
|
||||
|
||||
let overlayBuffer = await sharp({
|
||||
create: {
|
||||
width: Math.max(1, Math.round(layer.width)),
|
||||
height: Math.max(1, Math.round(layer.height)),
|
||||
channels: 4,
|
||||
background: {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
alpha: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
.composite(slotOverlay ? [slotOverlay] : [])
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
if (layer.maskUrl) {
|
||||
try {
|
||||
const maskBuffer = await readCachedBuffer(layer.maskUrl);
|
||||
const maskBuffer = await sharp(await readCachedBuffer(layer.maskUrl))
|
||||
.resize({
|
||||
width: Math.max(1, Math.round(layer.width)),
|
||||
height: Math.max(1, Math.round(layer.height)),
|
||||
fit: "fill",
|
||||
background: {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
alpha: 0,
|
||||
},
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
overlayBuffer = await sharp(overlayBuffer)
|
||||
.composite([
|
||||
{
|
||||
@ -508,10 +548,10 @@ export async function renderAgentPsdTemplate(params: {
|
||||
|
||||
const didAppend = await appendBufferOverlay({
|
||||
input: overlayBuffer,
|
||||
left: layer.x + placementFrame.left,
|
||||
top: layer.y + placementFrame.top,
|
||||
width: placementFrame.width,
|
||||
height: placementFrame.height,
|
||||
left: layer.x,
|
||||
top: layer.y,
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
});
|
||||
if (!didAppend) continue;
|
||||
continue;
|
||||
|
||||
23
lib/agents/psd-template-secondary-category-config.ts
Normal file
23
lib/agents/psd-template-secondary-category-config.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export interface AgentPsdTemplateSecondaryCategoryConfig {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export const AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY =
|
||||
"agent_psd_template_secondary_category_config";
|
||||
|
||||
export const defaultAgentPsdTemplateSecondaryCategoryConfig: AgentPsdTemplateSecondaryCategoryConfig = {
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
export function normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||
value: unknown
|
||||
): AgentPsdTemplateSecondaryCategoryConfig {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
enabled: raw.enabled === true,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user