125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
||
import multer from 'multer';
|
||
import sharp from 'sharp';
|
||
import path from 'path';
|
||
import fs from 'fs';
|
||
import { authMiddleware } from '../admin/auth.service';
|
||
import { updateSystemConfig } from '../admin/system-config.service';
|
||
|
||
const router = Router();
|
||
|
||
// ============ Upload ============
|
||
|
||
/**
|
||
* POST /api/admin/upload-fallback-image
|
||
* Upload a fallback cover image for search results without covers.
|
||
* Recommended: 320×180 JPEG/PNG (16:9), max 2MB.
|
||
*/
|
||
const uploadDir = path.resolve('/app/uploads/fallback');
|
||
if (!fs.existsSync(uploadDir)) {
|
||
fs.mkdirSync(uploadDir, { recursive: true });
|
||
}
|
||
|
||
const fallbackStorage = multer.diskStorage({
|
||
destination: (_req, _file, cb) => cb(null, uploadDir),
|
||
filename: (_req, _file, cb) => {
|
||
const ext = '.jpg';
|
||
cb(null, `fallback_cover_tmp${ext}`);
|
||
},
|
||
});
|
||
|
||
const upload = multer({
|
||
storage: fallbackStorage,
|
||
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max
|
||
fileFilter: (_req, file, cb) => {
|
||
if (file.mimetype.startsWith('image/')) {
|
||
cb(null, true);
|
||
} else {
|
||
cb(new Error('仅支持图片文件(JPEG/PNG)'));
|
||
}
|
||
},
|
||
});
|
||
|
||
router.post('/admin/upload-fallback-image', authMiddleware, upload.single('image'), async (req: Request, res: Response) => {
|
||
try {
|
||
if (!req.file) {
|
||
res.status(400).json({ error: '请选择要上传的图片' });
|
||
return;
|
||
}
|
||
// 压缩:最大宽度320px,JPEG quality 80
|
||
const outPath = path.resolve(uploadDir, 'fallback_cover.jpg');
|
||
await sharp(req.file.path)
|
||
.resize(320, undefined, { fit: 'inside', withoutEnlargement: true })
|
||
.jpeg({ quality: 80 })
|
||
.toFile(outPath);
|
||
// 删除原始上传文件(如果路径不同)
|
||
if (req.file.path !== outPath) {
|
||
fs.unlink(req.file.path, () => {});
|
||
}
|
||
const url = `/api/uploads/fallback/fallback_cover.jpg`;
|
||
updateSystemConfig('search_fallback_image', url);
|
||
const stat = fs.statSync(outPath);
|
||
res.json({ success: true, url, message: `✅ 兜底图已压缩上传 (${(stat.size / 1024).toFixed(1)}KB)` });
|
||
} catch (err: any) {
|
||
res.status(500).json({ error: err.message || '上传失败' });
|
||
}
|
||
});
|
||
|
||
/**
|
||
* POST /api/admin/upload-logo
|
||
* Upload a site logo image displayed on search page (home link) and homepage.
|
||
* Recommended: 320×60 or similar wide/banner ratio, JPEG/PNG/WebP, max 2MB.
|
||
*/
|
||
const logoUploadDir = path.resolve('/app/uploads/logo');
|
||
if (!fs.existsSync(logoUploadDir)) {
|
||
fs.mkdirSync(logoUploadDir, { recursive: true });
|
||
}
|
||
|
||
const logoStorage = multer.diskStorage({
|
||
destination: (_req, _file, cb) => cb(null, logoUploadDir),
|
||
filename: (_req, _file, cb) => {
|
||
cb(null, `site_logo_tmp.png`);
|
||
},
|
||
});
|
||
|
||
const logoUpload = multer({
|
||
storage: logoStorage,
|
||
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max
|
||
fileFilter: (_req, file, cb) => {
|
||
if (file.mimetype.startsWith('image/')) {
|
||
cb(null, true);
|
||
} else {
|
||
cb(new Error('仅支持图片文件(JPEG/PNG/WebP)'));
|
||
}
|
||
},
|
||
});
|
||
|
||
router.post('/admin/upload-logo', authMiddleware, logoUpload.single('image'), async (req: Request, res: Response) => {
|
||
try {
|
||
if (!req.file) {
|
||
res.status(400).json({ error: '请选择要上传的图片' });
|
||
return;
|
||
}
|
||
// 压缩:最大宽度640px,PNG格式
|
||
const outPath = path.resolve(logoUploadDir, 'site_logo.png');
|
||
await sharp(req.file.path)
|
||
.resize(640, undefined, { fit: 'inside', withoutEnlargement: true })
|
||
.png({ compressionLevel: 9 })
|
||
.toFile(outPath);
|
||
if (req.file.path !== outPath) {
|
||
fs.unlink(req.file.path, () => {});
|
||
}
|
||
const url = `/api/uploads/logo/site_logo.png`;
|
||
updateSystemConfig('site_logo', url);
|
||
const stat = fs.statSync(outPath);
|
||
res.json({ success: true, url, message: `✅ 站点图标已压缩上传 (${(stat.size / 1024).toFixed(1)}KB)` });
|
||
} catch (err: any) {
|
||
res.status(500).json({ error: err.message || '上传失败' });
|
||
}
|
||
});
|
||
|
||
import { startQrLogin, getQrLoginStatus, cancelQrLogin } from '../cloud/qr-login.service';
|
||
|
||
// ===== 夸克扫码登录 (不需要 auth,用户未登录时也需要能用) =====
|
||
|
||
export default router; |