feat: 字体修改#771

This commit is contained in:
bietiaop
2025-02-06 15:28:42 +08:00
parent d4556d9299
commit 9051b29565
10 changed files with 430 additions and 92 deletions

View File

@@ -0,0 +1,69 @@
import { Button } from '@heroui/button'
import { Input } from '@heroui/input'
import { useRef, useState } from 'react'
export interface FileInputProps {
onChange: (file: File) => Promise<void> | void
onDelete?: () => Promise<void> | void
label?: string
accept?: string
}
const FileInput: React.FC<FileInputProps> = ({
onChange,
onDelete,
label,
accept
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const [isLoading, setIsLoading] = useState(false)
return (
<div className="flex items-end gap-2">
<div className="flex-grow">
<Input
isDisabled={isLoading}
ref={inputRef}
label={label}
type="file"
placeholder="选择文件"
accept={accept}
onChange={async (e) => {
try {
setIsLoading(true)
const file = e.target.files?.[0]
if (file) {
await onChange(file)
}
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
if (inputRef.current) inputRef.current.value = ''
}
}}
/>
</div>
<Button
isDisabled={isLoading}
onPress={async () => {
try {
setIsLoading(true)
if (onDelete) await onDelete()
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
if (inputRef.current) inputRef.current.value = ''
}
}}
color="primary"
variant="flat"
size="sm"
>
</Button>
</div>
)
}
export default FileInput

View File

@@ -196,4 +196,26 @@ export default class FileManager {
)
return data.data
}
public static async uploadWebUIFont(file: File) {
const formData = new FormData()
formData.append('file', file)
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/File/font/upload/webui',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
)
return data.data
}
public static async deleteWebUIFont() {
const { data } = await serverRequest.post<ServerResponse<boolean>>(
'/File/font/delete/webui'
)
return data.data
}
}

View File

