perf: speed up PSD template loading
This commit is contained in:
parent
10dc87eaf7
commit
df6694de5d
97
app/api/psd-templates/[id]/route.ts
Normal file
97
app/api/psd-templates/[id]/route.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
|
import {
|
||||||
|
type AgentPsdTemplateMutationResponse,
|
||||||
|
getEditableAgentPsdTemplateLayers,
|
||||||
|
isAgentPsdTemplateReady,
|
||||||
|
isAgentPsdTemplateRuntimeReady,
|
||||||
|
isAgentPsdTextLayerSlot,
|
||||||
|
} from "@/lib/agents/psd-template-config";
|
||||||
|
import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair";
|
||||||
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
function jsonError(status: number, error: string) {
|
||||||
|
const payload: AgentPsdTemplateMutationResponse = {
|
||||||
|
success: false,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
return NextResponse.json(payload, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: Request,
|
||||||
|
context: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
const session = await auth().catch(() => null);
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return jsonError(401, "未登录");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id } = await context.params;
|
||||||
|
const [templates, fonts] = await Promise.all([
|
||||||
|
readAgentPsdTemplates(),
|
||||||
|
readAgentPsdFonts().catch(
|
||||||
|
() => [] as Awaited<ReturnType<typeof readAgentPsdFonts>>
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
||||||
|
const currentTemplate = templates.find(
|
||||||
|
(candidateTemplate) => candidateTemplate.id === id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentTemplate || currentTemplate.status !== "active") {
|
||||||
|
return jsonError(404, "未找到 PSD 模板");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAgentPsdTemplateReady(currentTemplate)) {
|
||||||
|
return jsonError(409, "PSD 模板尚未完成解析");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAgentPsdTemplateRuntimeReady(currentTemplate)) {
|
||||||
|
return jsonError(409, "PSD 模板资源尚未准备完成,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getEditableAgentPsdTemplateLayers(currentTemplate).length === 0) {
|
||||||
|
return jsonError(409, "当前模板没有可编辑图层");
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = repairAgentPsdTemplateFontBindings(currentTemplate, fonts);
|
||||||
|
const normalizedTemplate = {
|
||||||
|
...template,
|
||||||
|
layers: template.layers.map((layer) => {
|
||||||
|
if (!isAgentPsdTextLayerSlot(layer) || !layer.fontId) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
const font = fontMap.get(layer.fontId);
|
||||||
|
if (!font) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...layer,
|
||||||
|
fontFileUrl: font.fileUrl,
|
||||||
|
fontContentType: font.contentType,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
template: normalizedTemplate,
|
||||||
|
} satisfies AgentPsdTemplateMutationResponse);
|
||||||
|
response.headers.set(
|
||||||
|
"Cache-Control",
|
||||||
|
"private, max-age=60, stale-while-revalidate=300"
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[PSD Templates] detail failed:", error);
|
||||||
|
return jsonError(503, "PSD 模板详情读取失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,35 +1,36 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { auth } from "@/app/(auth)/auth";
|
|
||||||
import { db } from "@/lib/db";
|
|
||||||
import { systemSettings } from "@/lib/db/schema";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { type NextRequest, NextResponse } from "next/server";
|
||||||
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
import {
|
import {
|
||||||
|
AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY,
|
||||||
|
defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
|
normalizeAgentPsdTemplateCategoryTagConfig,
|
||||||
|
} from "@/lib/agents/psd-template-category-tag-config";
|
||||||
|
import {
|
||||||
|
type AgentPsdTemplate,
|
||||||
|
type AgentPsdTemplateListResponse,
|
||||||
getEditableAgentPsdTemplateLayers,
|
getEditableAgentPsdTemplateLayers,
|
||||||
isAgentPsdTextLayerSlot,
|
|
||||||
isAgentPsdTemplateReady,
|
isAgentPsdTemplateReady,
|
||||||
isAgentPsdTemplateRuntimeReady,
|
isAgentPsdTemplateRuntimeReady,
|
||||||
|
isAgentPsdTextLayerSlot,
|
||||||
normalizeAgentPsdTemplateMode,
|
normalizeAgentPsdTemplateMode,
|
||||||
type AgentPsdTemplateListResponse,
|
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
|
import { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
||||||
|
defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
normalizeAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
|
} from "@/lib/agents/psd-template-secondary-category-config";
|
||||||
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
import {
|
import {
|
||||||
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
AGENT_PSD_TEXT_COLOR_CONFIG_KEY,
|
||||||
defaultAgentPsdTextColorConfig,
|
defaultAgentPsdTextColorConfig,
|
||||||
normalizeAgentPsdTextColorConfig,
|
normalizeAgentPsdTextColorConfig,
|
||||||
resolveAgentPsdTextColorConfigForBrand,
|
resolveAgentPsdTextColorConfigForBrand,
|
||||||
} from "@/lib/agents/psd-text-color-config";
|
} from "@/lib/agents/psd-text-color-config";
|
||||||
import {
|
import { db } from "@/lib/db";
|
||||||
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY,
|
import { systemSettings } from "@/lib/db/schema";
|
||||||
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 { repairAgentPsdTemplateFontBindings } from "@/lib/agents/psd-template-repair";
|
|
||||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@ -43,6 +44,13 @@ function jsonError(status: number, error: string) {
|
|||||||
return NextResponse.json(payload, { status });
|
return NextResponse.json(payload, { status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toPublicTemplateSummary(template: AgentPsdTemplate): AgentPsdTemplate {
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
layers: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const session = await auth().catch(() => null);
|
const session = await auth().catch(() => null);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
@ -50,14 +58,24 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestedBrand = String(request.nextUrl.searchParams.get("brand") || "").trim();
|
const requestedBrand = String(
|
||||||
const requestedModeRaw = String(request.nextUrl.searchParams.get("mode") || "").trim();
|
request.nextUrl.searchParams.get("brand") || ""
|
||||||
|
).trim();
|
||||||
|
const requestedModeRaw = String(
|
||||||
|
request.nextUrl.searchParams.get("mode") || ""
|
||||||
|
).trim();
|
||||||
|
const summaryOnly =
|
||||||
|
String(request.nextUrl.searchParams.get("summary") || "").trim() === "1";
|
||||||
const requestedMode =
|
const requestedMode =
|
||||||
requestedModeRaw === "main" || requestedModeRaw === "sku"
|
requestedModeRaw === "main" || requestedModeRaw === "sku"
|
||||||
? normalizeAgentPsdTemplateMode(requestedModeRaw)
|
? normalizeAgentPsdTemplateMode(requestedModeRaw)
|
||||||
: null;
|
: null;
|
||||||
const [allTemplates, textColorSetting, secondaryCategorySetting, categoryTagSetting] =
|
const [
|
||||||
await Promise.all([
|
allTemplates,
|
||||||
|
textColorSetting,
|
||||||
|
secondaryCategorySetting,
|
||||||
|
categoryTagSetting,
|
||||||
|
] = await Promise.all([
|
||||||
readAgentPsdTemplates(),
|
readAgentPsdTemplates(),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
@ -69,34 +87,49 @@ export async function GET(request: NextRequest) {
|
|||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(systemSettings)
|
.from(systemSettings)
|
||||||
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY))
|
.where(
|
||||||
|
eq(
|
||||||
|
systemSettings.key,
|
||||||
|
AGENT_PSD_TEMPLATE_SECONDARY_CATEGORY_CONFIG_KEY
|
||||||
|
)
|
||||||
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(systemSettings)
|
.from(systemSettings)
|
||||||
.where(eq(systemSettings.key, AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY))
|
.where(
|
||||||
|
eq(systemSettings.key, AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY)
|
||||||
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
.catch(() => null),
|
.catch(() => null),
|
||||||
]);
|
]);
|
||||||
const fonts = (await readAgentPsdFonts().catch(
|
let templates = allTemplates
|
||||||
() => [] as Awaited<ReturnType<typeof readAgentPsdFonts>>
|
|
||||||
)) as Awaited<ReturnType<typeof readAgentPsdFonts>>;
|
|
||||||
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
|
||||||
const templates =
|
|
||||||
allTemplates
|
|
||||||
.filter((template) => template.status === "active")
|
.filter((template) => template.status === "active")
|
||||||
.filter(isAgentPsdTemplateReady)
|
.filter(isAgentPsdTemplateReady)
|
||||||
.filter((template) =>
|
.filter((template) =>
|
||||||
requestedBrand ? template.brandName === requestedBrand : true
|
requestedBrand ? template.brandName === requestedBrand : true
|
||||||
)
|
)
|
||||||
.filter((template) => (requestedMode ? template.mode === requestedMode : true))
|
.filter((template) =>
|
||||||
|
requestedMode ? template.mode === requestedMode : true
|
||||||
|
)
|
||||||
.filter(isAgentPsdTemplateRuntimeReady)
|
.filter(isAgentPsdTemplateRuntimeReady)
|
||||||
.filter((template) => getEditableAgentPsdTemplateLayers(template).length > 0)
|
.filter(
|
||||||
.map((template) => repairAgentPsdTemplateFontBindings(template, fonts))
|
(template) => getEditableAgentPsdTemplateLayers(template).length > 0
|
||||||
.map((template) => ({
|
);
|
||||||
|
|
||||||
|
if (summaryOnly) {
|
||||||
|
templates = templates.map(toPublicTemplateSummary);
|
||||||
|
} else {
|
||||||
|
const fonts = (await readAgentPsdFonts().catch(
|
||||||
|
() => [] as Awaited<ReturnType<typeof readAgentPsdFonts>>
|
||||||
|
)) as Awaited<ReturnType<typeof readAgentPsdFonts>>;
|
||||||
|
const fontMap = new Map(fonts.map((font) => [font.id, font]));
|
||||||
|
templates = templates
|
||||||
|
.map((template) => repairAgentPsdTemplateFontBindings(template, fonts))
|
||||||
|
.map((template) => ({
|
||||||
...template,
|
...template,
|
||||||
layers: template.layers.map((layer) => {
|
layers: template.layers.map((layer) => {
|
||||||
if (!isAgentPsdTextLayerSlot(layer) || !layer.fontId) {
|
if (!isAgentPsdTextLayerSlot(layer) || !layer.fontId) {
|
||||||
@ -115,12 +148,13 @@ export async function GET(request: NextRequest) {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedTextColorConfig = textColorSetting
|
const normalizedTextColorConfig = textColorSetting
|
||||||
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
? normalizeAgentPsdTextColorConfig(textColorSetting.value)
|
||||||
: defaultAgentPsdTextColorConfig;
|
: defaultAgentPsdTextColorConfig;
|
||||||
|
|
||||||
return NextResponse.json({
|
const response = NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
templates,
|
templates,
|
||||||
textColorConfig: requestedBrand
|
textColorConfig: requestedBrand
|
||||||
@ -130,12 +164,19 @@ export async function GET(request: NextRequest) {
|
|||||||
)
|
)
|
||||||
: normalizedTextColorConfig,
|
: normalizedTextColorConfig,
|
||||||
secondaryCategoryConfig: secondaryCategorySetting
|
secondaryCategoryConfig: secondaryCategorySetting
|
||||||
? normalizeAgentPsdTemplateSecondaryCategoryConfig(secondaryCategorySetting.value)
|
? normalizeAgentPsdTemplateSecondaryCategoryConfig(
|
||||||
|
secondaryCategorySetting.value
|
||||||
|
)
|
||||||
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
: defaultAgentPsdTemplateSecondaryCategoryConfig,
|
||||||
categoryTagConfig: categoryTagSetting
|
categoryTagConfig: categoryTagSetting
|
||||||
? normalizeAgentPsdTemplateCategoryTagConfig(categoryTagSetting.value)
|
? normalizeAgentPsdTemplateCategoryTagConfig(categoryTagSetting.value)
|
||||||
: defaultAgentPsdTemplateCategoryTagConfig,
|
: defaultAgentPsdTemplateCategoryTagConfig,
|
||||||
} satisfies AgentPsdTemplateListResponse);
|
} satisfies AgentPsdTemplateListResponse);
|
||||||
|
response.headers.set(
|
||||||
|
"Cache-Control",
|
||||||
|
"private, max-age=60, stale-while-revalidate=300"
|
||||||
|
);
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[PSD Templates] list failed:", error);
|
console.error("[PSD Templates] list failed:", error);
|
||||||
return jsonError(503, "PSD 模板读取失败");
|
return jsonError(503, "PSD 模板读取失败");
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import useSWR from "swr";
|
import useSWR, { preload } from "swr";
|
||||||
import { AgentPsdTemplateExportPreview } from "@/components/agents/agent-psd-template-export-preview";
|
import { AgentPsdTemplateExportPreview } from "@/components/agents/agent-psd-template-export-preview";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
@ -60,6 +60,7 @@ import {
|
|||||||
type AgentPsdTemplateLayerSlot,
|
type AgentPsdTemplateLayerSlot,
|
||||||
type AgentPsdTemplateListResponse,
|
type AgentPsdTemplateListResponse,
|
||||||
type AgentPsdTemplateMode,
|
type AgentPsdTemplateMode,
|
||||||
|
type AgentPsdTemplateMutationResponse,
|
||||||
type AgentPsdTemplateRenderResponse,
|
type AgentPsdTemplateRenderResponse,
|
||||||
type AgentPsdTemplateTextLayerSlot,
|
type AgentPsdTemplateTextLayerSlot,
|
||||||
isAgentPsdImageLayerSlot,
|
isAgentPsdImageLayerSlot,
|
||||||
@ -81,9 +82,27 @@ import {
|
|||||||
} from "@/lib/agents/psd-text-layout";
|
} from "@/lib/agents/psd-text-layout";
|
||||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||||
|
|
||||||
|
function buildTemplateListRequestUrl(params?: {
|
||||||
|
brandName?: string | null;
|
||||||
|
summaryOnly?: boolean;
|
||||||
|
}) {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params?.summaryOnly !== false) {
|
||||||
|
searchParams.set("summary", "1");
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedBrandName = String(params?.brandName || "").trim();
|
||||||
|
if (normalizedBrandName) {
|
||||||
|
searchParams.set("brand", normalizedBrandName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
return queryString ? `/api/psd-templates?${queryString}` : "/api/psd-templates";
|
||||||
|
}
|
||||||
|
|
||||||
const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { cache: "no-store" });
|
const response = await fetch(url, { cache: "default" });
|
||||||
const payload = (await response
|
const payload = (await response
|
||||||
.json()
|
.json()
|
||||||
.catch(() => null)) as Partial<AgentPsdTemplateListResponse> | null;
|
.catch(() => null)) as Partial<AgentPsdTemplateListResponse> | null;
|
||||||
@ -131,6 +150,38 @@ const fetcher = async (url: string): Promise<AgentPsdTemplateListResponse> => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const templateDetailFetcher = async (
|
||||||
|
url: string
|
||||||
|
): Promise<AgentPsdTemplateMutationResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { cache: "default" });
|
||||||
|
const payload = (await response
|
||||||
|
.json()
|
||||||
|
.catch(() => null)) as Partial<AgentPsdTemplateMutationResponse> | null;
|
||||||
|
|
||||||
|
if (!response.ok || !payload || payload.success !== true || !payload.template) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
typeof payload?.error === "string"
|
||||||
|
? payload.error
|
||||||
|
: "PSD 模板详情加载失败",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
template: parseAgentPsdTemplates([payload.template])[0],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "PSD 模板详情加载失败",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const PREVIEW_VIEWPORT_PADDING = 56;
|
const PREVIEW_VIEWPORT_PADDING = 56;
|
||||||
const PREVIEW_STAGE_MAX_SCALE = 0.82;
|
const PREVIEW_STAGE_MAX_SCALE = 0.82;
|
||||||
const PREVIEW_STAGE_TARGET_WIDTH = 760;
|
const PREVIEW_STAGE_TARGET_WIDTH = 760;
|
||||||
@ -139,6 +190,28 @@ const MIN_IMAGE_SCALE = 0.4;
|
|||||||
const MAX_IMAGE_SCALE = 3;
|
const MAX_IMAGE_SCALE = 3;
|
||||||
const UNASSIGNED_BRAND_LABEL = "未分类";
|
const UNASSIGNED_BRAND_LABEL = "未分类";
|
||||||
|
|
||||||
|
function buildTemplatePreviewThumbUrl(
|
||||||
|
previewUrl: string,
|
||||||
|
params?: {
|
||||||
|
width?: number;
|
||||||
|
quality?: number;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const normalizedUrl = String(previewUrl || "").trim();
|
||||||
|
if (!normalizedUrl) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
url: normalizedUrl,
|
||||||
|
w: String(Math.max(80, Math.round(params?.width || 160))),
|
||||||
|
q: String(Math.max(40, Math.round(params?.quality || 72))),
|
||||||
|
f: "webp",
|
||||||
|
});
|
||||||
|
|
||||||
|
return `/api/image-proxy?${searchParams.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeManagedBrandName(value?: string | null) {
|
function normalizeManagedBrandName(value?: string | null) {
|
||||||
const normalized = String(value || "").trim();
|
const normalized = String(value || "").trim();
|
||||||
if (!normalized || normalized === "未分配品牌") {
|
if (!normalized || normalized === "未分配品牌") {
|
||||||
@ -316,7 +389,11 @@ function renderTemplateSelectContent(
|
|||||||
<img
|
<img
|
||||||
alt={template.name}
|
alt={template.name}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
src={template.previewUrl}
|
decoding="async"
|
||||||
|
loading="lazy"
|
||||||
|
src={buildTemplatePreviewThumbUrl(template.previewUrl, {
|
||||||
|
width: compact ? 96 : 92,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 text-left">
|
<div className="min-w-0 text-left">
|
||||||
@ -352,11 +429,13 @@ function renderTemplatePickerCardContent(
|
|||||||
showSecondaryCategory?: boolean;
|
showSecondaryCategory?: boolean;
|
||||||
showBrand?: boolean;
|
showBrand?: boolean;
|
||||||
emphasize?: boolean;
|
emphasize?: boolean;
|
||||||
|
eager?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
||||||
const showBrand = params?.showBrand === true;
|
const showBrand = params?.showBrand === true;
|
||||||
const emphasize = params?.emphasize === true;
|
const emphasize = params?.emphasize === true;
|
||||||
|
const eager = params?.eager === true;
|
||||||
const metaLabel = getTemplateMetaLabel(
|
const metaLabel = getTemplateMetaLabel(
|
||||||
template,
|
template,
|
||||||
showSecondaryCategory,
|
showSecondaryCategory,
|
||||||
@ -369,7 +448,12 @@ function renderTemplatePickerCardContent(
|
|||||||
<img
|
<img
|
||||||
alt={template.name}
|
alt={template.name}
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full object-cover"
|
||||||
src={template.previewUrl}
|
decoding="async"
|
||||||
|
fetchPriority={eager ? "high" : "low"}
|
||||||
|
loading={eager ? "eager" : "lazy"}
|
||||||
|
src={buildTemplatePreviewThumbUrl(template.previewUrl, {
|
||||||
|
width: 112,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@ -1000,20 +1084,21 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
);
|
);
|
||||||
const normalizedTemplateMode =
|
const normalizedTemplateMode =
|
||||||
templateMode === "sku" ? "sku" : templateMode === "main" ? "main" : "";
|
templateMode === "sku" ? "sku" : templateMode === "main" ? "main" : "";
|
||||||
|
const templateListPrefetchUrl = useMemo(
|
||||||
|
() =>
|
||||||
|
buildTemplateListRequestUrl({
|
||||||
|
brandName: normalizedBrandName,
|
||||||
|
summaryOnly: true,
|
||||||
|
}),
|
||||||
|
[normalizedBrandName]
|
||||||
|
);
|
||||||
const templateListRequestUrl = useMemo(() => {
|
const templateListRequestUrl = useMemo(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
return templateListPrefetchUrl;
|
||||||
if (normalizedBrandName) {
|
}, [open, templateListPrefetchUrl]);
|
||||||
searchParams.set("brand", normalizedBrandName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchParams.size > 0
|
|
||||||
? `/api/psd-templates?${searchParams.toString()}`
|
|
||||||
: "/api/psd-templates";
|
|
||||||
}, [open, normalizedBrandName]);
|
|
||||||
const { data, isLoading } = useSWR<AgentPsdTemplateListResponse>(
|
const { data, isLoading } = useSWR<AgentPsdTemplateListResponse>(
|
||||||
templateListRequestUrl,
|
templateListRequestUrl,
|
||||||
fetcher,
|
fetcher,
|
||||||
@ -1057,8 +1142,26 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
),
|
),
|
||||||
[activeCategoryTags, allTemplates, normalizedSelectedCategoryNames]
|
[activeCategoryTags, allTemplates, normalizedSelectedCategoryNames]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string>("");
|
||||||
|
const selectedTemplateDetailRequestUrl = useMemo(() => {
|
||||||
|
if (!open || !selectedTemplateId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/api/psd-templates/${encodeURIComponent(selectedTemplateId)}`;
|
||||||
|
}, [open, selectedTemplateId]);
|
||||||
|
const {
|
||||||
|
data: selectedTemplateDetailData,
|
||||||
|
isLoading: isSelectedTemplateDetailLoading,
|
||||||
|
} = useSWR<AgentPsdTemplateMutationResponse>(
|
||||||
|
selectedTemplateDetailRequestUrl,
|
||||||
|
templateDetailFetcher,
|
||||||
|
{
|
||||||
|
dedupingInterval: 60_000,
|
||||||
|
keepPreviousData: true,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
const [selectedLayerId, setSelectedLayerId] = useState<string>("");
|
||||||
const [imageAssignments, setImageAssignments] = useState<
|
const [imageAssignments, setImageAssignments] = useState<
|
||||||
Record<string, string>
|
Record<string, string>
|
||||||
@ -1107,11 +1210,32 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
textColorAssignmentsRef.current = textColorAssignments;
|
textColorAssignmentsRef.current = textColorAssignments;
|
||||||
selectedLayerIdRef.current = selectedLayerId;
|
selectedLayerIdRef.current = selectedLayerId;
|
||||||
|
|
||||||
const selectedTemplate = useMemo(
|
const selectedTemplateSummary = useMemo(
|
||||||
() =>
|
() =>
|
||||||
templates.find((template) => template.id === selectedTemplateId) || null,
|
templates.find((template) => template.id === selectedTemplateId) || null,
|
||||||
[selectedTemplateId, templates]
|
[selectedTemplateId, templates]
|
||||||
);
|
);
|
||||||
|
const selectedTemplateDetail = useMemo(() => {
|
||||||
|
if (!selectedTemplateDetailData?.success || !selectedTemplateDetailData.template) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedTemplateDetailData.template.id === selectedTemplateId
|
||||||
|
? selectedTemplateDetailData.template
|
||||||
|
: null;
|
||||||
|
}, [selectedTemplateDetailData, selectedTemplateId]);
|
||||||
|
const selectedTemplate = useMemo(
|
||||||
|
() => selectedTemplateDetail || selectedTemplateSummary,
|
||||||
|
[selectedTemplateDetail, selectedTemplateSummary]
|
||||||
|
);
|
||||||
|
const selectedTemplateDetailError =
|
||||||
|
selectedTemplateDetailData?.success === false
|
||||||
|
? String(selectedTemplateDetailData.error || "").trim()
|
||||||
|
: "";
|
||||||
|
const isSelectedTemplateLoading =
|
||||||
|
Boolean(selectedTemplateSummary) &&
|
||||||
|
(selectedTemplateSummary?.layers.length || 0) === 0 &&
|
||||||
|
isSelectedTemplateDetailLoading;
|
||||||
const effectiveTemplateBrandName = useMemo(
|
const effectiveTemplateBrandName = useMemo(
|
||||||
() =>
|
() =>
|
||||||
normalizedBrandName || String(selectedTemplate?.brandName || "").trim(),
|
normalizedBrandName || String(selectedTemplate?.brandName || "").trim(),
|
||||||
@ -1185,6 +1309,25 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
templates: grouped,
|
templates: grouped,
|
||||||
}));
|
}));
|
||||||
}, [selectedModeTemplates, shouldGroupTemplatesBySecondaryCategory]);
|
}, [selectedModeTemplates, shouldGroupTemplatesBySecondaryCategory]);
|
||||||
|
const preferredInitialTemplate = useMemo(() => {
|
||||||
|
if (normalizedTemplateMode === "sku" && skuModeTemplates.length > 0) {
|
||||||
|
return skuModeTemplates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedTemplateMode === "main" && mainModeTemplates.length > 0) {
|
||||||
|
return mainModeTemplates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainModeTemplates.length > 0) {
|
||||||
|
return mainModeTemplates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skuModeTemplates.length > 0) {
|
||||||
|
return skuModeTemplates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates[0] || null;
|
||||||
|
}, [mainModeTemplates, normalizedTemplateMode, skuModeTemplates, templates]);
|
||||||
|
|
||||||
const editableImageLayers = useMemo(
|
const editableImageLayers = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -1228,6 +1371,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
);
|
);
|
||||||
const shouldShowCompositeFallback = useMemo(
|
const shouldShowCompositeFallback = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
orderedPreviewLayers.length === 0 ||
|
||||||
orderedPreviewLayers.some(
|
orderedPreviewLayers.some(
|
||||||
(layer) => !String(layer.sourceUrl || "").trim()
|
(layer) => !String(layer.sourceUrl || "").trim()
|
||||||
),
|
),
|
||||||
@ -1530,6 +1674,35 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
const selectedTemplateSelectValue = selectedTemplate?.id;
|
const selectedTemplateSelectValue = selectedTemplate?.id;
|
||||||
const hasTemplateOptions = templates.length > 0;
|
const hasTemplateOptions = templates.length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined" || !templateListPrefetchUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preloadTemplates = () => {
|
||||||
|
void preload(templateListPrefetchUrl, fetcher);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof window.requestIdleCallback === "function") {
|
||||||
|
const idleId = window.requestIdleCallback(preloadTemplates, {
|
||||||
|
timeout: 1200,
|
||||||
|
});
|
||||||
|
return () => window.cancelIdleCallback?.(idleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(preloadTemplates, 300);
|
||||||
|
return () => window.clearTimeout(timeoutId);
|
||||||
|
}, [templateListPrefetchUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!preferredInitialTemplate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const detailUrl = `/api/psd-templates/${encodeURIComponent(preferredInitialTemplate.id)}`;
|
||||||
|
void preload(detailUrl, templateDetailFetcher);
|
||||||
|
}, [preferredInitialTemplate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
return;
|
return;
|
||||||
@ -1959,6 +2132,10 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
toast.error("请先选择 PSD 模板");
|
toast.error("请先选择 PSD 模板");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (selectedTemplate.layers.length === 0) {
|
||||||
|
toast.error("模板详情仍在加载,请稍候再试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsRendering(true);
|
setIsRendering(true);
|
||||||
@ -2076,10 +2253,14 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
{selectedTemplate && (
|
{selectedTemplate && (
|
||||||
<Button
|
<Button
|
||||||
className="h-9 rounded-xl bg-emerald-500 px-4 font-medium text-white shadow-[0_2px_10px_rgba(16,185,129,0.2)] ring-1 ring-emerald-500/50 transition-all hover:bg-emerald-400 hover:shadow-[0_4px_15px_rgba(16,185,129,0.3)] disabled:opacity-50"
|
className="h-9 rounded-xl bg-emerald-500 px-4 font-medium text-white shadow-[0_2px_10px_rgba(16,185,129,0.2)] ring-1 ring-emerald-500/50 transition-all hover:bg-emerald-400 hover:shadow-[0_4px_15px_rgba(16,185,129,0.3)] disabled:opacity-50"
|
||||||
disabled={isRendering}
|
disabled={
|
||||||
|
isRendering ||
|
||||||
|
isSelectedTemplateLoading ||
|
||||||
|
selectedTemplateLayers.length === 0
|
||||||
|
}
|
||||||
onClick={handleRender}
|
onClick={handleRender}
|
||||||
>
|
>
|
||||||
{isRendering ? (
|
{isRendering || isSelectedTemplateLoading ? (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
<Sparkles className="mr-2 h-4 w-4" />
|
<Sparkles className="mr-2 h-4 w-4" />
|
||||||
@ -2255,7 +2436,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
) : mainModeTemplates.length > 0 ? (
|
) : mainModeTemplates.length > 0 ? (
|
||||||
mainModeTemplates.map((template) => (
|
mainModeTemplates.map((template, index) => (
|
||||||
<button
|
<button
|
||||||
className={`w-full rounded-2xl border p-3 text-left transition-all duration-200 ${
|
className={`w-full rounded-2xl border p-3 text-left transition-all duration-200 ${
|
||||||
selectedTemplateId === template.id
|
selectedTemplateId === template.id
|
||||||
@ -2271,6 +2452,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
shouldGroupTemplatesBySecondaryCategory,
|
shouldGroupTemplatesBySecondaryCategory,
|
||||||
showBrand: shouldShowTemplateBrand,
|
showBrand: shouldShowTemplateBrand,
|
||||||
emphasize: true,
|
emphasize: true,
|
||||||
|
eager: index < 2,
|
||||||
})}
|
})}
|
||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
@ -2298,7 +2480,14 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
<img
|
<img
|
||||||
alt={selectedTemplate.name}
|
alt={selectedTemplate.name}
|
||||||
className="h-full w-full object-contain"
|
className="h-full w-full object-contain"
|
||||||
src={selectedTemplate.previewUrl}
|
decoding="async"
|
||||||
|
src={buildTemplatePreviewThumbUrl(
|
||||||
|
selectedTemplate.previewUrl,
|
||||||
|
{
|
||||||
|
width: 440,
|
||||||
|
quality: 78,
|
||||||
|
}
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2336,41 +2525,70 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
ref={previewViewportRef}
|
ref={previewViewportRef}
|
||||||
>
|
>
|
||||||
{selectedTemplate ? (
|
{selectedTemplate ? (
|
||||||
<div
|
selectedTemplateLayers.length === 0 ? (
|
||||||
className="relative shrink-0"
|
<div className="flex h-full w-full flex-col items-center justify-center rounded-2xl border border-white/10 border-dashed bg-white/[0.02] p-8 text-center text-sm text-white/50 shadow-inner">
|
||||||
style={{
|
{selectedTemplateDetailError ? (
|
||||||
width: selectedTemplate.width * stageScale,
|
<>
|
||||||
height: selectedTemplate.height * stageScale,
|
<div className="font-medium text-white/80">
|
||||||
}}
|
模板详情加载失败
|
||||||
>
|
</div>
|
||||||
|
<div className="mt-2 max-w-md text-xs leading-relaxed text-white/45">
|
||||||
|
{selectedTemplateDetailError}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mb-4 h-7 w-7 animate-spin text-emerald-400" />
|
||||||
|
<div className="font-medium text-white/80">
|
||||||
|
正在加载模板图层与编辑配置
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 max-w-md text-xs leading-relaxed text-white/45">
|
||||||
|
模板卡片已经优先显示,当前正在按需加载这个模板的完整图层、字体和编辑底图。
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 left-0 overflow-hidden rounded-[20px] shadow-2xl"
|
className="relative shrink-0"
|
||||||
style={{
|
style={{
|
||||||
width: selectedTemplate.width,
|
width: selectedTemplate.width * stageScale,
|
||||||
height: selectedTemplate.height,
|
height: selectedTemplate.height * stageScale,
|
||||||
transform: `scale(${stageScale})`,
|
|
||||||
transformOrigin: "top left",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{shouldShowCompositeFallback ? (
|
<div
|
||||||
<img
|
className="absolute top-0 left-0 overflow-hidden rounded-[20px] shadow-2xl"
|
||||||
alt={`${selectedTemplate.name} 编辑底图`}
|
style={{
|
||||||
className="absolute inset-0 h-full w-full object-cover"
|
width: selectedTemplate.width,
|
||||||
src={
|
height: selectedTemplate.height,
|
||||||
selectedTemplate.renderBaseUrl ||
|
transform: `scale(${stageScale})`,
|
||||||
selectedTemplate.previewUrl
|
transformOrigin: "top left",
|
||||||
}
|
}}
|
||||||
/>
|
>
|
||||||
) : null}
|
{shouldShowCompositeFallback ? (
|
||||||
|
<img
|
||||||
|
alt={`${selectedTemplate.name} 编辑底图`}
|
||||||
|
className="absolute inset-0 h-full w-full object-cover"
|
||||||
|
decoding="async"
|
||||||
|
src={buildTemplatePreviewThumbUrl(
|
||||||
|
selectedTemplate.renderBaseUrl ||
|
||||||
|
selectedTemplate.previewUrl,
|
||||||
|
{
|
||||||
|
width: 1280,
|
||||||
|
quality: 80,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="pointer-events-none absolute inset-0">
|
<div className="pointer-events-none absolute inset-0">
|
||||||
{orderedPreviewLayers.map((layer) => {
|
{orderedPreviewLayers.map((layer) => {
|
||||||
const layerBoxStyle = {
|
const layerBoxStyle = {
|
||||||
left: layer.x,
|
left: layer.x,
|
||||||
top: layer.y,
|
top: layer.y,
|
||||||
width: layer.width,
|
width: layer.width,
|
||||||
height: layer.height,
|
height: layer.height,
|
||||||
} satisfies CSSProperties;
|
} satisfies CSSProperties;
|
||||||
|
|
||||||
if (isAgentPsdImageLayerSlot(layer)) {
|
if (isAgentPsdImageLayerSlot(layer)) {
|
||||||
const assignedUrl =
|
const assignedUrl =
|
||||||
@ -2487,7 +2705,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
{orderedEditableImageLayers.map((layer) => {
|
{orderedEditableImageLayers.map((layer) => {
|
||||||
const isSelected = selectedLayerId === layer.id;
|
const isSelected = selectedLayerId === layer.id;
|
||||||
const frameStyle = getLayerFrameStyle(
|
const frameStyle = getLayerFrameStyle(
|
||||||
@ -2711,9 +2929,10 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-5">
|
<div className="flex h-full w-full flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-5">
|
||||||
<div className="mb-4 flex items-center justify-between gap-3">
|
<div className="mb-4 flex items-center justify-between gap-3">
|
||||||
@ -2749,7 +2968,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
</div>
|
</div>
|
||||||
) : skuModeTemplates.length > 0 ? (
|
) : skuModeTemplates.length > 0 ? (
|
||||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{skuModeTemplates.map((template) => (
|
{skuModeTemplates.map((template, index) => (
|
||||||
<button
|
<button
|
||||||
className={`rounded-2xl border p-3 text-left transition-all duration-200 ${
|
className={`rounded-2xl border p-3 text-left transition-all duration-200 ${
|
||||||
selectedTemplateId === template.id
|
selectedTemplateId === template.id
|
||||||
@ -2765,6 +2984,7 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
shouldGroupTemplatesBySecondaryCategory,
|
shouldGroupTemplatesBySecondaryCategory,
|
||||||
showBrand: shouldShowTemplateBrand,
|
showBrand: shouldShowTemplateBrand,
|
||||||
emphasize: true,
|
emphasize: true,
|
||||||
|
eager: index < 4,
|
||||||
})}
|
})}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@ -2953,6 +3173,29 @@ export function AgentPsdTemplateEditorDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : selectedTemplate && selectedTemplateLayers.length === 0 ? (
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center rounded-2xl border border-white/10 border-dashed bg-white/[0.02] p-8 text-center text-sm text-white/40 shadow-inner">
|
||||||
|
{selectedTemplateDetailError ? (
|
||||||
|
<>
|
||||||
|
<div className="font-medium text-white/80">
|
||||||
|
模板详情加载失败
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 max-w-xs text-xs leading-relaxed text-white/45">
|
||||||
|
{selectedTemplateDetailError}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mb-4 h-6 w-6 animate-spin text-emerald-400" />
|
||||||
|
<div className="font-medium text-white/80">
|
||||||
|
正在加载这个模板的编辑图层
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 max-w-xs text-xs leading-relaxed text-white/45">
|
||||||
|
首屏先展示模板列表,完整文字图层、图片图层和字体配置会按需补回来。
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center rounded-2xl border border-white/10 border-dashed bg-white/[0.02] p-8 text-center text-sm text-white/40 shadow-inner">
|
<div className="flex flex-1 flex-col items-center justify-center rounded-2xl border border-white/10 border-dashed bg-white/[0.02] p-8 text-center text-sm text-white/40 shadow-inner">
|
||||||
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-full border border-white/10 bg-white/5 shadow-inner">
|
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-full border border-white/10 bg-white/5 shadow-inner">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user