Some checks failed
CI Check / check (push) Has been cancelled
- 创建统一的Modal组件,采用Apple风格设计 - 实现毛玻璃效果、圆角、动画和细边框描边 - 修复弹窗打开时背景滚动问题 - 修复分类管理中重复卡片和数组字符串显示问题 - 优化选项解析逻辑,添加去重和过滤机制
247 lines
7.7 KiB
TypeScript
247 lines
7.7 KiB
TypeScript
/**
|
||
* 模板分类 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 }
|
||
)
|
||
}
|
||
}
|