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

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

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

485 lines
12 KiB
TypeScript
Raw Permalink 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.

/**
* 分类管理与模板的双向同步
*
* 功能:
* - 分类管理修改选项时同步到模板
* - 模板修改选项时同步到字段池
* - 删除字段时警告影响范围
* - 使用版本号检测同步冲突
* - 同步失败时回滚
* - 记录同步历史日志
*
* @created 2024-12-13
*/
import { prisma } from '@/lib/prisma'
// 类型定义
export interface SyncResult {
success: boolean
syncedCount: number
conflictCount: number
errors: Array<{ source: string; target: string; error: string }>
rollbackPerformed: boolean
}
export interface SyncLogEntry {
id: string
timestamp: Date
operation: 'add' | 'update' | 'delete' | 'sync'
source: 'category' | 'template'
fieldKey: string
optionValue?: string
details: string
success: boolean
error?: string
}
// 内存中的同步日志(生产环境应该持久化到数据库)
const syncLogs: SyncLogEntry[] = []
const MAX_LOG_ENTRIES = 1000
/**
* 记录同步日志
*/
function logSync(entry: Omit<SyncLogEntry, 'id' | 'timestamp'>): void {
const logEntry: SyncLogEntry = {
id: `sync_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date(),
...entry,
}
syncLogs.unshift(logEntry)
// 限制日志数量
if (syncLogs.length > MAX_LOG_ENTRIES) {
syncLogs.pop()
}
// 输出到控制台(开发环境)
if (process.env.NODE_ENV === 'development') {
console.log(`[CategorySync] ${entry.operation} ${entry.source}:${entry.fieldKey}`, entry.details)
}
}
/**
* 获取同步日志
*/
export function getSyncLogs(limit = 100): SyncLogEntry[] {
return syncLogs.slice(0, limit)
}
/**
* 清除同步日志
*/
export function clearSyncLogs(): void {
syncLogs.length = 0
}
/**
* 从分类管理同步选项到模板
* 当在分类管理中添加/修改/删除选项时调用
*/
export async function syncOptionToTemplates(
fieldKey: string,
operation: 'add' | 'update' | 'delete',
optionValue: string,
newValue?: string
): Promise<SyncResult> {
const result: SyncResult = {
success: true,
syncedCount: 0,
conflictCount: 0,
errors: [],
rollbackPerformed: false,
}
try {
// 获取所有使用该字段的模板字段
const templateFields = await prisma.uploadField.findMany({
where: { key: fieldKey },
include: {
template: true,
},
})
if (templateFields.length === 0) {
logSync({
operation,
source: 'category',
fieldKey,
optionValue,
details: '没有找到使用该字段的模板',
success: true,
})
return result
}
for (const field of templateFields) {
try {
// 解析现有选项
let options: string[] = []
if (field.optionsStatic) {
try {
const parsed = JSON.parse(field.optionsStatic)
options = Array.isArray(parsed) ? parsed : []
} catch {
options = []
}
}
let updated = false
let newOptions = [...options]
switch (operation) {
case 'add':
if (!options.includes(optionValue)) {
newOptions.push(optionValue)
updated = true
}
break
case 'update':
if (newValue && options.includes(optionValue)) {
newOptions = options.map(opt => opt === optionValue ? newValue : opt)
updated = true
}
break
case 'delete':
if (options.includes(optionValue)) {
newOptions = options.filter(opt => opt !== optionValue)
updated = true
}
break
}
if (updated) {
// 检查版本冲突
const currentField = await prisma.uploadField.findUnique({
where: { id: field.id },
})
if (currentField && currentField.updatedAt > field.updatedAt) {
result.conflictCount++
result.errors.push({
source: `category:${fieldKey}`,
target: `template:${field.templateId}:${field.key}`,
error: '版本冲突:模板字段已被其他操作修改',
})
continue
}
// 更新模板字段
await prisma.uploadField.update({
where: { id: field.id },
data: {
optionsStatic: JSON.stringify(newOptions),
updatedAt: new Date(),
},
})
result.syncedCount++
}
} catch (error) {
result.errors.push({
source: `category:${fieldKey}`,
target: `template:${field.templateId}:${field.key}`,
error: error instanceof Error ? error.message : '未知错误',
})
}
}
logSync({
operation,
source: 'category',
fieldKey,
optionValue,
details: `同步到 ${result.syncedCount} 个模板字段,${result.conflictCount} 个冲突`,
success: result.errors.length === 0,
error: result.errors.length > 0 ? result.errors[0].error : undefined,
})
result.success = result.errors.length === 0
} catch (error) {
result.success = false
result.errors.push({
source: `category:${fieldKey}`,
target: 'templates',
error: error instanceof Error ? error.message : '未知错误',
})
logSync({
operation,
source: 'category',
fieldKey,
optionValue,
details: '同步失败',
success: false,
error: error instanceof Error ? error.message : '未知错误',
})
}
return result
}
/**
* 从模板同步选项到字段池
* 当在模板管理中修改字段选项时调用
*/
export async function syncOptionToFieldPool(
fieldKey: string,
options: string[]
): Promise<SyncResult> {
const result: SyncResult = {
success: true,
syncedCount: 0,
conflictCount: 0,
errors: [],
rollbackPerformed: false,
}
try {
// 获取字段池中的字段
const fieldDef = await prisma.fieldDefinition.findUnique({
where: { key: fieldKey },
include: {
options: {
orderBy: { orderIndex: 'asc' },
},
},
})
if (!fieldDef) {
// 字段不存在,创建新字段
await prisma.fieldDefinition.create({
data: {
key: fieldKey,
label: fieldKey,
type: 'select',
source: 'template',
isStandard: false,
isFilterEnabled: false,
filterOrder: 0,
},
})
}
// 获取现有选项
const existingOptions = fieldDef?.options || []
const existingValues = new Set(existingOptions.map(o => o.value))
// 获取字段ID(可能是新创建的)
const field = await prisma.fieldDefinition.findUnique({
where: { key: fieldKey },
select: { id: true },
})
if (!field) {
throw new Error(`字段 ${fieldKey} 不存在`)
}
// 添加新选项
let orderIndex = existingOptions.length
for (const optionValue of options) {
if (!existingValues.has(optionValue)) {
await prisma.fieldOption.create({
data: {
fieldId: field.id,
value: optionValue,
label: optionValue,
orderIndex: orderIndex++,
},
})
result.syncedCount++
}
}
logSync({
operation: 'sync',
source: 'template',
fieldKey,
details: `同步 ${result.syncedCount} 个新选项到字段池`,
success: true,
})
result.success = true
} catch (error) {
result.success = false
result.errors.push({
source: `template:${fieldKey}`,
target: `fieldPool:${fieldKey}`,
error: error instanceof Error ? error.message : '未知错误',
})
logSync({
operation: 'sync',
source: 'template',
fieldKey,
details: '同步到字段池失败',
success: false,
error: error instanceof Error ? error.message : '未知错误',
})
}
return result
}
/**
* 获取字段删除的影响范围
*/
export async function getFieldDeletionImpact(fieldKey: string): Promise<{
templateCount: number
templateNames: string[]
optionCount: number
canDelete: boolean
warning: string
}> {
// 获取使用该字段的模板
const templateFields = await prisma.uploadField.findMany({
where: { key: fieldKey },
include: {
template: {
select: { name: true },
},
},
})
const templateNames = templateFields.map(f => f.template.name)
const templateCount = templateNames.length
// 获取字段定义
const fieldDef = await prisma.fieldDefinition.findUnique({
where: { key: fieldKey },
select: { id: true },
})
// 获取选项数量
const optionCount = fieldDef
? await prisma.fieldOption.count({
where: { fieldId: fieldDef.id },
})
: 0
// 判断是否可以删除
const canDelete = templateCount === 0
// 生成警告信息
let warning = ''
if (templateCount > 0) {
warning = `该字段被 ${templateCount} 个模板使用(${templateNames.slice(0, 3).join('、')}${templateCount > 3 ? '等' : ''}),删除后这些模板将失去该字段。`
}
if (optionCount > 0) {
warning += warning ? '\n' : ''
warning += `该字段有 ${optionCount} 个选项将被一并删除。`
}
return {
templateCount,
templateNames,
optionCount,
canDelete,
warning,
}
}
/**
* 执行带回滚的同步操作
*/
export async function syncWithRollback<T>(
operation: () => Promise<T>,
rollback: () => Promise<void>
): Promise<{ success: boolean; result?: T; error?: string; rollbackPerformed: boolean }> {
try {
const result = await operation()
return { success: true, result, rollbackPerformed: false }
} catch (error) {
// 尝试回滚
try {
await rollback()
return {
success: false,
error: error instanceof Error ? error.message : '未知错误',
rollbackPerformed: true,
}
} catch (rollbackError) {
return {
success: false,
error: `操作失败: ${error instanceof Error ? error.message : '未知错误'}; 回滚也失败: ${rollbackError instanceof Error ? rollbackError.message : '未知错误'}`,
rollbackPerformed: false,
}
}
}
}
/**
* 批量同步所有模板字段到字段池
*/
export async function syncAllTemplatesToFieldPool(): Promise<SyncResult> {
const result: SyncResult = {
success: true,
syncedCount: 0,
conflictCount: 0,
errors: [],
rollbackPerformed: false,
}
try {
// 获取所有模板字段
const templateFields = await prisma.uploadField.findMany({
where: {
type: 'select',
optionsStatic: { not: null },
},
})
// 按字段key分组
const fieldOptionsMap = new Map<string, Set<string>>()
for (const field of templateFields) {
if (!field.optionsStatic) continue
try {
const options = JSON.parse(field.optionsStatic)
if (!Array.isArray(options)) continue
const existing = fieldOptionsMap.get(field.key) || new Set()
options.forEach((opt: string) => {
if (typeof opt === 'string' && opt.trim()) {
existing.add(opt.trim())
}
})
fieldOptionsMap.set(field.key, existing)
} catch {
// 忽略解析错误
}
}
// 同步到字段池
for (const [fieldKey, options] of Array.from(fieldOptionsMap.entries())) {
const syncResult = await syncOptionToFieldPool(fieldKey, Array.from(options))
result.syncedCount += syncResult.syncedCount
result.errors.push(...syncResult.errors)
}
logSync({
operation: 'sync',
source: 'template',
fieldKey: '*',
details: `批量同步完成,共同步 ${result.syncedCount} 个选项`,
success: result.errors.length === 0,
})
result.success = result.errors.length === 0
} catch (error) {
result.success = false
result.errors.push({
source: 'templates',
target: 'fieldPool',
error: error instanceof Error ? error.message : '未知错误',
})
}
return result
}