- 后台全面引入 Naive UI 组件库,统一 UI 规范 - 前台 usePublicApi 切换到 API 调用(GET /api/content/[key]) - 前后台数据源统一(site_content 表) - 产品线统一管理(tour/camp/course 三套编辑器) - 产品数据归一化(itinerary/faq/pricing 格式统一) - 表单提交 API 改写入 submissions 表 - 新增 submissions 标记已读 API - site-content PUT 支持 upsert - 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb) - 消除 PageHero 类型警告(useSEO reactive 修复) - 25 个前台页面全部 HTTP 200,展示效果不变 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
59 行
1.9 KiB
JavaScript
59 行
1.9 KiB
JavaScript
import { writeFileSync, existsSync, mkdirSync } from 'fs'
|
|
import { join, extname } from 'path'
|
|
import { requireAdmin } from '../../utils/auth.js'
|
|
|
|
// 允许的图片类型
|
|
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml']
|
|
const MAX_SIZE = 10 * 1024 * 1024 // 10MB
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
requireAdmin(event)
|
|
|
|
const formData = await readMultipartFormData(event)
|
|
if (!formData || !formData.length) {
|
|
throw createError({ statusCode: 422, message: '请选择要上传的文件' })
|
|
}
|
|
|
|
const results = []
|
|
|
|
for (const file of formData) {
|
|
if (!file.filename || !file.data) continue
|
|
|
|
// 校验文件类型
|
|
const mimeType = file.type || ''
|
|
if (!ALLOWED_TYPES.includes(mimeType)) {
|
|
throw createError({ statusCode: 422, message: `不支持的文件类型: ${mimeType},仅支持 JPG/PNG/WebP/GIF/SVG` })
|
|
}
|
|
|
|
// 校验文件大小
|
|
if (file.data.length > MAX_SIZE) {
|
|
throw createError({ statusCode: 422, message: `文件过大,最大 10MB` })
|
|
}
|
|
|
|
// 生成文件名: 时间戳-原始文件名
|
|
const ext = extname(file.filename).toLowerCase()
|
|
const safeName = file.filename
|
|
.replace(ext, '')
|
|
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '_')
|
|
.slice(0, 50)
|
|
const fileName = `${Date.now()}-${safeName}${ext}`
|
|
|
|
// 按年月分目录
|
|
const now = new Date()
|
|
const subDir = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}`
|
|
const uploadDir = join(process.cwd(), 'public', 'uploads', subDir)
|
|
|
|
if (!existsSync(uploadDir)) {
|
|
mkdirSync(uploadDir, { recursive: true })
|
|
}
|
|
|
|
const filePath = join(uploadDir, fileName)
|
|
writeFileSync(filePath, file.data)
|
|
|
|
const url = `/uploads/${subDir}/${fileName}`
|
|
results.push({ url, name: file.filename, size: file.data.length })
|
|
}
|
|
|
|
return { success: true, files: results }
|
|
})
|