1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
262 lines
6.4 KiB
TypeScript
262 lines
6.4 KiB
TypeScript
/**
|
||
* 自定义字段数据存储和读取工具
|
||
*
|
||
* 功能:
|
||
* - 将自定义字段key映射到Material模型的标准字段
|
||
* - 非标准字段存储到metadata JSON字段
|
||
* - 读取时合并标准字段和metadata数据
|
||
*/
|
||
|
||
/**
|
||
* Material模型的标准字段列表
|
||
* 这些字段直接存储在数据库列中
|
||
*/
|
||
export const STANDARD_MATERIAL_FIELDS = [
|
||
'productModel',
|
||
'brandName',
|
||
'seriesName',
|
||
'skuCode',
|
||
'materialTexture',
|
||
'categoryName',
|
||
'sceneName',
|
||
'materialType',
|
||
'materialKind',
|
||
'renderer',
|
||
'renderTime',
|
||
'imageResolution',
|
||
'colorMode',
|
||
'authorName',
|
||
'platformName',
|
||
] as const
|
||
|
||
export type StandardMaterialField = typeof STANDARD_MATERIAL_FIELDS[number]
|
||
|
||
/**
|
||
* 检查字段key是否为标准字段
|
||
*/
|
||
export function isStandardField(fieldKey: string): fieldKey is StandardMaterialField {
|
||
return STANDARD_MATERIAL_FIELDS.includes(fieldKey as StandardMaterialField)
|
||
}
|
||
|
||
/**
|
||
* 字段值类型
|
||
*/
|
||
export type FieldValue = string | number | boolean | null | undefined
|
||
|
||
/**
|
||
* 分离标准字段和自定义字段
|
||
*/
|
||
export function separateFieldValues(
|
||
templateFields: Record<string, FieldValue>
|
||
): {
|
||
standardFields: Partial<Record<StandardMaterialField, string>>
|
||
customFields: Record<string, FieldValue>
|
||
} {
|
||
const standardFields: Partial<Record<StandardMaterialField, string>> = {}
|
||
const customFields: Record<string, FieldValue> = {}
|
||
|
||
for (const [key, value] of Object.entries(templateFields)) {
|
||
if (isStandardField(key)) {
|
||
// 标准字段转换为字符串存储
|
||
standardFields[key] = value === null || value === undefined ? '' : String(value)
|
||
} else {
|
||
// 自定义字段保留原始类型
|
||
customFields[key] = value
|
||
}
|
||
}
|
||
|
||
return { standardFields, customFields }
|
||
}
|
||
|
||
/**
|
||
* 构建metadata JSON字符串
|
||
*/
|
||
export function buildMetadata(
|
||
customFields: Record<string, FieldValue>,
|
||
templateId?: string | null,
|
||
previewUrls?: string[],
|
||
existingMetadata?: string | null
|
||
): string | null {
|
||
// 解析现有metadata
|
||
let metadataObj: Record<string, unknown> = {}
|
||
if (existingMetadata) {
|
||
try {
|
||
metadataObj = JSON.parse(existingMetadata)
|
||
} catch {
|
||
// 忽略解析错误
|
||
}
|
||
}
|
||
|
||
// 更新templateId
|
||
if (templateId) {
|
||
metadataObj.templateId = templateId
|
||
}
|
||
|
||
// 更新自定义字段
|
||
if (Object.keys(customFields).length > 0) {
|
||
metadataObj.templateFields = {
|
||
...(metadataObj.templateFields as Record<string, unknown> || {}),
|
||
...customFields,
|
||
}
|
||
}
|
||
|
||
// 更新预览图URLs
|
||
if (previewUrls && previewUrls.length > 0) {
|
||
metadataObj.previewUrls = previewUrls
|
||
}
|
||
|
||
// 如果没有任何数据,返回null
|
||
if (Object.keys(metadataObj).length === 0) {
|
||
return null
|
||
}
|
||
|
||
return JSON.stringify(metadataObj)
|
||
}
|
||
|
||
/**
|
||
* 解析metadata JSON字符串
|
||
*/
|
||
export function parseMetadata(metadata: string | null | undefined): {
|
||
templateId?: string
|
||
templateFields: Record<string, FieldValue>
|
||
previewUrls?: string[]
|
||
} {
|
||
if (!metadata) {
|
||
return { templateFields: {} }
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(metadata)
|
||
return {
|
||
templateId: parsed.templateId,
|
||
templateFields: parsed.templateFields || {},
|
||
previewUrls: parsed.previewUrls,
|
||
}
|
||
} catch {
|
||
return { templateFields: {} }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从Material记录中读取所有字段值(合并标准字段和metadata)
|
||
*/
|
||
export function readAllFieldValues(
|
||
material: Record<string, unknown>
|
||
): Record<string, FieldValue> {
|
||
const result: Record<string, FieldValue> = {}
|
||
|
||
// 读取标准字段
|
||
for (const field of STANDARD_MATERIAL_FIELDS) {
|
||
const value = material[field]
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
result[field] = value as FieldValue
|
||
}
|
||
}
|
||
|
||
// 读取metadata中的自定义字段
|
||
const metadata = parseMetadata(material.metadata as string | null)
|
||
for (const [key, value] of Object.entries(metadata.templateFields)) {
|
||
// 自定义字段不覆盖标准字段
|
||
if (!isStandardField(key)) {
|
||
result[key] = value
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
/**
|
||
* 准备Material创建/更新数据
|
||
*/
|
||
export function prepareMaterialData(
|
||
templateFields: Record<string, FieldValue>,
|
||
templateId?: string | null,
|
||
previewUrls?: string[],
|
||
existingMetadata?: string | null
|
||
): {
|
||
standardData: Partial<Record<StandardMaterialField, string>>
|
||
metadata: string | null
|
||
} {
|
||
const { standardFields, customFields } = separateFieldValues(templateFields)
|
||
const metadata = buildMetadata(customFields, templateId, previewUrls, existingMetadata)
|
||
|
||
return {
|
||
standardData: standardFields,
|
||
metadata,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查metadata大小是否超过限制
|
||
*/
|
||
export function checkMetadataSize(
|
||
metadata: string | null,
|
||
maxSizeBytes: number = 65535 // 默认64KB,适合TEXT类型
|
||
): { ok: boolean; size: number; maxSize: number } {
|
||
const size = metadata ? new TextEncoder().encode(metadata).length : 0
|
||
return {
|
||
ok: size <= maxSizeBytes,
|
||
size,
|
||
maxSize: maxSizeBytes,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 压缩metadata(移除空值和默认值)
|
||
*/
|
||
export function compressMetadata(metadata: string | null): string | null {
|
||
if (!metadata) return null
|
||
|
||
try {
|
||
const parsed = JSON.parse(metadata)
|
||
|
||
// 移除空的templateFields
|
||
if (parsed.templateFields) {
|
||
const cleanedFields: Record<string, unknown> = {}
|
||
for (const [key, value] of Object.entries(parsed.templateFields)) {
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
cleanedFields[key] = value
|
||
}
|
||
}
|
||
if (Object.keys(cleanedFields).length > 0) {
|
||
parsed.templateFields = cleanedFields
|
||
} else {
|
||
delete parsed.templateFields
|
||
}
|
||
}
|
||
|
||
// 移除空的previewUrls
|
||
if (parsed.previewUrls && Array.isArray(parsed.previewUrls) && parsed.previewUrls.length === 0) {
|
||
delete parsed.previewUrls
|
||
}
|
||
|
||
if (Object.keys(parsed).length === 0) {
|
||
return null
|
||
}
|
||
|
||
return JSON.stringify(parsed)
|
||
} catch {
|
||
return metadata
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取字段的存储位置
|
||
*/
|
||
export function getFieldStorageLocation(fieldKey: string): 'standard' | 'metadata' {
|
||
return isStandardField(fieldKey) ? 'standard' : 'metadata'
|
||
}
|
||
|
||
/**
|
||
* 批量获取字段存储位置
|
||
*/
|
||
export function getFieldsStorageLocations(
|
||
fieldKeys: string[]
|
||
): Map<string, 'standard' | 'metadata'> {
|
||
const result = new Map<string, 'standard' | 'metadata'>()
|
||
for (const key of fieldKeys) {
|
||
result.set(key, getFieldStorageLocation(key))
|
||
}
|
||
return result
|
||
}
|