|
|
@ -2,51 +2,46 @@ import multer from 'multer'; |
|
|
import fs from 'node:fs'; |
|
|
import fs from 'node:fs'; |
|
|
import path from 'node:path'; |
|
|
import path from 'node:path'; |
|
|
import { callNodeListener } from 'h3'; |
|
|
import { callNodeListener } from 'h3'; |
|
|
|
|
|
import { |
|
|
|
|
|
RELATIVE_ASSETS_DIR, |
|
|
|
|
|
POST_MEDIA_PUBLIC_PREFIX, |
|
|
|
|
|
ALLOWED_MIME_TYPES, |
|
|
|
|
|
MAX_FILE_SIZE, |
|
|
|
|
|
MAX_FILE_COUNT, |
|
|
|
|
|
} from '#server/constants/upload'; |
|
|
|
|
|
import { getContextUser } from '#server/utils/context'; |
|
|
|
|
|
|
|
|
// 类型定义
|
|
|
|
|
|
interface IFile { |
|
|
interface IFile { |
|
|
name: string; |
|
|
name: string; |
|
|
url: string; // 前端可直接访问的 URL
|
|
|
url: string; |
|
|
path: string; // 服务器存储路径
|
|
|
|
|
|
mimeType: string; |
|
|
mimeType: string; |
|
|
size: number; |
|
|
size: number; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export default defineWrappedResponseHandler({ auth: 'optional' }, async (event) => { |
|
|
export default defineWrappedResponseHandler({ auth: 'required' }, async (event) => { |
|
|
try { |
|
|
const user = getContextUser(event)!; |
|
|
// 存储目录
|
|
|
|
|
|
const uploadDir = path.join(process.cwd(), 'public/assets'); |
|
|
const uploadDir = path.resolve(RELATIVE_ASSETS_DIR); |
|
|
|
|
|
|
|
|
// 自动创建目录
|
|
|
|
|
|
if (!fs.existsSync(uploadDir)) { |
|
|
if (!fs.existsSync(uploadDir)) { |
|
|
fs.mkdirSync(uploadDir, { recursive: true }); |
|
|
fs.mkdirSync(uploadDir, { recursive: true }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 配置存储
|
|
|
|
|
|
const storage = multer.diskStorage({ |
|
|
const storage = multer.diskStorage({ |
|
|
destination: uploadDir, |
|
|
destination: uploadDir, |
|
|
filename: (req, file, cb) => { |
|
|
filename: (_req, file, cb) => { |
|
|
// 生成唯一文件名:时间戳 + 原始文件名(安全处理)
|
|
|
|
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); |
|
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); |
|
|
// 获取文件后缀
|
|
|
|
|
|
const ext = path.extname(file.originalname).toLowerCase(); |
|
|
const ext = path.extname(file.originalname).toLowerCase(); |
|
|
// 干净的文件名(只保留字母数字)
|
|
|
|
|
|
const baseName = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, '-'); |
|
|
const baseName = path.basename(file.originalname, ext).replace(/[^a-z0-9]/gi, '-'); |
|
|
// 最终文件名(带后缀)
|
|
|
cb(null, `${uniqueSuffix}-${baseName}${ext}`); |
|
|
const filename = `${uniqueSuffix}-${baseName}${ext}`; |
|
|
|
|
|
cb(null, filename); |
|
|
|
|
|
}, |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// 上传配置
|
|
|
|
|
|
const upload = multer({ |
|
|
const upload = multer({ |
|
|
storage, |
|
|
storage, |
|
|
limits: { |
|
|
limits: { fileSize: MAX_FILE_SIZE }, |
|
|
fileSize: 5 * 1024 * 1024, // 5MB 限制
|
|
|
fileFilter: (_req, file, cb) => { |
|
|
}, |
|
|
if ((ALLOWED_MIME_TYPES as readonly string[]).includes(file.mimetype)) { |
|
|
fileFilter: (req, file, cb) => { |
|
|
|
|
|
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/webp']; |
|
|
|
|
|
if (allowedTypes.includes(file.mimetype)) { |
|
|
|
|
|
cb(null, true); |
|
|
cb(null, true); |
|
|
} else { |
|
|
} else { |
|
|
cb(new Error('只支持 PNG/JPG/WebP 格式图片')); |
|
|
cb(new Error('只支持 PNG/JPG/WebP 格式图片')); |
|
|
@ -54,33 +49,22 @@ export default defineWrappedResponseHandler({ auth: 'optional' }, async (event) |
|
|
}, |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// 执行上传(最多 10 个文件)
|
|
|
|
|
|
await callNodeListener( |
|
|
await callNodeListener( |
|
|
// @ts-expect-error Nuxt 类型兼容
|
|
|
// @ts-expect-error Nuxt 类型兼容
|
|
|
upload.array('file', 10), |
|
|
upload.array('file', MAX_FILE_COUNT), |
|
|
event.node.req, |
|
|
event.node.req, |
|
|
event.node.res |
|
|
event.node.res, |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
// 获取上传后的文件
|
|
|
|
|
|
// @ts-expect-error
|
|
|
// @ts-expect-error
|
|
|
const uploadedFiles = event.node.req.files || []; |
|
|
const uploadedFiles = event.node.req.files || []; |
|
|
|
|
|
|
|
|
// 格式化返回数据
|
|
|
|
|
|
const result: IFile[] = uploadedFiles.map((file: any) => ({ |
|
|
const result: IFile[] = uploadedFiles.map((file: any) => ({ |
|
|
name: file.originalname, |
|
|
name: file.originalname, |
|
|
url: `/public/assets/${file.filename}`, // ✅ 前端可直接访问
|
|
|
url: `${POST_MEDIA_PUBLIC_PREFIX}${file.filename}`, |
|
|
mimeType: file.mimetype, |
|
|
mimeType: file.mimetype, |
|
|
size: file.size, |
|
|
size: file.size, |
|
|
})); |
|
|
})); |
|
|
|
|
|
|
|
|
return result; |
|
|
return result; |
|
|
|
|
|
|
|
|
} catch (err: any) { |
|
|
|
|
|
console.error('上传失败:', err); |
|
|
|
|
|
return createError({ |
|
|
|
|
|
statusCode: 400, |
|
|
|
|
|
statusMessage: err.message || '上传失败', |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
}); |
|
|
|