Support custom WebUI fonts and UI additions

Backend: add CheckWebUIFontExist API and route; set --font-family-mono CSS variable in InitWebUi for aacute/custom/default. Improve webui font uploader: force saved filename to CustomFont, robustly clean old webui/CustomFont files, and log failures.

Frontend: add FileManager.checkWebUIFontExists; update theme settings to show upload UI only when 'custom' is selected, display uploaded status, attempt delete-before-upload, reload after actions, and adjust Accordion props. ColorPicker: enable pointer events on PopoverContent to allow dragging. applyFont now sets --font-family-mono for all modes.
This commit is contained in:
手瓜一十雪 2026-02-01 14:00:27 +08:00
parent 1239f622d2
commit 90e3936204
8 changed files with 115 additions and 45 deletions

View File

@ -231,10 +231,13 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
// 添加字体变量
if (fontMode === 'aacute') {
css += "--font-family-base: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
css += "--font-family-mono: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
} else if (fontMode === 'custom') {
css += "--font-family-base: 'CustomFont', var(--font-family-fallbacks) !important;";
css += "--font-family-mono: 'CustomFont', var(--font-family-fallbacks) !important;";
} else {
css += '--font-family-base: var(--font-family-fallbacks) !important;';
css += '--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;';
}
css += '}';
@ -245,10 +248,13 @@ export async function InitWebUi (logger: ILogWrapper, pathWrapper: NapCatPathWra
// 添加字体变量
if (fontMode === 'aacute') {
css += "--font-family-base: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
css += "--font-family-mono: 'Aa偷吃可爱长大的', var(--font-family-fallbacks) !important;";
} else if (fontMode === 'custom') {
css += "--font-family-base: 'CustomFont', var(--font-family-fallbacks) !important;";
css += "--font-family-mono: 'CustomFont', var(--font-family-fallbacks) !important;";
} else {
css += '--font-family-base: var(--font-family-fallbacks) !important;';
css += '--font-family-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;';
}
css += '}';

View File

@ -653,3 +653,13 @@ export const DeleteWebUIFontHandler: RequestHandler = async (_req, res) => {
return sendError(res, '删除字体文件失败');
}
};
// 检查WebUI字体文件是否存在
export const CheckWebUIFontExistHandler: RequestHandler = async (_req, res) => {
try {
const exists = await WebUiConfig.CheckWebUIFontExist();
return sendSuccess(res, exists);
} catch (_error) {
return sendError(res, '检查字体文件失败');
}
};

View File

@ -16,6 +16,7 @@ import {
UploadHandler,
UploadWebUIFontHandler,
DeleteWebUIFontHandler, // 添加上传处理器
CheckWebUIFontExistHandler, // Add this
} from '../api/File';
const router: Router = Router();
@ -46,4 +47,5 @@ router.post('/upload', UploadHandler);
router.post('/font/upload/webui', UploadWebUIFontHandler);
router.post('/font/delete/webui', DeleteWebUIFontHandler);
router.get('/font/exists/webui', CheckWebUIFontExistHandler); // Add this
export { router as FileRouter };

View File

@ -9,15 +9,30 @@ const SUPPORTED_FONT_EXTENSIONS = ['.woff', '.woff2', '.ttf', '.otf'];
// 清理旧的字体文件
const cleanOldFontFiles = (fontsPath: string) => {
for (const ext of SUPPORTED_FONT_EXTENSIONS) {
const fontPath = path.join(fontsPath, `webui${ext}`);
try {
if (fs.existsSync(fontPath)) {
fs.unlinkSync(fontPath);
}
} catch {
// 忽略删除失败
try {
// 确保字体目录存在
if (!fs.existsSync(fontsPath)) {
return;
}
// 遍历目录下所有文件
const files = fs.readdirSync(fontsPath);
for (const file of files) {
// 检查文件名是否以 webui 或 CustomFont 开头,且是支持的字体扩展名
const ext = path.extname(file).toLowerCase();
const name = path.basename(file, ext);
if (SUPPORTED_FONT_EXTENSIONS.includes(ext) && (name === 'webui' || name === 'CustomFont')) {
try {
fs.unlinkSync(path.join(fontsPath, file));
} catch (e) {
console.error(`Failed to delete old font file ${file}:`, e);
}
}
}
} catch (err) {
console.error('Failed to clean old font files:', err);
}
};
@ -36,9 +51,9 @@ export const webUIFontStorage = multer.diskStorage({
}
},
filename: (_, file, cb) => {
// 保留原始扩展名,统一文件名为 webui
// 强制文件名为 CustomFont保留原始扩展名
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `webui${ext}`);
cb(null, `CustomFont${ext}`);
},
});

