flnxi-Wu/lib/field-migration.ts
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

384 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 字段迁移工具
*
* 功能:
* - 字段key修改的数据迁移
* - 字段类型转换验证
* - 批量更新素材字段结构
* - 字段恢复功能
*/
import { parseMetadata, buildMetadata, isStandardField, FieldValue } from './field-data-storage'
/**
* 字段迁移操作类型
*/
export type MigrationOperation =
| { type: 'rename'; oldKey: string; newKey: string }
| { type: 'delete'; key: string }
| { type: 'restore'; key: string; defaultValue?: string }
| { type: 'typeChange'; key: string; oldType: string; newType: string }
/**
* 迁移结果
*/
export interface MigrationResult {
success: boolean
materialId: string
operation: MigrationOperation
oldValue?: unknown
newValue?: unknown
error?: string
}
/**
* 批量迁移结果
*/
export interface BatchMigrationResult {
totalCount: number
successCount: number
failedCount: number
skippedCount: number
results: MigrationResult[]
}
/**
* 字段类型兼容性检查
*/
export function checkTypeCompatibility(
oldType: string,
newType: string,
sampleValue?: unknown
): { compatible: boolean; warning?: string; canConvert: boolean } {
// 相同类型总是兼容
if (oldType === newType) {
return { compatible: true, canConvert: true }
}
// 定义类型转换规则
const conversionRules: Record<string, Record<string, { compatible: boolean; warning?: string }>> = {
text: {
textarea: { compatible: true },
number: { compatible: false, warning: '文本可能无法转换为数字' },
select: { compatible: true, warning: '文本值可能不在选项列表中' },
checkbox: { compatible: false, warning: '文本无法转换为布尔值' },
date: { compatible: false, warning: '文本格式可能不是有效日期' },
},
textarea: {
text: { compatible: true, warning: '多行文本将被截断为单行' },
number: { compatible: false, warning: '文本可能无法转换为数字' },
select: { compatible: true, warning: '文本值可能不在选项列表中' },
},
number: {
text: { compatible: true },
textarea: { compatible: true },
select: { compatible: true, warning: '数字值可能不在选项列表中' },
},
select: {
text: { compatible: true },
textarea: { compatible: true },
number: { compatible: false, warning: '选项值可能无法转换为数字' },
},
checkbox: {
text: { compatible: true },
select: { compatible: true, warning: '布尔值将转换为 "true" 或 "false"' },
},
date: {
text: { compatible: true },
textarea: { compatible: true },
},
}
const rules = conversionRules[oldType]?.[newType]
if (rules) {
return { ...rules, canConvert: rules.compatible }
}
// 默认不兼容
return {
compatible: false,
warning: `不支持从 ${oldType} 转换到 ${newType}`,
canConvert: false
}
}
/**
* 转换字段值类型
*/
export function convertFieldValue(
value: unknown,
fromType: string,
toType: string
): { success: boolean; value: unknown; error?: string } {
if (value === null || value === undefined) {
return { success: true, value: null }
}
const stringValue = String(value)
try {
switch (toType) {
case 'text':
case 'textarea':
return { success: true, value: stringValue }
case 'number':
const num = parseFloat(stringValue)
if (isNaN(num)) {
return { success: false, value: null, error: `无法将 "${stringValue}" 转换为数字` }
}
return { success: true, value: num }
case 'checkbox':
const boolValue = stringValue.toLowerCase() === 'true' || stringValue === '1'
return { success: true, value: boolValue }
case 'date':
const date = new Date(stringValue)
if (isNaN(date.getTime())) {
return { success: false, value: null, error: `无法将 "${stringValue}" 转换为日期` }
}
return { success: true, value: date.toISOString().split('T')[0] }
case 'select':
// select类型保持原值,由调用方验证是否在选项列表中
return { success: true, value: stringValue }
default:
return { success: true, value: stringValue }
}
} catch (error) {
return {
success: false,
value: null,
error: `转换失败: ${error instanceof Error ? error.message : String(error)}`
}
}
}
/**
* 重命名metadata中的字段key
*/
export function renameFieldInMetadata(
metadata: string | null,
oldKey: string,
newKey: string
): { metadata: string | null; oldValue: unknown; migrated: boolean } {
if (!metadata) {
return { metadata: null, oldValue: undefined, migrated: false }
}
const parsed = parseMetadata(metadata)
const templateFields = parsed.templateFields
if (!(oldKey in templateFields)) {
return { metadata, oldValue: undefined, migrated: false }
}
const oldValue = templateFields[oldKey]
delete templateFields[oldKey]
templateFields[newKey] = oldValue
const newMetadata = buildMetadata(
templateFields,
parsed.templateId,
parsed.previewUrls
)
return { metadata: newMetadata, oldValue, migrated: true }
}
/**
* 从metadata中删除字段
*/
export function deleteFieldFromMetadata(
metadata: string | null,
key: string
): { metadata: string | null; oldValue: unknown; deleted: boolean } {
if (!metadata) {
return { metadata: null, oldValue: undefined, deleted: false }
}
const parsed = parseMetadata(metadata)
const templateFields = parsed.templateFields
if (!(key in templateFields)) {
return { metadata, oldValue: undefined, deleted: false }
}
const oldValue = templateFields[key]
delete templateFields[key]
const newMetadata = buildMetadata(
templateFields,
parsed.templateId,
parsed.previewUrls
)
return { metadata: newMetadata, oldValue, deleted: true }
}
/**
* 恢复metadata中的字段(从备份或默认值)
*/
export function restoreFieldInMetadata(
metadata: string | null,
key: string,
value: FieldValue
): { metadata: string | null; restored: boolean } {
const parsed = parseMetadata(metadata)
const templateFields = parsed.templateFields
// 如果字段已存在,不覆盖
if (key in templateFields) {
return { metadata, restored: false }
}
templateFields[key] = value
const newMetadata = buildMetadata(
templateFields,
parsed.templateId,
parsed.previewUrls
)
return { metadata: newMetadata, restored: true }
}
/**
* 验证迁移操作
*/
export function validateMigrationOperation(
operation: MigrationOperation
): { valid: boolean; error?: string } {
switch (operation.type) {
case 'rename':
if (!operation.oldKey || !operation.newKey) {
return { valid: false, error: '重命名操作需要提供旧键名和新键名' }
}
if (operation.oldKey === operation.newKey) {
return { valid: false, error: '新键名不能与旧键名相同' }
}
// 检查是否尝试重命名标准字段
if (isStandardField(operation.oldKey)) {
return { valid: false, error: `不能重命名标准字段 "${operation.oldKey}"` }
}
break
case 'delete':
if (!operation.key) {
return { valid: false, error: '删除操作需要提供键名' }
}
if (isStandardField(operation.key)) {
return { valid: false, error: `不能删除标准字段 "${operation.key}"` }
}
break
case 'restore':
if (!operation.key) {
return { valid: false, error: '恢复操作需要提供键名' }
}
break
case 'typeChange':
if (!operation.key || !operation.oldType || !operation.newType) {
return { valid: false, error: '类型转换操作需要提供键名、旧类型和新类型' }
}
const compatibility = checkTypeCompatibility(operation.oldType, operation.newType)
if (!compatibility.canConvert) {
return { valid: false, error: compatibility.warning || '类型不兼容' }
}
break
}
return { valid: true }
}
/**
* 生成迁移预览
*/
export function generateMigrationPreview(
materials: Array<{ id: string; metadata: string | null }>,
operation: MigrationOperation
): {
affectedCount: number
sampleChanges: Array<{ materialId: string; before: unknown; after: unknown }>
} {
const affectedMaterials: Array<{ materialId: string; before: unknown; after: unknown }> = []
for (const material of materials) {
const parsed = parseMetadata(material.metadata)
const templateFields = parsed.templateFields
switch (operation.type) {
case 'rename':
if (operation.oldKey in templateFields) {
affectedMaterials.push({
materialId: material.id,
before: { [operation.oldKey]: templateFields[operation.oldKey] },
after: { [operation.newKey]: templateFields[operation.oldKey] },
})
}
break
case 'delete':
if (operation.key in templateFields) {
affectedMaterials.push({
materialId: material.id,
before: { [operation.key]: templateFields[operation.key] },
after: { [operation.key]: undefined },
})
}
break
case 'restore':
if (!(operation.key in templateFields)) {
affectedMaterials.push({
materialId: material.id,
before: { [operation.key]: undefined },
after: { [operation.key]: operation.defaultValue },
})
}
break
case 'typeChange':
if (operation.key in templateFields) {
const oldValue = templateFields[operation.key]
const converted = convertFieldValue(oldValue, operation.oldType, operation.newType)
affectedMaterials.push({
materialId: material.id,
before: { [operation.key]: oldValue, type: operation.oldType },
after: { [operation.key]: converted.value, type: operation.newType },
})
}
break
}
}
return {
affectedCount: affectedMaterials.length,
sampleChanges: affectedMaterials.slice(0, 5), // 只返回前5个示例
}
}
/**
* 创建迁移回滚数据
*/
export function createRollbackData(
materialId: string,
operation: MigrationOperation,
originalMetadata: string | null
): {
materialId: string
operation: MigrationOperation
originalMetadata: string | null
timestamp: string
} {
return {
materialId,
operation,
originalMetadata,
timestamp: new Date().toISOString(),
}
}