697 lines
21 KiB
TypeScript
697 lines
21 KiB
TypeScript
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 = 7;
|
|
|
|
export type AgentPsdTemplateStatus = "active" | "disabled";
|
|
export type AgentPsdTemplateParseStatus =
|
|
| "pending"
|
|
| "processing"
|
|
| "completed"
|
|
| "failed";
|
|
export type AgentPsdTemplateMode = "main" | "sku";
|
|
export type AgentPsdTemplateReplaceMode = "cover" | "contain" | "stretch";
|
|
export type AgentPsdTemplateSlotType = "image" | "text";
|
|
export type AgentPsdTextAlign = "left" | "center" | "right";
|
|
export type AgentPsdTextStyleSource = "auto" | "custom";
|
|
|
|
export interface AgentPsdTemplateImageContentBounds {
|
|
left: number;
|
|
top: number;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
export interface AgentPsdTemplateBaseLayerSlot {
|
|
id: string;
|
|
layerId: string;
|
|
layerName: string;
|
|
layerPath: string;
|
|
depth: number;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
isEditable: boolean;
|
|
isSuperLocked?: boolean;
|
|
isRequired: boolean;
|
|
previewUrl?: string;
|
|
previewPath?: string;
|
|
sourceUrl?: string;
|
|
sourcePath?: string;
|
|
}
|
|
|
|
export interface AgentPsdTemplateImageLayerSlot
|
|
extends AgentPsdTemplateBaseLayerSlot {
|
|
slotType: "image";
|
|
isDefaultResultSource: boolean;
|
|
allowUserUploadOverride: boolean;
|
|
forceClipMask?: boolean;
|
|
clipMaskLayerId?: string;
|
|
replaceMode: AgentPsdTemplateReplaceMode;
|
|
contentBounds?: AgentPsdTemplateImageContentBounds;
|
|
maskUrl?: string;
|
|
maskPath?: string;
|
|
}
|
|
|
|
export interface AgentPsdTemplateTextLayerSlot
|
|
extends AgentPsdTemplateBaseLayerSlot {
|
|
slotType: "text";
|
|
styleSource?: AgentPsdTextStyleSource;
|
|
defaultText?: string;
|
|
placeholder?: string;
|
|
detectedFontName?: string;
|
|
detectedFontNames?: string[];
|
|
fontId?: string;
|
|
fontFamily?: string;
|
|
fontLabel?: string;
|
|
fontFileUrl?: string;
|
|
fontContentType?: string;
|
|
fontSize: number;
|
|
lineHeight: number;
|
|
letterSpacing: number;
|
|
fontWeight: number;
|
|
textColor: string;
|
|
textAlign: AgentPsdTextAlign;
|
|
paddingX: number;
|
|
paddingY: number;
|
|
maxLines: number;
|
|
}
|
|
|
|
export type AgentPsdTemplateLayerSlot =
|
|
| AgentPsdTemplateImageLayerSlot
|
|
| AgentPsdTemplateTextLayerSlot;
|
|
|
|
export interface AgentPsdTemplateRenderSegment {
|
|
id: string;
|
|
beforeLayerId?: string;
|
|
url: string;
|
|
path?: string;
|
|
}
|
|
|
|
export interface AgentPsdTemplate {
|
|
id: string;
|
|
name: string;
|
|
brandName: string;
|
|
mode: AgentPsdTemplateMode;
|
|
secondaryCategory?: string;
|
|
categoryTags?: string[];
|
|
description?: string;
|
|
status: AgentPsdTemplateStatus;
|
|
parseStatus: AgentPsdTemplateParseStatus;
|
|
parseProgress?: number;
|
|
parseStage?: string;
|
|
parseError?: string;
|
|
parseStartedAt?: string;
|
|
parseHeartbeatAt?: string;
|
|
fileName: string;
|
|
fileUrl: string;
|
|
filePath?: string;
|
|
previewUrl: string;
|
|
previewPath?: string;
|
|
renderBaseUrl?: string;
|
|
renderBasePath?: string;
|
|
renderSegments?: AgentPsdTemplateRenderSegment[];
|
|
width: number;
|
|
height: number;
|
|
layerCount: number;
|
|
layers: AgentPsdTemplateLayerSlot[];
|
|
assetVersion?: number;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
uploadedById?: string;
|
|
}
|
|
|
|
export interface AgentPsdTemplateListResponse {
|
|
success: boolean;
|
|
templates: AgentPsdTemplate[];
|
|
textColorConfig?: AgentPsdTextColorConfig;
|
|
secondaryCategoryConfig?: AgentPsdTemplateSecondaryCategoryConfig;
|
|
categoryTagConfig?: AgentPsdTemplateCategoryTagConfig;
|
|
error?: string;
|
|
}
|
|
|
|
export interface AgentPsdTemplateMutationResponse {
|
|
success: boolean;
|
|
template?: AgentPsdTemplate;
|
|
error?: string;
|
|
}
|
|
|
|
export interface AgentPsdTemplateRenderResult {
|
|
url: string;
|
|
pathname: string;
|
|
contentType?: string;
|
|
}
|
|
|
|
export type AgentPsdTemplateRenderStatus =
|
|
| "pending"
|
|
| "running"
|
|
| "succeeded"
|
|
| "failed";
|
|
|
|
export interface AgentPsdTemplateRenderResponse {
|
|
success: boolean;
|
|
processing?: boolean;
|
|
status?: AgentPsdTemplateRenderStatus;
|
|
taskId?: string;
|
|
pollAfterMs?: number;
|
|
result?: AgentPsdTemplateRenderResult;
|
|
error?: string;
|
|
}
|
|
|
|
function normalizeText(value: unknown) {
|
|
return String(value || "").trim();
|
|
}
|
|
|
|
function normalizeUrl(value: unknown) {
|
|
const text = normalizeText(value);
|
|
return /^https?:\/\//i.test(text) ? text : "";
|
|
}
|
|
|
|
function normalizeOptionalUrl(value: unknown) {
|
|
const url = normalizeUrl(value);
|
|
return url || undefined;
|
|
}
|
|
|
|
function toIsoDateString(value: unknown) {
|
|
const text = normalizeText(value);
|
|
if (!text) return null;
|
|
|
|
const date = new Date(text);
|
|
if (Number.isNaN(date.getTime())) return null;
|
|
return date.toISOString();
|
|
}
|
|
|
|
function normalizePositiveInteger(value: unknown) {
|
|
const numberValue = Number(value);
|
|
if (!Number.isFinite(numberValue)) return 0;
|
|
return Math.max(0, Math.round(numberValue));
|
|
}
|
|
|
|
function normalizeInteger(value: unknown) {
|
|
const numberValue = Number(value);
|
|
if (!Number.isFinite(numberValue)) return 0;
|
|
return Math.round(numberValue);
|
|
}
|
|
|
|
function normalizePositiveNumber(value: unknown, fallback: number) {
|
|
const numberValue = Number(value);
|
|
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
|
return fallback;
|
|
}
|
|
return numberValue;
|
|
}
|
|
|
|
function normalizeColor(value: unknown, fallback = "#111111") {
|
|
const text = normalizeText(value);
|
|
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(text)) {
|
|
return text;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
export function normalizeAgentPsdTemplateStatus(value: unknown): AgentPsdTemplateStatus {
|
|
return normalizeText(value) === "active" ? "active" : "disabled";
|
|
}
|
|
|
|
export function normalizeAgentPsdTemplateMode(value: unknown): AgentPsdTemplateMode {
|
|
return normalizeText(value) === "sku" ? "sku" : "main";
|
|
}
|
|
|
|
export function normalizeAgentPsdTemplateParseStatus(
|
|
value: unknown
|
|
): AgentPsdTemplateParseStatus {
|
|
const status = normalizeText(value);
|
|
if (
|
|
status === "pending" ||
|
|
status === "processing" ||
|
|
status === "failed"
|
|
) {
|
|
return status;
|
|
}
|
|
return "completed";
|
|
}
|
|
|
|
export function normalizeAgentPsdTemplateReplaceMode(
|
|
value: unknown
|
|
): AgentPsdTemplateReplaceMode {
|
|
const mode = normalizeText(value);
|
|
if (mode === "contain" || mode === "stretch") {
|
|
return mode;
|
|
}
|
|
return "cover";
|
|
}
|
|
|
|
function normalizeAgentPsdTemplateImageContentBounds(
|
|
value: unknown,
|
|
containerWidth: number,
|
|
containerHeight: number
|
|
): AgentPsdTemplateImageContentBounds | undefined {
|
|
if (!value || typeof value !== "object") {
|
|
return undefined;
|
|
}
|
|
|
|
const record = value as Record<string, unknown>;
|
|
const left = normalizePositiveInteger(record.left);
|
|
const top = normalizePositiveInteger(record.top);
|
|
const width = normalizePositiveInteger(record.width);
|
|
const height = normalizePositiveInteger(record.height);
|
|
|
|
if (width <= 0 || height <= 0) {
|
|
return undefined;
|
|
}
|
|
|
|
const clampedLeft = Math.min(left, Math.max(0, containerWidth - 1));
|
|
const clampedTop = Math.min(top, Math.max(0, containerHeight - 1));
|
|
const maxWidth = Math.max(1, containerWidth - clampedLeft);
|
|
const maxHeight = Math.max(1, containerHeight - clampedTop);
|
|
const clampedWidth = Math.min(width, maxWidth);
|
|
const clampedHeight = Math.min(height, maxHeight);
|
|
|
|
if (
|
|
clampedLeft === 0 &&
|
|
clampedTop === 0 &&
|
|
clampedWidth === containerWidth &&
|
|
clampedHeight === containerHeight
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
left: clampedLeft,
|
|
top: clampedTop,
|
|
width: clampedWidth,
|
|
height: clampedHeight,
|
|
};
|
|
}
|
|
|
|
export function normalizeAgentPsdTemplateSlotType(
|
|
value: unknown
|
|
): AgentPsdTemplateSlotType {
|
|
return normalizeText(value) === "text" ? "text" : "image";
|
|
}
|
|
|
|
export function normalizeAgentPsdTextAlign(value: unknown): AgentPsdTextAlign {
|
|
const align = normalizeText(value);
|
|
if (align === "center" || align === "right") {
|
|
return align;
|
|
}
|
|
return "left";
|
|
}
|
|
|
|
export function resolveAgentPsdTextLayerEffectiveAlign(
|
|
layer: Pick<
|
|
AgentPsdTemplateTextLayerSlot,
|
|
"paddingX" | "styleSource" | "textAlign" | "width"
|
|
>
|
|
): AgentPsdTextAlign {
|
|
const textAlign = normalizeAgentPsdTextAlign(layer.textAlign);
|
|
const paddingX = normalizePositiveInteger(layer.paddingX);
|
|
const width = normalizePositiveInteger(layer.width);
|
|
|
|
if (
|
|
layer.styleSource !== "custom" &&
|
|
textAlign === "center" &&
|
|
paddingX <= Math.max(2, Math.round(width * 0.02))
|
|
) {
|
|
return "left";
|
|
}
|
|
|
|
return textAlign;
|
|
}
|
|
|
|
export function normalizeAgentPsdTextStyleSource(value: unknown): AgentPsdTextStyleSource {
|
|
return normalizeText(value) === "custom" ? "custom" : "auto";
|
|
}
|
|
|
|
export function parseAgentPsdTemplateLayers(value: unknown) {
|
|
if (!Array.isArray(value)) return [] as AgentPsdTemplateLayerSlot[];
|
|
|
|
const layers: AgentPsdTemplateLayerSlot[] = [];
|
|
|
|
for (const entry of value) {
|
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
continue;
|
|
}
|
|
|
|
const record = entry as Record<string, unknown>;
|
|
const id = normalizeText(record.id);
|
|
const layerId = normalizeText(record.layerId);
|
|
const layerName = normalizeText(record.layerName);
|
|
const layerPath = normalizeText(record.layerPath);
|
|
const width = normalizePositiveInteger(record.width);
|
|
const height = normalizePositiveInteger(record.height);
|
|
const slotType = normalizeAgentPsdTemplateSlotType(record.slotType);
|
|
|
|
if (!id || !layerId || !layerName || !layerPath || width <= 0 || height <= 0) {
|
|
continue;
|
|
}
|
|
|
|
const isEditable = record.isEditable === true;
|
|
const isSuperLocked = record.isSuperLocked === true;
|
|
const baseRecord = {
|
|
id,
|
|
layerId,
|
|
layerName,
|
|
layerPath,
|
|
depth: Math.max(0, normalizePositiveInteger(record.depth)),
|
|
x: normalizeInteger(record.x),
|
|
y: normalizeInteger(record.y),
|
|
width,
|
|
height,
|
|
isEditable,
|
|
isSuperLocked,
|
|
isRequired: isEditable && !isSuperLocked && record.isRequired === true,
|
|
previewUrl: normalizeOptionalUrl(record.previewUrl),
|
|
previewPath: normalizeText(record.previewPath) || undefined,
|
|
sourceUrl: normalizeOptionalUrl(record.sourceUrl),
|
|
sourcePath: normalizeText(record.sourcePath) || undefined,
|
|
} satisfies AgentPsdTemplateBaseLayerSlot;
|
|
|
|
if (slotType === "text") {
|
|
const estimatedFontSize = Math.max(16, Math.min(height, Math.round(height * 0.78)));
|
|
layers.push({
|
|
...baseRecord,
|
|
slotType: "text",
|
|
styleSource: normalizeAgentPsdTextStyleSource(record.styleSource),
|
|
defaultText: normalizeText(record.defaultText) || undefined,
|
|
placeholder: normalizeText(record.placeholder) || undefined,
|
|
detectedFontName: normalizeText(record.detectedFontName) || undefined,
|
|
detectedFontNames: Array.isArray(record.detectedFontNames)
|
|
? Array.from(
|
|
new Set(
|
|
record.detectedFontNames
|
|
.map((item) => normalizeText(item))
|
|
.filter(Boolean)
|
|
)
|
|
).slice(0, 8)
|
|
: undefined,
|
|
fontId: normalizeText(record.fontId) || undefined,
|
|
fontFamily: normalizeText(record.fontFamily) || undefined,
|
|
fontLabel: normalizeText(record.fontLabel) || undefined,
|
|
fontFileUrl: normalizeOptionalUrl(record.fontFileUrl),
|
|
fontContentType: normalizeText(record.fontContentType) || undefined,
|
|
fontSize: normalizePositiveNumber(record.fontSize, estimatedFontSize),
|
|
lineHeight: normalizePositiveNumber(record.lineHeight, 1.2),
|
|
letterSpacing: Number.isFinite(Number(record.letterSpacing))
|
|
? Number(record.letterSpacing)
|
|
: 0,
|
|
fontWeight: Math.max(
|
|
100,
|
|
Math.min(900, normalizePositiveInteger(record.fontWeight) || 500)
|
|
),
|
|
textColor: normalizeColor(record.textColor),
|
|
textAlign: normalizeAgentPsdTextAlign(record.textAlign),
|
|
paddingX: normalizePositiveInteger(record.paddingX),
|
|
paddingY: normalizePositiveInteger(record.paddingY),
|
|
maxLines: Math.max(1, normalizePositiveInteger(record.maxLines) || 4),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
layers.push({
|
|
...baseRecord,
|
|
slotType: "image",
|
|
isDefaultResultSource:
|
|
isEditable && !isSuperLocked && record.isDefaultResultSource === true,
|
|
allowUserUploadOverride:
|
|
isEditable && !isSuperLocked ? record.allowUserUploadOverride !== false : false,
|
|
forceClipMask: isEditable && !isSuperLocked && record.forceClipMask === true,
|
|
clipMaskLayerId:
|
|
isEditable && !isSuperLocked && record.forceClipMask === true
|
|
? normalizeText(record.clipMaskLayerId) || undefined
|
|
: undefined,
|
|
replaceMode: normalizeAgentPsdTemplateReplaceMode(record.replaceMode),
|
|
contentBounds: normalizeAgentPsdTemplateImageContentBounds(
|
|
record.contentBounds,
|
|
width,
|
|
height
|
|
),
|
|
maskUrl: normalizeOptionalUrl(record.maskUrl),
|
|
maskPath: normalizeText(record.maskPath) || undefined,
|
|
});
|
|
}
|
|
|
|
let defaultAssigned = false;
|
|
return layers.map((layer) => {
|
|
if (layer.slotType === "image") {
|
|
if (!layer.isEditable || !layer.isDefaultResultSource) {
|
|
return {
|
|
...layer,
|
|
isDefaultResultSource: false,
|
|
};
|
|
}
|
|
|
|
if (defaultAssigned) {
|
|
return {
|
|
...layer,
|
|
isDefaultResultSource: false,
|
|
};
|
|
}
|
|
|
|
defaultAssigned = true;
|
|
return layer;
|
|
}
|
|
|
|
return layer;
|
|
});
|
|
}
|
|
|
|
function parseAgentPsdTemplateRenderSegments(value: unknown) {
|
|
if (!Array.isArray(value)) return [] as AgentPsdTemplateRenderSegment[];
|
|
|
|
const segments: AgentPsdTemplateRenderSegment[] = [];
|
|
const seenIds = new Set<string>();
|
|
|
|
for (const entry of value) {
|
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
continue;
|
|
}
|
|
|
|
const record = entry as Record<string, unknown>;
|
|
const id = normalizeText(record.id);
|
|
const url = normalizeUrl(record.url);
|
|
if (!id || !url || seenIds.has(id)) {
|
|
continue;
|
|
}
|
|
|
|
seenIds.add(id);
|
|
segments.push({
|
|
id,
|
|
beforeLayerId: normalizeText(record.beforeLayerId) || undefined,
|
|
url,
|
|
path: normalizeText(record.path) || undefined,
|
|
});
|
|
}
|
|
|
|
return segments;
|
|
}
|
|
|
|
export function parseAgentPsdTemplates(value: unknown) {
|
|
if (!Array.isArray(value)) return [] as AgentPsdTemplate[];
|
|
|
|
const templates: AgentPsdTemplate[] = [];
|
|
|
|
for (const entry of value) {
|
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
continue;
|
|
}
|
|
|
|
const record = entry as Record<string, unknown>;
|
|
const id = normalizeText(record.id);
|
|
const name = normalizeText(record.name);
|
|
const brandName = normalizeText(record.brandName) || "未分配品牌";
|
|
const fileName = normalizeText(record.fileName);
|
|
const fileUrl = normalizeUrl(record.fileUrl);
|
|
const previewUrl = normalizeUrl(record.previewUrl);
|
|
const createdAt = toIsoDateString(record.createdAt);
|
|
const updatedAt = toIsoDateString(record.updatedAt) || createdAt;
|
|
const width = normalizePositiveInteger(record.width);
|
|
const height = normalizePositiveInteger(record.height);
|
|
const layers = parseAgentPsdTemplateLayers(record.layers);
|
|
const layerCount = Math.max(layers.length, normalizePositiveInteger(record.layerCount));
|
|
|
|
if (
|
|
!id ||
|
|
!name ||
|
|
!fileName ||
|
|
!fileUrl ||
|
|
!previewUrl ||
|
|
!createdAt ||
|
|
!updatedAt ||
|
|
width <= 0 ||
|
|
height <= 0
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
const parseStatus = normalizeAgentPsdTemplateParseStatus(record.parseStatus);
|
|
const rawParseProgress = normalizePositiveInteger(record.parseProgress);
|
|
const parseStartedAt = toIsoDateString(record.parseStartedAt) || undefined;
|
|
const parseHeartbeatAt = toIsoDateString(record.parseHeartbeatAt) || undefined;
|
|
|
|
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,
|
|
parseProgress:
|
|
parseStatus === "completed" && rawParseProgress <= 0
|
|
? 100
|
|
: Math.min(100, Math.max(0, rawParseProgress)),
|
|
parseStage: normalizeText(record.parseStage) || undefined,
|
|
parseError: normalizeText(record.parseError) || undefined,
|
|
parseStartedAt,
|
|
parseHeartbeatAt,
|
|
fileName,
|
|
fileUrl,
|
|
filePath: normalizeText(record.filePath) || undefined,
|
|
previewUrl,
|
|
previewPath: normalizeText(record.previewPath) || undefined,
|
|
renderBaseUrl:
|
|
normalizeOptionalUrl(record.renderBaseUrl) || normalizeOptionalUrl(record.previewUrl),
|
|
renderBasePath: normalizeText(record.renderBasePath) || undefined,
|
|
renderSegments: parseAgentPsdTemplateRenderSegments(record.renderSegments),
|
|
width,
|
|
height,
|
|
layerCount,
|
|
layers,
|
|
assetVersion: normalizePositiveInteger(record.assetVersion),
|
|
createdAt,
|
|
updatedAt,
|
|
uploadedById: normalizeText(record.uploadedById) || undefined,
|
|
});
|
|
}
|
|
|
|
return templates.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
}
|
|
|
|
export function upsertAgentPsdTemplate(
|
|
templates: AgentPsdTemplate[],
|
|
template: AgentPsdTemplate
|
|
) {
|
|
return [template, ...templates.filter((entry) => entry.id !== template.id)].sort((left, right) =>
|
|
right.updatedAt.localeCompare(left.updatedAt)
|
|
);
|
|
}
|
|
|
|
export function isAgentPsdTemplateReady(template: AgentPsdTemplate) {
|
|
return template.parseStatus === "completed" && template.layers.length > 0;
|
|
}
|
|
|
|
export function hasAgentPsdTemplateStoredLayerSource(
|
|
layer: Pick<AgentPsdTemplateLayerSlot, "sourceUrl" | "sourcePath">
|
|
) {
|
|
return Boolean(
|
|
String(layer.sourcePath || "").trim() || String(layer.sourceUrl || "").trim()
|
|
);
|
|
}
|
|
|
|
export function hasAgentPsdTemplateRenderSegments(
|
|
template: Pick<AgentPsdTemplate, "renderSegments">
|
|
) {
|
|
return Boolean(
|
|
template.renderSegments?.some((segment) => String(segment.url || "").trim())
|
|
);
|
|
}
|
|
|
|
export function hasAgentPsdTemplateDedicatedRenderBase(
|
|
template: Pick<AgentPsdTemplate, "renderBaseUrl" | "renderBasePath" | "previewUrl">
|
|
) {
|
|
const renderBaseUrl = String(template.renderBaseUrl || "").trim();
|
|
const previewUrl = String(template.previewUrl || "").trim();
|
|
return Boolean(String(template.renderBasePath || "").trim()) || (
|
|
Boolean(renderBaseUrl) &&
|
|
renderBaseUrl !== previewUrl
|
|
);
|
|
}
|
|
|
|
export function getAgentPsdTemplateRuntimeMissingLayers(template: AgentPsdTemplate) {
|
|
const renderBaseUrl = String(template.renderBaseUrl || "").trim();
|
|
const hasUsableRenderBase =
|
|
hasAgentPsdTemplateDedicatedRenderBase(template) &&
|
|
Boolean(renderBaseUrl) &&
|
|
renderBaseUrl !== String(template.previewUrl || "").trim();
|
|
|
|
return template.layers.filter(
|
|
(layer) => {
|
|
if (hasUsableRenderBase) {
|
|
return (
|
|
isAgentPsdImageLayerSlot(layer) &&
|
|
isAgentPsdTemplateLayerUserEditable(layer) &&
|
|
!hasAgentPsdTemplateStoredLayerSource(layer)
|
|
);
|
|
}
|
|
|
|
return !hasAgentPsdTemplateStoredLayerSource(layer);
|
|
}
|
|
);
|
|
}
|
|
|
|
export function getAgentPsdTemplateRuntimeIssue(template: AgentPsdTemplate) {
|
|
if (!isAgentPsdTemplateReady(template)) {
|
|
return "PSD 模板尚未完成解析,请先在后台重新解析后再使用";
|
|
}
|
|
|
|
const missingLayers = getAgentPsdTemplateRuntimeMissingLayers(template);
|
|
if (missingLayers.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const layerNames = Array.from(
|
|
new Set(missingLayers.map((layer) => String(layer.layerName || "").trim()).filter(Boolean))
|
|
);
|
|
const summarizedLayerNames =
|
|
layerNames.length <= 3
|
|
? layerNames.join("、")
|
|
: `${layerNames.slice(0, 3).join("、")} 等 ${layerNames.length} 个图层`;
|
|
|
|
return summarizedLayerNames
|
|
? `PSD 模板图层资源不完整(${summarizedLayerNames}),请在后台重新解析该模板后再导出`
|
|
: "PSD 模板图层资源不完整,请在后台重新解析该模板后再导出";
|
|
}
|
|
|
|
export function isAgentPsdTemplateRuntimeReady(template: AgentPsdTemplate) {
|
|
return getAgentPsdTemplateRuntimeIssue(template) === null;
|
|
}
|
|
|
|
export function getEditableAgentPsdTemplateLayers(template: AgentPsdTemplate) {
|
|
return template.layers.filter(isAgentPsdTemplateLayerUserEditable);
|
|
}
|
|
|
|
export function isAgentPsdLayerSuperLocked(
|
|
layer: Pick<AgentPsdTemplateBaseLayerSlot, "isSuperLocked">
|
|
) {
|
|
return layer.isSuperLocked === true;
|
|
}
|
|
|
|
export function isAgentPsdTemplateLayerUserEditable(
|
|
layer: AgentPsdTemplateLayerSlot
|
|
) {
|
|
return layer.isEditable && !isAgentPsdLayerSuperLocked(layer);
|
|
}
|
|
|
|
export function isAgentPsdTextLayerSlot(
|
|
layer: AgentPsdTemplateLayerSlot
|
|
): layer is AgentPsdTemplateTextLayerSlot {
|
|
return layer.slotType === "text";
|
|
}
|
|
|
|
export function isAgentPsdImageLayerSlot(
|
|
layer: AgentPsdTemplateLayerSlot
|
|
): layer is AgentPsdTemplateImageLayerSlot {
|
|
return layer.slotType === "image";
|
|
}
|