View File

@ -5,8 +5,8 @@ import { ColorResult, SketchPicker } from 'react-color';
// 假定 heroui 提供的 Popover组件
interface ColorPickerProps {
color: string
onChange: (color: ColorResult) => void
color: string;
onChange: (color: ColorResult) => void;
}
const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
@ -22,7 +22,8 @@ const ColorPicker: React.FC<ColorPickerProps> = ({ color, onChange }) => {
style={{ background: color }}
/>
</PopoverTrigger>
<PopoverContent>
{/* 移除 PopoverContent 默认的事件阻止,允许鼠标拖动到外部 */}
<PopoverContent className='pointer-events-auto'>
<SketchPicker
color={color}
onChange={handleChange}

View File

@ -218,4 +218,11 @@ export default class FileManager {
);
return data.data;
}
public static async checkWebUIFontExists () {
const { data } = await serverRequest.get<ServerResponse<boolean>>(
'/File/font/exists/webui'
);
return data.data;
}
}

View File

@ -162,6 +162,7 @@ const ThemeConfigCard = () => {
const [dataLoaded, setDataLoaded] = useState(false);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [customFontExists, setCustomFontExists] = useState(false);
// 使用 useRef 存储 style 标签引用和状态
const styleTagRef = useRef<HTMLStyleElement | null>(null);
@ -213,6 +214,10 @@ const ThemeConfigCard = () => {
}
setDataLoaded(true);
setHasUnsavedChanges(false);
// 检查自定义字体是否存在
FileManager.checkWebUIFontExists().then(exists => {
setCustomFontExists(exists);
}).catch(err => console.error('Failed to check custom font:', err));
}, [data, setOnebotValue]);
// 实时应用字体预设(预览)
@ -354,7 +359,11 @@ const ThemeConfigCard = () => {
</div>
<div className='p-4'>
<Accordion variant='splitted' defaultExpandedKeys={['font', 'select']}>
<Accordion
variant='splitted'
defaultExpandedKeys={['font']}
selectionMode='single'
>
<AccordionItem
key='font'
aria-label='Font Settings'
@ -381,38 +390,55 @@ const ThemeConfigCard = () => {
</Select>
)}
/>
<div className='p-3 rounded-lg bg-default-100 dark:bg-default-50/30'>
<div className='text-sm text-default-500 mb-2'>
"自定义字体"
{theme.fontMode === 'custom' && (
<div className='p-3 rounded-lg bg-default-100 dark:bg-default-50/30'>
<div className='text-sm text-default-500 mb-2'>
"自定义字体"
</div>
{customFontExists && (
<div className='mb-2 flex items-center gap-2 text-sm text-primary'>
<FaCheck />
</div>
)}
<FileInput
label='上传字体文件'
placeholder='选择字体文件 (.woff/.woff2/.ttf/.otf)'
accept='.ttf,.otf,.woff,.woff2'
onChange={async (file) => {
try {
// 如果已存在自定义字体,先尝试删除
if (customFontExists) {
try {
await FileManager.deleteWebUIFont();
} catch (e) {
console.warn('Failed to delete existing font before upload:', e);
// 继续尝试上传,后端可能会覆盖或报错
}
}
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();
toast.success('删除成功,即将刷新页面');
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
toast.error('删除失败: ' + (error as Error).message);
}
}}
/>
</div>
<FileInput
label='上传字体文件'
placeholder='选择字体文件 (.woff/.woff2/.ttf/.otf)'
accept='.ttf,.otf,.woff,.woff2'
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();
toast.success('删除成功,即将刷新页面');
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
toast.error('删除失败: ' + (error as Error).message);
}
}}
/>
</div>
)}
</div>
</AccordionItem>

View File

@ -180,11 +180,14 @@ export const applyFont = (mode: string) => {
if (mode === 'aacute') {
root.style.setProperty('--font-family-base', "'Aa偷吃可爱长大的', var(--font-family-fallbacks)", 'important');
root.style.setProperty('--font-family-mono', "'Aa偷吃可爱长大的', var(--font-family-fallbacks)", 'important');
} else if (mode === 'custom') {
root.style.setProperty('--font-family-base', "'CustomFont', var(--font-family-fallbacks)", 'important');
root.style.setProperty('--font-family-mono', "'CustomFont', var(--font-family-fallbacks)", 'important');
} else {
// system or default - restore default
root.style.setProperty('--font-family-base', 'var(--font-family-fallbacks)', 'important');
root.style.setProperty('--font-family-mono', 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', 'important');
}
};