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

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

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

419 lines
9.9 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.

/**
* 分类管理批量操作工具
*
* 支持以下批量操作:
* - 批量启用/禁用字段
* - 批量删除字段
* - 批量导入选项(CSV/JSON)
* - 批量导出选项
*
* @created 2024-12-13
*/
import { prisma } from '@/lib/prisma'
// 类型定义
export interface BatchOperationResult {
success: boolean
processed: number
failed: number
errors: Array<{ key: string; error: string }>
}
export interface ImportedOption {
fieldKey: string
value: string
label?: string
}
export interface ExportedField {
key: string
label: string
filterDisplayName: string | null
filterOrder: number
isFilterEnabled: boolean
options: Array<{ value: string; label: string | null }>
}
/**
* 批量启用/禁用字段
*/
export async function batchToggleFields(
fieldKeys: string[],
enabled: boolean
): Promise<BatchOperationResult> {
const result: BatchOperationResult = {
success: true,
processed: 0,
failed: 0,
errors: [],
}
for (const key of fieldKeys) {
try {
await prisma.fieldDefinition.update({
where: { key },
data: {
isFilterEnabled: enabled,
updatedAt: new Date(),
},
})
result.processed++
} catch (error) {
result.failed++
result.errors.push({
key,
error: error instanceof Error ? error.message : '未知错误',
})
}
}
result.success = result.failed === 0
return result
}
/**
* 批量删除字段(仅删除字段池中的记录,不影响模板)
*/
export async function batchDeleteFields(
fieldKeys: string[]
): Promise<BatchOperationResult> {
const result: BatchOperationResult = {
success: true,
processed: 0,
failed: 0,
errors: [],
}
for (const key of fieldKeys) {
try {
// 先获取字段ID
const field = await prisma.fieldDefinition.findUnique({
where: { key },
select: { id: true },
})
if (field) {
// 先删除关联的选项(使用 fieldId)
await prisma.fieldOption.deleteMany({
where: { fieldId: field.id },
})
// 再删除字段定义
await prisma.fieldDefinition.delete({
where: { key },
})
}
result.processed++
} catch (error) {
result.failed++
result.errors.push({
key,
error: error instanceof Error ? error.message : '未知错误',
})
}
}
result.success = result.failed === 0
return result
}
/**
* 解析CSV格式的选项数据
* 格式:fieldKey,value,label(label可选)
*/
export function parseCSVOptions(csvContent: string): ImportedOption[] {
const lines = csvContent.split('\n').filter(line => line.trim())
const options: ImportedOption[] = []
// 跳过标题行(如果有)
const startIndex = lines[0]?.toLowerCase().includes('fieldkey') ? 1 : 0
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) continue
// 处理CSV中的引号
const parts = parseCSVLine(line)
if (parts.length >= 2) {
options.push({
fieldKey: parts[0].trim(),
value: parts[1].trim(),
label: parts[2]?.trim() || undefined,
})
}
}
return options
}
/**
* 解析单行CSV(处理引号)
*/
function parseCSVLine(line: string): string[] {
const result: string[] = []
let current = ''
let inQuotes = false
for (let i = 0; i < line.length; i++) {
const char = line[i]
if (char === '"') {
if (inQuotes && line[i + 1] === '"') {
current += '"'
i++
} else {
inQuotes = !inQuotes
}
} else if (char === ',' && !inQuotes) {
result.push(current)
current = ''
} else {
current += char
}
}
result.push(current)
return result
}
/**
* 解析JSON格式的选项数据
*/
export function parseJSONOptions(jsonContent: string): ImportedOption[] {
try {
const data = JSON.parse(jsonContent)
// 支持两种格式:
// 1. 数组格式:[{ fieldKey, value, label? }, ...]
// 2. 对象格式:{ fieldKey: [{ value, label? }, ...], ... }
if (Array.isArray(data)) {
return data.filter(item => item.fieldKey && item.value).map(item => ({
fieldKey: String(item.fieldKey),
value: String(item.value),
label: item.label ? String(item.label) : undefined,
}))
}
if (typeof data === 'object' && data !== null) {
const options: ImportedOption[] = []
for (const [fieldKey, values] of Object.entries(data)) {
if (Array.isArray(values)) {
for (const item of values) {
if (typeof item === 'string') {
options.push({ fieldKey, value: item })
} else if (item && typeof item === 'object' && item.value) {
options.push({
fieldKey,
value: String(item.value),
label: item.label ? String(item.label) : undefined,
})
}
}
}
}
return options
}
return []
} catch {
return []
}
}
/**
* 批量导入选项
*/
export async function batchImportOptions(
options: ImportedOption[],
mode: 'merge' | 'replace' = 'merge'
): Promise<BatchOperationResult> {
const result: BatchOperationResult = {
success: true,
processed: 0,
failed: 0,
errors: [],
}
// 按字段分组
const optionsByField = new Map<string, ImportedOption[]>()
for (const option of options) {
const existing = optionsByField.get(option.fieldKey) || []
existing.push(option)
optionsByField.set(option.fieldKey, existing)
}
for (const [fieldKey, fieldOptions] of Array.from(optionsByField.entries())) {
try {
// 检查字段是否存在
const field = await prisma.fieldDefinition.findUnique({
where: { key: fieldKey },
})
if (!field) {
result.failed += fieldOptions.length
result.errors.push({
key: fieldKey,
error: `字段 ${fieldKey} 不存在`,
})
continue
}
if (mode === 'replace') {
// 替换模式:先删除所有现有选项
await prisma.fieldOption.deleteMany({
where: { fieldId: field.id },
})
}
// 获取现有选项(用于去重和确定orderIndex)
const existingOptions = await prisma.fieldOption.findMany({
where: { fieldId: field.id },
orderBy: { orderIndex: 'desc' },
})
const existingValues = new Set(existingOptions.map(o => o.value))
let nextOrderIndex = (existingOptions[0]?.orderIndex ?? -1) + 1
// 添加新选项
for (const option of fieldOptions) {
if (mode === 'merge' && existingValues.has(option.value)) {
// 合并模式下跳过已存在的选项
continue
}
await prisma.fieldOption.create({
data: {
fieldId: field.id,
value: option.value,
label: option.label || '',
orderIndex: nextOrderIndex++,
},
})
result.processed++
}
} catch (error) {
result.failed += fieldOptions.length
result.errors.push({
key: fieldKey,
error: error instanceof Error ? error.message : '未知错误',
})
}
}
result.success = result.failed === 0
return result
}
/**
* 导出所有字段和选项
*/
export async function exportAllFields(): Promise<ExportedField[]> {
const fields = await prisma.fieldDefinition.findMany({
orderBy: [
{ filterOrder: 'asc' },
{ label: 'asc' },
],
include: {
options: {
orderBy: { orderIndex: 'asc' },
},
},
})
return fields.map(field => ({
key: field.key,
label: field.label,
filterDisplayName: field.filterDisplayName,
filterOrder: field.filterOrder,
isFilterEnabled: field.isFilterEnabled,
options: field.options.map(opt => ({
value: opt.value,
label: opt.label,
})),
}))
}
/**
* 导出为CSV格式
*/
export function exportToCSV(fields: ExportedField[]): string {
const lines: string[] = ['fieldKey,value,label']
for (const field of fields) {
for (const option of field.options) {
const escapedValue = escapeCSVField(option.value)
const escapedLabel = escapeCSVField(option.label || '')
lines.push(`${field.key},${escapedValue},${escapedLabel}`)
}
}
return lines.join('\n')
}
/**
* 导出为JSON格式
*/
export function exportToJSON(fields: ExportedField[]): string {
return JSON.stringify(fields, null, 2)
}
/**
* 转义CSV字段
*/
function escapeCSVField(value: string): string {
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`
}
return value
}
/**
* 获取字段的影响范围(用于删除前警告)
*/
export async function getFieldImpactInfo(fieldKey: string): Promise<{
templateCount: number
materialCount: number
optionCount: number
}> {
// 获取使用该字段的模板数量
const templateCount = await prisma.uploadField.count({
where: { key: fieldKey },
})
// 获取字段定义
const field = await prisma.fieldDefinition.findUnique({
where: { key: fieldKey },
select: { id: true },
})
// 获取该字段的选项数量
const optionCount = field
? await prisma.fieldOption.count({
where: { fieldId: field.id },
})
: 0
// 获取使用该字段的素材数量
// 注意:这是一个近似值,只统计标准字段
let materialCount = 0
try {
// 对于标准字段,直接查询
const standardFields = ['categoryName', 'sceneName', 'productModel', 'renderer', 'brandName', 'seriesName']
if (standardFields.includes(fieldKey)) {
materialCount = await prisma.material.count({
where: {
[fieldKey]: { not: '' },
},
})
}
} catch {
materialCount = 0
}
return {
templateCount,
materialCount,
optionCount,
}
}