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` }) } // 生成文件名: 时间戳-原始文件名 // 只保留 ASCII 字母数字下划线短横,中文/特殊字符一律去掉,避免 URL 编码与静态服务文件名不一致导致 404 const ext = extname(file.filename).toLowerCase() const stripped = file.filename .replace(ext, '') .replace(/[^a-zA-Z0-9_-]/g, '') .slice(0, 30) const safeName = stripped || 'img' 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 } })