export interface StandardModelWhiteBgView { key: StandardModelWhiteBgKey label: string fieldName: string } export type StandardModelWhiteBgKey = | 'leftFront45' | 'rightFront45' | 'leftBack45' | 'rightBack45' | 'front' | 'back' | 'left' | 'right' export const STANDARD_MODEL_WHITE_BG_VIEWS: StandardModelWhiteBgView[] = [ { key: 'leftFront45', label: '左前45°', fieldName: 'whiteBgLeftFront45' }, { key: 'rightFront45', label: '右前45°', fieldName: 'whiteBgRightFront45' }, { key: 'leftBack45', label: '左后45°', fieldName: 'whiteBgLeftBack45' }, { key: 'rightBack45', label: '右后45°', fieldName: 'whiteBgRightBack45' }, { key: 'front', label: '正视图', fieldName: 'whiteBgFront' }, { key: 'back', label: '后视图', fieldName: 'whiteBgBack' }, { key: 'left', label: '左视图', fieldName: 'whiteBgLeft' }, { key: 'right', label: '右视图', fieldName: 'whiteBgRight' }, ] export const STANDARD_MODEL_WHITE_BG_OTHER_FIELD_NAME = 'whiteBgOtherImages' export const LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE = 'standard-model-white-bg-zip' export const LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX = 'standard-model-white-bg-zip:' export const STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX = 'standard-model-white-bg-zip-v2:' export const STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX = 'standard-model-white-bg-view:' export const STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX = 'standard-model-white-bg-other:' export interface ParsedStandardModelWhiteBgAssetType { kind: 'view' | 'other' | 'zip' skuId: string viewKey?: StandardModelWhiteBgKey index?: number } export interface WhiteBackgroundAssetLike { assetType: string sortOrder?: number | string | bigint | null } export interface GroupedWhiteBackgroundAssets { skuId: string fixed: Partial> other: T[] zipAssets: T[] } export function getWhiteBackgroundFieldName(key: StandardModelWhiteBgKey): string { const target = STANDARD_MODEL_WHITE_BG_VIEWS.find((item) => item.key === key) return target ? target.fieldName : `whiteBg${key}` } export function getMissingWhiteBackgroundViews( files: Partial>, ): StandardModelWhiteBgKey[] { return STANDARD_MODEL_WHITE_BG_VIEWS .map((item) => item.key) .filter((key) => !files[key]) } export function hasCompleteWhiteBackgroundSet( files: Partial>, ): boolean { return getMissingWhiteBackgroundViews(files).length === 0 } export function normalizeWhiteBackgroundSkuName(value: string): string { return String(value || '').replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase().trim() } function sanitizeWhiteBackgroundFilePart(value: string): string { return String(value || '') .replace(/[\/\\]/g, '-') .replace(/[<>:"|?*]/g, '') .replace(/\s+/g, '') .trim() } export function buildWhiteBackgroundFixedFileName( skuName: string, productCategoryName: string, viewLabel: string, ext: string, ): string { const normalizedSkuName = normalizeWhiteBackgroundSkuName(skuName) const safeCategoryName = sanitizeWhiteBackgroundFilePart(productCategoryName) const safeViewLabel = sanitizeWhiteBackgroundFilePart(viewLabel) const normalizedExt = ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}` const baseName = [normalizedSkuName, safeCategoryName, safeViewLabel].filter(Boolean).join('-') return `${baseName || 'white-background'}${normalizedExt}` } export function getCurrentWhiteBackgroundAssetType(skuId: string): string { return `${STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX}${skuId}` } export function getWhiteBackgroundViewAssetType( skuId: string, key: StandardModelWhiteBgKey, ): string { return `${STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX}${skuId}:${key}` } export function getWhiteBackgroundOtherAssetType(skuId: string, index: number): string { return `${STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX}${skuId}:${index}` } export function getLegacyWhiteBackgroundAssetTypes(skuId: string): string[] { return [ LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE, `${LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX}${skuId}`, ] } export function getAllWhiteBackgroundAssetTypesForSku(skuId: string): string[] { return [ getCurrentWhiteBackgroundAssetType(skuId), ...getLegacyWhiteBackgroundAssetTypes(skuId), ] } export function getWhiteBackgroundViewAssetPrefixForSku(skuId: string): string { return `${STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX}${skuId}:` } export function getWhiteBackgroundOtherAssetPrefixForSku(skuId: string): string { return `${STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX}${skuId}:` } export function getWhiteBackgroundAssetWhereForSku(skuId: string) { return { OR: [ { assetType: getCurrentWhiteBackgroundAssetType(skuId) }, { assetType: LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE }, { assetType: `${LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX}${skuId}` }, { assetType: { startsWith: getWhiteBackgroundViewAssetPrefixForSku(skuId) } }, { assetType: { startsWith: getWhiteBackgroundOtherAssetPrefixForSku(skuId) } }, ], } } export function getWhiteBackgroundRelativeDirectory(materialId: string, skuId: string): string { return ['uploads', 'skus', materialId, 'white-background', skuId].join('/') } export function replaceWhiteBackgroundFileExtension(fileName: string, ext: string = '.webp'): string { const normalizedExt = ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}` const normalizedFileName = String(fileName || '').trim().replace(/\\/g, '/') if (!normalizedFileName) { return `white-background${normalizedExt}` } const lastSlashIndex = normalizedFileName.lastIndexOf('/') const directory = lastSlashIndex >= 0 ? normalizedFileName.slice(0, lastSlashIndex + 1) : '' const rawBaseName = lastSlashIndex >= 0 ? normalizedFileName.slice(lastSlashIndex + 1) : normalizedFileName const nextBaseName = rawBaseName.replace(/\.[^/.]+$/, '') || 'white-background' return `${directory}${nextBaseName}${normalizedExt}` } export function getWhiteBackgroundRelativeFilePath( materialId: string, skuId: string, fileName: string, ): string { return `${getWhiteBackgroundRelativeDirectory(materialId, skuId)}/${fileName}`.replace(/\\/g, '/') } export function getWhiteBackgroundPreviewRelativeDirectory(materialId: string, skuId: string): string { return `${getWhiteBackgroundRelativeDirectory(materialId, skuId)}/preview`.replace(/\\/g, '/') } export function getWhiteBackgroundPreviewRelativeFilePath( materialId: string, skuId: string, fileName: string, ): string { return `${getWhiteBackgroundPreviewRelativeDirectory(materialId, skuId)}/${replaceWhiteBackgroundFileExtension(fileName)}`.replace(/\\/g, '/') } export function getWhiteBackgroundOtherPreviewRelativeFilePath( materialId: string, skuId: string, fileName: string, ): string { return `${getWhiteBackgroundPreviewRelativeDirectory(materialId, skuId)}/other/${replaceWhiteBackgroundFileExtension(fileName)}`.replace(/\\/g, '/') } export function getWhiteBackgroundPreviewRelativePathFromAssetPath(fileUrl: string): string | null { const normalizedFileUrl = String(fileUrl || '').trim().replace(/\\/g, '/') if (!normalizedFileUrl || /^https?:\/\//i.test(normalizedFileUrl)) { return null } const normalizedRelativePath = normalizedFileUrl.replace(/^\.?\//, '') const segments = normalizedRelativePath.split('/').filter(Boolean) const whiteBackgroundIndex = segments.indexOf('white-background') if (whiteBackgroundIndex < 0 || !segments[whiteBackgroundIndex + 1]) { return null } const skuRootSegments = segments.slice(0, whiteBackgroundIndex + 2) const assetSegments = segments.slice(whiteBackgroundIndex + 2) if (assetSegments.length === 0) { return null } if (assetSegments[0] === 'preview') { return normalizedRelativePath } if (assetSegments[0] === 'other') { const otherFileName = assetSegments.slice(1).join('/') if (!otherFileName) { return null } return `${skuRootSegments.join('/')}/preview/other/${replaceWhiteBackgroundFileExtension(otherFileName)}`.replace(/\\/g, '/') } return `${skuRootSegments.join('/')}/preview/${replaceWhiteBackgroundFileExtension(assetSegments.join('/'))}`.replace(/\\/g, '/') } export function getWhiteBackgroundZipFileName(skuName: string): string { return `${skuName}_白底图.zip` } export function countWhiteBackgroundFixedViews( group?: Pick, 'fixed'> | null, ): number { if (!group) { return 0 } return STANDARD_MODEL_WHITE_BG_VIEWS.filter((view) => Boolean(group.fixed[view.key])).length } export function countWhiteBackgroundOtherImages( group?: Pick, 'other'> | null, ): number { return group?.other?.length || 0 } export function countWhiteBackgroundAssets( group?: Pick, 'fixed' | 'other' | 'zipAssets'> | null, ): number { if (!group) { return 0 } return ( countWhiteBackgroundFixedViews(group) + countWhiteBackgroundOtherImages(group) + (group.zipAssets?.length || 0) ) } export function hasAnyWhiteBackgroundAssetGroup( group?: Pick, 'fixed' | 'other' | 'zipAssets'> | null, ): boolean { return countWhiteBackgroundAssets(group) > 0 } export function getMissingWhiteBackgroundViewsFromAssetGroup( group?: Pick, 'fixed'> | null, ): StandardModelWhiteBgView[] { if (!group) { return [...STANDARD_MODEL_WHITE_BG_VIEWS] } return STANDARD_MODEL_WHITE_BG_VIEWS.filter((view) => !group.fixed[view.key]) } export function parseStandardModelWhiteBgAssetType( assetType: string, ): ParsedStandardModelWhiteBgAssetType | null { const trimmed = String(assetType || '').trim() if (!trimmed) { return null } if (trimmed.startsWith(STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX)) { const remainder = trimmed.slice(STANDARD_MODEL_WHITE_BG_VIEW_ASSET_TYPE_PREFIX.length) const separatorIndex = remainder.indexOf(':') if (separatorIndex <= 0) { return null } const skuId = remainder.slice(0, separatorIndex) const viewKey = remainder.slice(separatorIndex + 1) as StandardModelWhiteBgKey if (!STANDARD_MODEL_WHITE_BG_VIEWS.some((view) => view.key === viewKey)) { return null } return { kind: 'view', skuId, viewKey, } } if (trimmed.startsWith(STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX)) { const remainder = trimmed.slice(STANDARD_MODEL_WHITE_BG_OTHER_ASSET_TYPE_PREFIX.length) const separatorIndex = remainder.indexOf(':') if (separatorIndex <= 0) { return null } const skuId = remainder.slice(0, separatorIndex) const index = Number(remainder.slice(separatorIndex + 1)) if (!Number.isInteger(index) || index <= 0) { return null } return { kind: 'other', skuId, index, } } if (trimmed === LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE) { return { kind: 'zip', skuId: '', } } if (trimmed.startsWith(LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX)) { return { kind: 'zip', skuId: trimmed.slice(LEGACY_STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX.length), } } if (trimmed.startsWith(STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX)) { return { kind: 'zip', skuId: trimmed.slice(STANDARD_MODEL_WHITE_BG_ASSET_TYPE_PREFIX.length), } } return null } export function isWhiteBackgroundAssetType(assetType: string): boolean { return parseStandardModelWhiteBgAssetType(assetType) !== null } function getAssetSortOrder(asset: WhiteBackgroundAssetLike): number { const raw = asset.sortOrder if (typeof raw === 'bigint') { return Number(raw) } if (typeof raw === 'string') { const parsed = Number(raw) return Number.isFinite(parsed) ? parsed : 0 } if (typeof raw === 'number') { return Number.isFinite(raw) ? raw : 0 } return 0 } export function groupWhiteBackgroundAssetsBySku( assets: T[], ): Map> { const groups = new Map>() for (const asset of assets) { const parsed = parseStandardModelWhiteBgAssetType(asset.assetType) if (!parsed) { continue } const groupSkuId = parsed.skuId if (!groupSkuId) { continue } const current = groups.get(groupSkuId) || { skuId: groupSkuId, fixed: {}, other: [], zipAssets: [], } if (parsed.kind === 'view' && parsed.viewKey) { current.fixed[parsed.viewKey] = asset } else if (parsed.kind === 'other') { current.other.push(asset) } else if (parsed.kind === 'zip') { current.zipAssets.push(asset) } groups.set(groupSkuId, current) } groups.forEach((group) => { group.other.sort((left, right) => getAssetSortOrder(left) - getAssetSortOrder(right)) group.zipAssets.sort((left, right) => getAssetSortOrder(left) - getAssetSortOrder(right)) }) return groups } export function hasCompleteWhiteBackgroundAssetGroup( group?: Pick, 'fixed'> | null, ): boolean { if (!group) { return false } return STANDARD_MODEL_WHITE_BG_VIEWS.every((view) => Boolean(group.fixed[view.key])) } export function getNextWhiteBackgroundOtherIndex( group?: Pick, 'other'> | null, ): number { if (!group?.other?.length) { return 1 } let maxIndex = 0 for (const asset of group.other) { const parsed = parseStandardModelWhiteBgAssetType(asset.assetType) if (parsed?.kind === 'other' && parsed.index && parsed.index > maxIndex) { maxIndex = parsed.index } } return maxIndex + 1 }