@@ -7,11 +7,13 @@ import toast from 'react-hot-toast'
import key from '@/const/key'
import SaveButtons from '@/components/button/save_buttons'
import FileInput from '@/components/input/file_input'
import ImageInput from '@/components/input/image_input'
import useMusic from '@/hooks/use-music'
import { siteConfig } from '@/config/site'
import FileManager from '@/controllers/file_manager'
const WebUIConfigCard = () => {
const {
@@ -59,17 +61,47 @@ const WebUIConfigCard = () => {
return (
<>
<title>WebUI配置 - NapCat WebUI</title>
<Controller
control={control}
name="musicListID"
render={({ field }) => (
<Input
{...field}
label="网易云音乐歌单ID网页内音乐播放器"
placeholder="请输入歌单ID"
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full">WebUI字体</div>
<div className="text-sm text-default-400">
<FileInput
label="中文字体"
onChange={async (file) => {
try {
await FileManager.uploadWebUIFont(file)
toast.success('上传成功')
setTimeout(() => {
window.location.reload()
}, 1000)
} catch (error) {
toast.error('上传失败: ' + (error as Error).message)
}
}}
onDelete={async () => {
try {
await FileManager.deleteWebUIFont()
} catch (error) {
toast.error('删除失败: ' + (error as Error).message)
}
}}
/>
)}
/>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full">WebUI音乐播放器</div>
<Controller
control={control}
name="musicListID"
render={({ field }) => (
<Input
{...field}
label="网易云音乐歌单ID网页内音乐播放器"
placeholder="请输入歌单ID"
/>
)}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex-shrink-0 w-full"></div>
<Controller

View File

@@ -10,9 +10,10 @@ import { WebUiConfigWrapper } from '@webapi/helper/config';
import { ALLRouter } from '@webapi/router';
import { cors } from '@webapi/middleware/cors';
import { createUrl } from '@webapi/utils/url';
import { sendSuccess } from '@webapi/utils/response';
import { sendError, sendSuccess } from '@webapi/utils/response';
import { join } from 'node:path';
import { terminalManager } from '@webapi/terminal/terminal_manager';
import multer from 'multer'; // 新增引入multer用于错误捕获
// 实例化Express
const app = express();
@@ -42,10 +43,22 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
// CORS中间件
// TODO:
app.use(cors);
// 如果是webui字体文件挂载字体文件
app.use('/webui/fonts/AaCute.woff', async (_req, res, next) => {
const isFontExist = await WebUiConfigWrapper.CheckWebUIFontExist();
console.log(isFontExist, 'isFontExist');
if (isFontExist) {
res.sendFile(WebUiConfigWrapper.GetWebUIFontPath());
} else {
next();
}
});
// ------------中间件结束------------
// ------------挂载路由------------
// 挂载静态路由(前端),路径为 [/前缀]/webui
// 挂载静态路由(前端),路径为 /webui
app.use('/webui', express.static(pathWrapper.staticPath));
// 初始化WebSocket服务器
server.on('upgrade', (request, socket, head) => {
@@ -64,7 +77,19 @@ export async function InitWebUi(logger: LogWrapper, pathWrapper: NapCatPathWrapp
app.all('/', (_req, res) => {
sendSuccess(res, null, 'NapCat WebAPI is now running!');
});
// ------------路由挂载结束------------
// 错误处理中间件捕获multer的错误
app.use((err: Error, _: express.Request, res: express.Response, next: express.NextFunction) => {
if (err instanceof multer.MulterError) {
return sendError(res, err.message, true);
}
next(err);
});
// 全局错误处理中间件非multer错误
app.use((_: Error, __: express.Request, res: express.Response, ___: express.NextFunction) => {
sendError(res, 'An unknown error occurred.', true);
});
// ------------启动服务------------
server.listen(config.port, config.host, async () => {

View File

@@ -1,4 +1,4 @@
import type { RequestHandler, Request } from 'express';
import type { RequestHandler } from 'express';
import { sendError, sendSuccess } from '../utils/response';
import fsProm from 'fs/promises';
import fs from 'fs';
@@ -7,7 +7,9 @@ import os from 'os';
import compressing from 'compressing';
import { PassThrough } from 'stream';
import multer from 'multer';
import { randomUUID } from 'crypto';
import { WebUiConfigWrapper } from '../helper/config';
import webUIFontUploader from '../uploader/webui_font';
import diskUploader from '../uploader/disk';
const isWindows = os.platform() === 'win32';
@@ -268,11 +270,11 @@ export const BatchMoveHandler: RequestHandler = async (req, res) => {
// 新增:文件下载处理方法(注意流式传输,不将整个文件读入内存)
export const DownloadHandler: RequestHandler = async (req, res) => {
try {
const filePath = normalizePath( req.query[ 'path' ] as string );
const filePath = normalizePath(req.query['path'] as string);
if (!filePath) {
return sendError( res, '参数错误' );
return sendError(res, '参数错误');
}
const stat = await fsProm.stat(filePath);
res.setHeader('Content-Type', 'application/octet-stream');
@@ -327,74 +329,71 @@ export const BatchDownloadHandler: RequestHandler = async (req, res) => {
}
};
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题
const decodeFileName = (fileName: string): string => {
// 修改上传处理方法
export const UploadHandler: RequestHandler = async (req, res) => {
try {
return Buffer.from(fileName, 'binary').toString('utf8');
} catch {
return fileName;
await diskUploader(req, res);
return sendSuccess(res, true, '文件上传成功', true);
} catch (error) {
let errorMessage = '文件上传失败';
if (error instanceof multer.MulterError) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
errorMessage = '文件大小超过限制40MB';
break;
case 'LIMIT_UNEXPECTED_FILE':
errorMessage = '无效的文件上传字段';
break;
default:
errorMessage = `上传错误: ${error.message}`;
}
} else if (error instanceof Error) {
errorMessage = error.message;
}
return sendError(res, errorMessage, true);
}
};
// 修改上传处理方法
export const UploadHandler: RequestHandler = (req, res) => {
const uploadPath = (req.query['path'] || '') as string;
// 上传WebUI字体文件处理方法
export const UploadWebUIFontHandler: RequestHandler = async (req, res) => {
try {
await webUIFontUploader(req, res);
return sendSuccess(res, true, '字体文件上传成功', true);
} catch (error) {
let errorMessage = '字体文件上传失败';
const storage = multer.diskStorage({
destination: (
_: Request,
file: Express.Multer.File,
cb: (error: Error | null, destination: string) => void
) => {
try {
const decodedName = decodeFileName(file.originalname);
if (!uploadPath) {
return cb(new Error('上传路径不能为空'), '');
}
if (isWindows && uploadPath === '\\') {
return cb(new Error('根目录不允许上传文件'), '');
}
// 处理文件夹上传的情况
if (decodedName.includes('/') || decodedName.includes('\\')) {
const fullPath = path.join(uploadPath, path.dirname(decodedName));
fs.mkdirSync(fullPath, { recursive: true });
cb(null, fullPath);
} else {
cb(null, uploadPath);
}
} catch (error) {
cb(error as Error, '');
if (error instanceof multer.MulterError) {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
errorMessage = '字体文件大小超过限制40MB';
break;
case 'LIMIT_UNEXPECTED_FILE':
errorMessage = '无效的文件上传字段';
break;
default:
errorMessage = `上传错误: ${error.message}`;
}
},
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
try {
const decodedName = decodeFileName(file.originalname);
const fileName = path.basename(decodedName);
// 检查文件是否存在
const fullPath = path.join(uploadPath, decodedName);
if (fs.existsSync(fullPath)) {
const ext = path.extname(fileName);
const name = path.basename(fileName, ext);
cb(null, `${name}-${randomUUID()}${ext}`);
} else {
cb(null, fileName);
}
} catch (error) {
cb(error as Error, '');
}
},
});
const upload = multer({ storage }).array('files');
upload(req, res, (err: any) => {
if (err) {
return sendError(res, err.message || '文件上传失败');
} else if (error instanceof Error) {
errorMessage = error.message;
}
return sendSuccess(res, true);
});
return sendError(res, errorMessage, true);
}
};
// 删除WebUI字体文件处理方法
export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
try {
const fontPath = WebUiConfigWrapper.GetWebUIFontPath();
const exists = await WebUiConfigWrapper.CheckWebUIFontExist();
if (!exists) {
return sendSuccess(res, true);
}
await fsProm.unlink(fontPath);
return sendSuccess(res, true);
} catch (error) {
return sendError(res, '删除字体文件失败');
}
};

View File

@@ -203,4 +203,32 @@ export class WebUiConfigWrapper {
}
return '';
}
// 获取字体文件夹内的字体列表
public static async GetFontList(): Promise<string[]> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
if (
await fs
.access(fontsPath, constants.F_OK)
.then(() => true)
.catch(() => false)
) {
return (await fs.readdir(fontsPath)).filter((file) => file.endsWith('.ttf'));
}
return [];
}
// 判断字体是否存在webui.woff
public static async CheckWebUIFontExist(): Promise<boolean> {
const fontsPath = resolve(webUiPathWrapper.configPath, './fonts');
return await fs
.access(resolve(fontsPath, './webui.woff'), constants.F_OK)
.then(() => true)
.catch(() => false);
}
// 获取webui字体文件路径
public static GetWebUIFontPath(): string {
return resolve(webUiPathWrapper.configPath, './fonts/webui.woff');
}
}

View File

@@ -13,7 +13,9 @@ import {
BatchMoveHandler,
DownloadHandler,
BatchDownloadHandler, // 新增下载处理方法
UploadHandler, // 添加上传处理器
UploadHandler,
UploadWebUIFontHandler,
DeleteWebUIFontHandler, // 添加上传处理器
} from '../api/File';
const router = Router();
@@ -37,5 +39,8 @@ router.post('/move', MoveHandler);
router.post('/batchMove', BatchMoveHandler);
router.post('/download', DownloadHandler);
router.post('/batchDownload', BatchDownloadHandler);
router.post('/upload', UploadHandler); // 添加上传处理路由
router.post('/upload', UploadHandler);
router.post('/font/upload/webui', UploadWebUIFontHandler);
router.post('/font/delete/webui', DeleteWebUIFontHandler);
export { router as FileRouter };

View File

@@ -0,0 +1,85 @@
import multer from 'multer';
import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import { randomUUID } from 'crypto';
const isWindows = process.platform === 'win32';
// 修改:使用 Buffer 转码文件名,解决文件上传时乱码问题
const decodeFileName = (fileName: string): string => {
try {
return Buffer.from(fileName, 'binary').toString('utf8');
} catch {
return fileName;
}
};
export const createDiskStorage = (uploadPath: string) => {
return multer.diskStorage({
destination: (
_: Request,
file: Express.Multer.File,
cb: (error: Error | null, destination: string) => void
) => {
try {
const decodedName = decodeFileName(file.originalname);
if (!uploadPath) {
return cb(new Error('上传路径不能为空'), '');
}
if (isWindows && uploadPath === '\\') {
return cb(new Error('根目录不允许上传文件'), '');
}
// 处理文件夹上传的情况
if (decodedName.includes('/') || decodedName.includes('\\')) {
const fullPath = path.join(uploadPath, path.dirname(decodedName));
fs.mkdirSync(fullPath, { recursive: true });
cb(null, fullPath);
} else {
cb(null, uploadPath);
}
} catch (error) {
cb(error as Error, '');
}
},
filename: (_: Request, file: Express.Multer.File, cb: (error: Error | null, filename: string) => void) => {
try {
const decodedName = decodeFileName(file.originalname);
const fileName = path.basename(decodedName);
// 检查文件是否存在
const fullPath = path.join(uploadPath, decodedName);
if (fs.existsSync(fullPath)) {
const ext = path.extname(fileName);
const name = path.basename(fileName, ext);
cb(null, `${name}-${randomUUID()}${ext}`);
} else {
cb(null, fileName);
}
} catch (error) {
cb(error as Error, '');
}
},
});
};
export const createDiskUpload = (uploadPath: string) => {
const upload = multer({ storage: createDiskStorage(uploadPath) }).array('files');
return upload;
};
const diskUploader = (req: Request, res: Response) => {
const uploadPath = (req.query['path'] || '') as string;
return new Promise((resolve, reject) => {
createDiskUpload(uploadPath)(req, res, (error) => {
if (error) {
// 错误处理
return reject(error);
}
return resolve(true);
});
});
};
export default diskUploader;

View File

@@ -0,0 +1,52 @@
import multer from 'multer';
import { WebUiConfigWrapper } from '../helper/config';
import path from 'path';
import fs from 'fs';
import type { Request, Response } from 'express';
export const webUIFontStorage = multer.diskStorage({
destination: (_, __, cb) => {
try {
const fontsPath = path.dirname(WebUiConfigWrapper.GetWebUIFontPath());
// 确保字体目录存在
fs.mkdirSync(fontsPath, { recursive: true });
cb(null, fontsPath);
} catch (error) {
// 确保错误信息被正确传递
cb(new Error(`创建字体目录失败:${(error as Error).message}`), '');
}
},
filename: (_, __, cb) => {
// 统一保存为webui.woff
cb(null, 'webui.woff');
},
});
export const webUIFontUpload = multer({
storage: webUIFontStorage,
fileFilter: (_, file, cb) => {
// 再次验证文件类型
if (!file.originalname.toLowerCase().endsWith('.woff')) {
cb(new Error('只支持WOFF格式的字体文件'));
return;
}
cb(null, true);
},
limits: {
fileSize: 40 * 1024 * 1024, // 限制40MB
},
}).single('file');
const webUIFontUploader = (req: Request, res: Response) => {
return new Promise((resolve, reject) => {
webUIFontUpload(req, res, (error) => {
if (error) {
// 错误处理
// sendError(res, error.message, true);
return reject(error);
}
return resolve(true);
});
});
};
export default webUIFontUploader;

View File

@@ -2,25 +2,46 @@ import type { Response } from 'express';
import { ResponseCode, HttpStatusCode } from '@webapi/const/status';
export const sendResponse = <T>(res: Response, data?: T, code: ResponseCode = 0, message = 'success') => {
res.status(HttpStatusCode.OK).json({
export const sendResponse = <T>(
res: Response,
data?: T,
code: ResponseCode = 0,
message = 'success',
useSend: boolean = false
) => {
const result = {
code,
message,
data,
});
};
if (useSend) {
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
return;
}
res.status(HttpStatusCode.OK).json(result);
};
export const sendError = (res: Response, message = 'error') => {
res.status(HttpStatusCode.OK).json({
export const sendError = (res: Response, message = 'error', useSend: boolean = false) => {
const result = {
code: ResponseCode.Error,
message,
});
};
if (useSend) {
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
return;
}
res.status(HttpStatusCode.OK).json(result);
};
export const sendSuccess = <T>(res: Response, data?: T, message = 'success') => {
res.status(HttpStatusCode.OK).json({
export const sendSuccess = <T>(res: Response, data?: T, message = 'success', useSend: boolean = false) => {
const result = {
code: ResponseCode.Success,
data,
message,
});
};
if (useSend) {
res.status(HttpStatusCode.OK).send(JSON.stringify(result));
return;
}
res.status(HttpStatusCode.OK).json(result);
};