mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 08:10:25 +00:00
Refactor font handling and theme config, switch to CodeMirror editor
Replaces Monaco editor with CodeMirror in the frontend, removing related dependencies and configuration. Refactors font management to support multiple formats (woff, woff2, ttf, otf) and dynamic font switching, including backend API and frontend theme config UI. Adds gzip compression middleware to backend. Updates theme config to allow font selection and custom font upload, and improves theme preview and color customization UI. Cleans up unused code and improves sidebar and terminal font sizing responsiveness.
This commit is contained in:
@@ -1,46 +1,126 @@
|
||||
import Editor, { OnMount, loader } from '@monaco-editor/react';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import React, { useImperativeHandle, useEffect, useState } from 'react';
|
||||
import CodeMirror, { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import monaco from '@/monaco';
|
||||
const getLanguageExtension = (lang?: string) => {
|
||||
switch (lang) {
|
||||
case 'json': return json();
|
||||
default: return [];
|
||||
}
|
||||
};
|
||||
|
||||
loader.config({
|
||||
monaco,
|
||||
});
|
||||
|
||||
export interface CodeEditorProps extends React.ComponentProps<typeof Editor> {
|
||||
test?: string;
|
||||
export interface CodeEditorProps {
|
||||
value?: string;
|
||||
defaultValue?: string;
|
||||
language?: string;
|
||||
defaultLanguage?: string;
|
||||
onChange?: (value: string | undefined) => void;
|
||||
height?: string;
|
||||
options?: any;
|
||||
onMount?: any;
|
||||
}
|
||||
|
||||
export type CodeEditorRef = monaco.editor.IStandaloneCodeEditor;
|
||||
export interface CodeEditorRef {
|
||||
getValue: () => string;
|
||||
}
|
||||
|
||||
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>(
|
||||
(props, ref) => {
|
||||
const { isDark } = useTheme();
|
||||
const CodeEditor = React.forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
|
||||
const { isDark } = useTheme();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [val, setVal] = useState(props.value || props.defaultValue || '');
|
||||
const internalRef = React.useRef<ReactCodeMirrorRef>(null);
|
||||
|
||||
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(editor);
|
||||
} else {
|
||||
(ref as React.RefObject<CodeEditorRef>).current = editor;
|
||||
}
|
||||
}
|
||||
if (props.onMount) {
|
||||
props.onMount(editor, monaco);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (props.value !== undefined) {
|
||||
setVal(props.value);
|
||||
}
|
||||
}, [props.value]);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
{...props}
|
||||
onMount={handleEditorDidMount}
|
||||
theme={isDark ? 'vs-dark' : 'light'}
|
||||
useImperativeHandle(ref, () => ({
|
||||
getValue: () => {
|
||||
// Prefer getting dynamic value from view, fallback to state
|
||||
return internalRef.current?.view?.state.doc.toString() || val;
|
||||
}
|
||||
}));
|
||||
|
||||
const customTheme = EditorView.theme({
|
||||
"&": {
|
||||
fontSize: "14px",
|
||||
height: "100% !important",
|
||||
},
|
||||
".cm-scroller": {
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace",
|
||||
lineHeight: "1.6",
|
||||
overflow: "auto !important",
|
||||
height: "100% !important",
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "transparent",
|
||||
borderRight: "none",
|
||||
color: isDark ? "#ffffff50" : "#00000040",
|
||||
},
|
||||
".cm-gutterElement": {
|
||||
paddingLeft: "12px",
|
||||
paddingRight: "12px",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "transparent",
|
||||
color: isDark ? "#fff" : "#000",
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: isDark ? "#fff" : "#000",
|
||||
paddingTop: "12px",
|
||||
paddingBottom: "12px",
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: isDark ? "#ffffff10" : "#00000008",
|
||||
},
|
||||
".cm-selectionMatch": {
|
||||
backgroundColor: isDark ? "#ffffff20" : "#00000010",
|
||||
},
|
||||
});
|
||||
|
||||
const extensions = [
|
||||
customTheme,
|
||||
getLanguageExtension(props.language || props.defaultLanguage),
|
||||
props.options?.wordWrap === 'on' ? EditorView.lineWrapping : [],
|
||||
props.options?.readOnly ? EditorView.editable.of(false) : [],
|
||||
].flat();
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ fontSize: props.options?.fontSize || 14, height: props.height || '100%', display: 'flex', flexDirection: 'column' }}
|
||||
className={clsx(
|
||||
'rounded-xl border overflow-hidden transition-colors',
|
||||
isDark
|
||||
? 'border-white/10 bg-[#282c34]'
|
||||
: 'border-default-200 bg-white'
|
||||
)}
|
||||
>
|
||||
<CodeMirror
|
||||
ref={internalRef}
|
||||
value={props.value ?? props.defaultValue}
|
||||
height="100%"
|
||||
className="h-full w-full"
|
||||
theme={isDark ? oneDark : 'light'}
|
||||
extensions={extensions}
|
||||
onChange={(value) => {
|
||||
setVal(value);
|
||||
props.onChange?.(value);
|
||||
}}
|
||||
readOnly={props.options?.readOnly}
|
||||
basicSetup={{
|
||||
lineNumbers: props.options?.lineNumbers !== 'off',
|
||||
foldGutter: props.options?.folding !== false,
|
||||
highlightActiveLine: props.options?.renderLineHighlight !== 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default CodeEditor;
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
import CodeEditor from '@/components/code_editor';
|
||||
|
||||
interface FileEditModalProps {
|
||||
isOpen: boolean
|
||||
file: { path: string; content: string } | null
|
||||
onClose: () => void
|
||||
onSave: () => void
|
||||
onContentChange: (newContent?: string) => void
|
||||
isOpen: boolean;
|
||||
file: { path: string; content: string; } | null;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
onContentChange: (newContent?: string) => void;
|
||||
}
|
||||
|
||||
export default function FileEditModal ({
|
||||
@@ -65,12 +65,20 @@ export default function FileEditModal ({
|
||||
return (
|
||||
<Modal size='full' isOpen={isOpen} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalHeader className='flex items-center gap-2 bg-content2 bg-opacity-50'>
|
||||
<ModalHeader className='flex items-center gap-2 border-b border-default-200/50'>
|
||||
<span>编辑文件</span>
|
||||
<Code className='text-xs'>{file?.path}</Code>
|
||||
<div className="ml-auto text-xs text-default-400 font-normal px-2">
|
||||
按 <span className="px-1 py-0.5 rounded border border-default-300 bg-default-100">Ctrl/Cmd + S</span> 保存
|
||||
</div>
|
||||
</ModalHeader>
|
||||
<ModalBody className='p-0'>
|
||||
<div className='h-full'>
|
||||
<ModalBody className='p-4 bg-content2/50'>
|
||||
<div className='h-full' onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||
e.preventDefault();
|
||||
onSave();
|
||||
}
|
||||
}}>
|
||||
<CodeEditor
|
||||
height='100%'
|
||||
value={file?.content || ''}
|
||||
@@ -80,7 +88,7 @@ export default function FileEditModal ({
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ModalFooter className="border-t border-default-200/50">
|
||||
<Button color='primary' variant='flat' onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
|
||||
@@ -274,8 +274,9 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
|
||||
<div className='flex-1 min-h-0 relative px-3 pb-2 mt-1'>
|
||||
<div className={clsx(
|
||||
'h-full rounded-xl overflow-y-auto no-scrollbar transition-all',
|
||||
hasBackground ? 'bg-transparent' : 'bg-white/10 dark:bg-black/10'
|
||||
'h-full transition-all',
|
||||
activeTab !== 'request' && 'rounded-xl overflow-y-auto no-scrollbar',
|
||||
hasBackground ? 'bg-transparent' : (activeTab !== 'request' && 'bg-white/10 dark:bg-black/10')
|
||||
)}>
|
||||
{activeTab === 'request' ? (
|
||||
<CodeEditor
|
||||
@@ -351,7 +352,7 @@ const OneBotApiDebug: React.FC<OneBotApiDebugProps> = (props) => {
|
||||
|
||||
{/* Response Content - Code Editor */}
|
||||
{responseExpanded && (
|
||||
<div style={{ height: responseHeight }} className="relative bg-black/5 dark:bg-black/20">
|
||||
<div style={{ height: responseHeight }} className="relative bg-transparent">
|
||||
<PageLoading loading={isFetching} />
|
||||
<CodeEditor
|
||||
value={responseContent || '// Waiting for response...'}
|
||||
|
||||
@@ -61,7 +61,7 @@ const OneBotSendModal: React.FC<OneBotSendModalProps> = (props) => {
|
||||
构造请求
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='h-96 dark:bg-[rgb(30,30,30)] p-2 rounded-md border border-default-100'>
|
||||
<div className='h-96'>
|
||||
<CodeEditor
|
||||
height='100%'
|
||||
defaultLanguage='json'
|
||||
|
||||
@@ -2,13 +2,13 @@ import { Spinner } from '@heroui/spinner';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface PageLoadingProps {
|
||||
loading?: boolean
|
||||
loading?: boolean;
|
||||
}
|
||||
const PageLoading: React.FC<PageLoadingProps> = ({ loading }) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'absolute top-0 left-0 w-full h-full bg-zinc-500 bg-opacity-10 z-50 flex justify-center items-center backdrop-blur',
|
||||
'absolute top-0 left-0 w-full h-full bg-zinc-500 bg-opacity-10 z-30 flex justify-center items-center backdrop-blur',
|
||||
{
|
||||
hidden: !loading,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { useLocalStorage } from '@uidotdev/usehooks';
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import React from 'react';
|
||||
import { IoMdLogOut } from 'react-icons/io';
|
||||
import { MdDarkMode, MdLightMode } from 'react-icons/md';
|
||||
|
||||
import key from '@/const/key';
|
||||
import useAuth from '@/hooks/auth';
|
||||
import useDialog from '@/hooks/use-dialog';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
@@ -24,7 +22,6 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
const { open, items, onClose } = props;
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
const { revokeAuth } = useAuth();
|
||||
const [b64img] = useLocalStorage(key.backgroundImage, '');
|
||||
const dialog = useDialog();
|
||||
const onRevokeAuth = () => {
|
||||
dialog.confirm({
|
||||
@@ -50,9 +47,9 @@ const SideBar: React.FC<SideBarProps> = (props) => {
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
className={clsx(
|
||||
'overflow-hidden fixed top-0 left-0 h-full z-50 md:static shadow-md md:shadow-none rounded-r-md md:rounded-none',
|
||||
b64img ? 'bg-black/20 backdrop-blur-md border-r border-white/10' : 'bg-background',
|
||||
'md:bg-transparent md:border-r-0 md:backdrop-blur-none'
|
||||
'overflow-hidden fixed top-0 left-0 h-full z-50 md:static md:shadow-none rounded-r-2xl md:rounded-none',
|
||||
'bg-content1/70 backdrop-blur-xl backdrop-saturate-150 shadow-xl',
|
||||
'md:bg-transparent md:backdrop-blur-none md:backdrop-saturate-100 md:shadow-none'
|
||||
)}
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: open ? '16rem' : 0 }}
|
||||
|
||||
@@ -36,8 +36,18 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||
const { theme } = useTheme();
|
||||
useEffect(() => {
|
||||
// 根据屏幕宽度决定字体大小,手机端使用更小的字体
|
||||
const isMobile = window.innerWidth < 768;
|
||||
const fontSize = isMobile ? 11 : 14;
|
||||
const width = window.innerWidth;
|
||||
// 按屏幕宽度自适应字体大小
|
||||
let fontSize = 16;
|
||||
if (width < 400) {
|
||||
fontSize = 4;
|
||||
} else if (width < 600) {
|
||||
fontSize = 5;
|
||||
} else if (width < 900) {
|
||||
fontSize = 6;
|
||||
} else if (width < 1280) {
|
||||
fontSize = 12;
|
||||
} // ≥1280: 16
|
||||
|
||||
const terminal = new Terminal({
|
||||
allowTransparency: true,
|
||||
@@ -60,10 +70,8 @@ const XTerm = forwardRef<XTermRef, XTermProps>((props, ref) => {
|
||||
terminal.loadAddon(fitAddon);
|
||||
terminal.open(domRef.current!);
|
||||
|
||||
// 只在非手机端使用 Canvas 渲染器,手机端使用默认 DOM 渲染器以避免渲染问题
|
||||
if (!isMobile) {
|
||||
terminal.loadAddon(new CanvasAddon());
|
||||
}
|
||||
// 所有端都使用 Canvas 渲染器(包括手机端)
|
||||
terminal.loadAddon(new CanvasAddon());
|
||||
terminal.onData((data) => {
|
||||
if (onInput) {
|
||||
onInput(data);
|
||||
|
||||
Reference in New Issue
Block a user