WuYongJie 8705b9846a
Some checks failed
CI Check / check (push) Has been cancelled
feat: 统一弹窗样式系统与分类管理优化
- 创建统一的Modal组件,采用Apple风格设计
- 实现毛玻璃效果、圆角、动画和细边框描边
- 修复弹窗打开时背景滚动问题
- 修复分类管理中重复卡片和数组字符串显示问题
- 优化选项解析逻辑,添加去重和过滤机制
2025-11-25 09:37:00 +08:00

247 lines
7.7 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.

/**
* 模板分类 API
*
* 从所有模板中提取勾选了"添加到分类"的下拉字段
* 相同 fieldKey 的字段只显示一个卡片,多个模板共享
* 同时包含标准字段模板中设置了 addToCategory 的字段
*/
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import {
BUILTIN_TEMPLATE_KEYS,
STANDARD_FIELDS,
createBuiltinTemplate,
} from '@/lib/upload-template-presets'
import { getManagedCategoryFields } from '@/lib/category-fields'
// 检查是否是数组字符串的辅助函数
function isArrayString(str: string): boolean {
const trimmed = str.trim()
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
return false
}
try {
const parsed = JSON.parse(trimmed)
return Array.isArray(parsed)
} catch {
return false
}
}
// 解析选项的辅助函数
function parseOptions(optionsStatic: string | null): string[] {
if (!optionsStatic) return []
// 先尝试直接解析为 JSON
try {
const parsed = JSON.parse(optionsStatic)
if (Array.isArray(parsed)) {
// 解析数组中的每个选项,如果选项本身是字符串化的数组,再次解析
const result: string[] = []
parsed.forEach(opt => {
const optStr = String(opt).trim()
if (!optStr) return
// 检查是否是字符串化的数组(如 "[\"默认店铺\"]")
if (isArrayString(optStr)) {
try {
const nestedParsed = JSON.parse(optStr)
if (Array.isArray(nestedParsed)) {
// 如果是数组,展开其中的元素,但排除数组字符串本身
nestedParsed.forEach(item => {
const itemStr = String(item).trim()
// 再次检查,避免嵌套的数组字符串
if (itemStr && !isArrayString(itemStr)) {
result.push(itemStr)
}
})
}
// 如果是数组字符串,不添加它本身
} catch {
// 解析失败,不添加
}
} else {
// 不是数组字符串,正常添加
result.push(optStr)
}
})
return result.filter(Boolean)
}
// 如果解析后不是数组,但原始字符串看起来像数组字符串,尝试再次解析
if (typeof parsed === 'string' && isArrayString(parsed)) {
try {
const doubleParsed = JSON.parse(parsed)
if (Array.isArray(doubleParsed)) {
return doubleParsed
.map(opt => String(opt).trim())
.filter(opt => opt && !isArrayString(opt))
}
} catch {
// 忽略
}
}
} catch {
// 如果不是 JSON,尝试按行分割
return optionsStatic
.split('\n')
.map(line => line.trim())
.filter(line => line && !isArrayString(line))
}
return []
}
export async function GET() {
try {
const managedFieldSet = new Set(await getManagedCategoryFields())
const includeField = (fieldKey: string) => managedFieldSet.has(fieldKey)
// 使用 Map 按 fieldKey 去重,相同 fieldKey 的字段合并到一个分类
const categoryMap = new Map<string, {
fieldKey: string
fieldLabel: string
options: Set<string>
}>()
const fieldLabelFallback = new Map<string, string>()
// 首先添加标准字段模板中设置了 addToCategory 的字段
Object.values(STANDARD_FIELDS).forEach(field => {
if (field.addToCategory === true && includeField(field.key)) {
const options = parseOptions(field.optionsStatic ?? null)
if (!categoryMap.has(field.key)) {
categoryMap.set(field.key, {
fieldKey: field.key,
fieldLabel: field.label,
options: new Set<string>(),
})
}
const entry = categoryMap.get(field.key)
options.forEach(opt => {
// 在添加到 Set 之前就过滤掉数组字符串
if (opt && !isArrayString(opt)) {
entry?.options.add(opt)
}
})
}
})
// 合并所有预制模板(新版本仅内存定义)
BUILTIN_TEMPLATE_KEYS.forEach(key => {
const template = createBuiltinTemplate(key)
template.fields.forEach(field => {
if (field.addToCategory && includeField(field.key)) {
const options = parseOptions(field.optionsStatic ?? null)
if (!categoryMap.has(field.key)) {
categoryMap.set(field.key, {
fieldKey: field.key,
fieldLabel: field.label,
options: new Set<string>(),
})
}
const entry = categoryMap.get(field.key)
options.forEach(opt => {
// 在添加到 Set 之前就过滤掉数组字符串
if (opt && !isArrayString(opt)) {
entry?.options.add(opt)
}
})
}
})
})
// 获取所有启用的模板及其字段
const templates = await prisma.uploadTemplate.findMany({
where: {
status: 'active',
},
include: {
fields: {
where: {
addToCategory: true,
},
orderBy: { orderIndex: 'asc' },
},
},
})
// 遍历所有模板的字段,合并到分类Map中
templates.forEach(template => {
template.fields.forEach(field => {
if (includeField(field.key)) {
fieldLabelFallback.set(field.key, field.label)
const options = parseOptions(field.optionsStatic ?? null)
const existing = categoryMap.get(field.key)
if (existing) {
options.forEach(opt => {
if (typeof opt === 'string' && opt.trim() && !isArrayString(opt.trim())) {
existing.options.add(opt.trim())
}
})
} else {
// 过滤掉数组字符串后再创建 Set
const filteredOptions = options.filter(opt => opt && !isArrayString(opt))
categoryMap.set(field.key, {
fieldKey: field.key,
fieldLabel: field.label,
options: new Set<string>(filteredOptions),
})
}
}
})
})
const resolveLabel = (fieldKey: string) =>
STANDARD_FIELDS[fieldKey]?.label ||
fieldLabelFallback.get(fieldKey) ||
fieldKey
managedFieldSet.forEach(fieldKey => {
if (!categoryMap.has(fieldKey)) {
categoryMap.set(fieldKey, {
fieldKey,
fieldLabel: resolveLabel(fieldKey),
options: new Set<string>(),
})
}
})
// 转换为数组格式并排序,确保去重
const categories = Array.from(categoryMap.values()).map(cat => {
// 使用 Set 再次去重,确保没有重复选项
// 同时过滤掉看起来像数组字符串的选项(如 "[\"默认店铺\"]")
const uniqueOptions = Array.from(new Set(cat.options))
.filter(opt => {
const trimmed = opt.trim()
// 排除看起来像数组字符串的选项
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
try {
const parsed = JSON.parse(trimmed)
// 如果是数组,说明这是数组字符串,应该被过滤掉
if (Array.isArray(parsed)) {
return false
}
} catch {
// 如果解析失败,保留原选项
}
}
return true
})
return {
fieldKey: cat.fieldKey,
fieldLabel: cat.fieldLabel,
options: uniqueOptions.sort(),
}
})
return NextResponse.json(categories)
} catch (error) {
console.error('Error fetching template categories:', error)
return NextResponse.json(
{ error: '获取模板分类失败' },
{ status: 500 }
)
}
}