hulai-website/server/api/admin/upload.post.js
Mimingguang ee078b61bc feat: 后台管理系统完整重构 + 前后台数据联通
- 后台全面引入 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>
2026-03-24 17:48:02 +08:00

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 }
})