feat(editor): 添加编辑器配置功能并优化布局

- 在根布局中添加编辑器配置面板按钮- 实现编辑器配置面板组件,支持字体、字号、行高等设置
- 新增 useEditorConfigStore 钩子,用于管理编辑器配置状态
- 在问题编辑器中应用用户配置,并支持 TypeScript 语法
This commit is contained in:
fly6516 2025-05-17 03:36:28 +08:00
parent 443adf055b
commit b36c4af282
4 changed files with 243 additions and 73 deletions

View File

@ -1,39 +1,68 @@
"use client"
import "@/app/globals.css"; import "@/app/globals.css";
import { Toaster } from "sonner"; import { Inter } from 'next/font/google';
import type { Metadata } from "next"; import { ThemeProvider } from '@/components/theme-provider';
import { getLocale } from "next-intl/server"; import { ThemeToggle } from '@/components/theme-toggle';
import { NextIntlClientProvider } from "next-intl"; import { EditorConfigPanel } from '@/components/editor-config-panel';
import { ThemeProvider } from "@/components/theme-provider"; import {useState} from "react";
import { SettingsDialog } from "@/components/settings-dialog";
export const metadata: Metadata = { const inter = Inter({
title: "Judge4c", subsets: ['latin'],
description: display: 'swap',
"A full-stack, open-source online judge platform designed to elevate college programming education.", });
};
interface RootLayoutProps { export default function RootLayout({
children,
}: {
children: React.ReactNode; children: React.ReactNode;
} }) {
const [showConfigPanel, setShowConfigPanel] = useState(false);
export default async function RootLayout({ children }: RootLayoutProps) {
const locale = await getLocale();
return ( return (
<html lang={locale} className="h-full" suppressHydrationWarning> <html lang="zh-CN" suppressHydrationWarning>
<body className="flex min-h-full antialiased"> <head>
<NextIntlClientProvider> <title>Judge4C</title>
<ThemeProvider <meta name="viewport" content="width=device-width, initial-scale=1.0" />
attribute="class" <link rel="icon" href="/icon.svg" />
defaultTheme="system" </head>
enableSystem <body className={inter.className}>
disableTransitionOnChange <ThemeProvider
> attribute="class"
<div className="w-full">{children}</div> defaultTheme="system"
<SettingsDialog /> enableSystem
<Toaster position="top-right" /> disableTransitionOnChange
</ThemeProvider> >
</NextIntlClientProvider> {/* 新增的配置面板按钮 */}
<div className="fixed top-4 right-4 z-50">
<ThemeToggle />
<button
onClick={() => setShowConfigPanel(true)}
className="mt-2 px-3 py-1 text-sm font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600"
>
</button>
</div>
{/* 配置面板 */}
{showConfigPanel && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
<div className="relative w-full max-w-md bg-white dark:bg-gray-800 rounded-lg shadow-xl">
<div className="absolute top-2 right-2">
<button
onClick={() => setShowConfigPanel(false)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
</button>
</div>
<EditorConfigPanel />
</div>
</div>
)}
{children}
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -0,0 +1,71 @@
import React from 'react';
import { useEditorConfigStore } from '@/lib/store';
export const EditorConfigPanel = () => {
const { config, updateConfig } = useEditorConfigStore();
const handleFontFamilyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateConfig({ fontFamily: e.target.value });
};
const handleFontSizeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateConfig({ fontSize: parseInt(e.target.value) });
};
const handleLineHeightChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateConfig({ lineHeight: parseInt(e.target.value) });
};
const handleReset = () => {
useEditorConfigStore.getState().resetConfig();
};
return (
<div className="p-4 space-y-4">
<h2 className="text-lg font-semibold"></h2>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<input
type="text"
value={config.fontFamily}
onChange={handleFontFamilyChange}
className="w-full p-2 border rounded-md"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<input
type="number"
min="10"
max="24"
value={config.fontSize}
onChange={handleFontSizeChange}
className="w-full p-2 border rounded-md"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<input
type="number"
min="18"
max="36"
value={config.lineHeight}
onChange={handleLineHeightChange}
className="w-full p-2 border rounded-md"
/>
</div>
<div className="pt-2">
<button
onClick={handleReset}
className="px-4 py-2 text-sm font-medium text-white bg-red-500 rounded-md hover:bg-red-600"
>
</button>
</div>
</div>
);
};

View File

@ -11,51 +11,84 @@ import { useCallback, useEffect, useRef } from "react";
import { connectToLanguageServer } from "@/lib/language-server"; import { connectToLanguageServer } from "@/lib/language-server";
import type { MonacoLanguageClient } from "monaco-languageclient"; import type { MonacoLanguageClient } from "monaco-languageclient";
import { DefaultEditorOptionConfig } from "@/config/editor-option"; import { DefaultEditorOptionConfig } from "@/config/editor-option";
import { useEditorConfigStore } from '@/lib/store'; // 新增导入
import * as monaco from 'monaco-editor';
// Dynamically import Monaco Editor with SSR disabled export const ProblemEditor = () => {
const Editor = dynamic( const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
async () => { const monacoRef = useRef<typeof monaco | null>(null);
await import("vscode");
const monaco = await import("monaco-editor");
self.MonacoEnvironment = { // 使用配置状态
getWorker(_, label) { const { config } = useEditorConfigStore();
if (label === "json") {
return new Worker( useEffect(() => {
new URL("monaco-editor/esm/vs/language/json/json.worker.js", import.meta.url) if (!monacoRef.current || !editorRef.current) return;
);
} // 设置语言和主题
if (label === "css" || label === "scss" || label === "less") { const { languages } = monaco;
return new Worker( monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
new URL("monaco-editor/esm/vs/language/css/css.worker.js", import.meta.url) // target保持使用ScriptTarget
); target: monaco.languages.typescript.ScriptTarget.ESNext,
} // module使用正确的ModuleKind类型
if (label === "html" || label === "handlebars" || label === "razor") { module: monaco.languages.typescript.ModuleKind.ESNext,
return new Worker( strict: true,
new URL("monaco-editor/esm/vs/language/html/html.worker.js", import.meta.url) jsx: monaco.languages.typescript.JsxEmit.React,
); esModuleInterop: true,
} isolatedModules: true,
if (label === "typescript" || label === "javascript") { experimentalDecorators: true,
return new Worker( moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
new URL("monaco-editor/esm/vs/language/typescript/ts.worker.js", import.meta.url) allowJs: true,
); allowSyntheticDefaultImports: true,
} typeRoots: ["../node_modules/@types"],
return new Worker( });
new URL("monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url)
); // 应用用户配置
}, editorRef.current.updateOptions(config);
}; }, []);
const { loader } = await import("@monaco-editor/react");
loader.config({ monaco }); // Dynamically import Monaco Editor with SSR disabled
return (await import("@monaco-editor/react")).Editor; const Editor = dynamic(
}, async () => {
{ await import("vscode");
ssr: false, const monaco = await import("monaco-editor");
loading: () => <Loading />,
} self.MonacoEnvironment = {
); getWorker(_, label) {
if (label === "json") {
return new Worker(
new URL("monaco-editor/esm/vs/language/json/json.worker.js", import.meta.url)
);
}
if (label === "css" || label === "scss" || label === "less") {
return new Worker(
new URL("monaco-editor/esm/vs/language/css/css.worker.js", import.meta.url)
);
}
if (label === "html" || label === "handlebars" || label === "razor") {
return new Worker(
new URL("monaco-editor/esm/vs/language/html/html.worker.js", import.meta.url)
);
}
if (label === "typescript" || label === "javascript") {
return new Worker(
new URL("monaco-editor/esm/vs/language/typescript/ts.worker.js", import.meta.url)
);
}
return new Worker(
new URL("monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url)
);
},
};
const { loader } = await import("@monaco-editor/react");
loader.config({ monaco });
return (await import("@monaco-editor/react")).Editor;
},
{
ssr: false,
loading: () => <Loading />,
}
);
export function ProblemEditor() {
const { const {
hydrated, hydrated,
editor, editor,

37
src/lib/store.ts Normal file
View File

@ -0,0 +1,37 @@
import { create } from 'zustand';
import { editor } from 'monaco-editor';
import { DefaultEditorOptionConfig } from '@/config/editor-option';
interface EditorConfigState {
config: editor.IEditorConstructionOptions;
updateConfig: (newConfig: Partial<editor.IEditorConstructionOptions>) => void;
resetConfig: () => void;
defaultConfig: editor.IEditorConstructionOptions;
}
export const useEditorConfigStore = create<EditorConfigState>((set) => {
// 从localStorage读取保存的配置
const savedConfig = localStorage.getItem('editorConfig');
const parsedConfig = savedConfig ? JSON.parse(savedConfig) : {};
return {
config: {
...DefaultEditorOptionConfig,
...parsedConfig,
},
defaultConfig: DefaultEditorOptionConfig,
updateConfig: (newConfig) => set((state) => {
const updatedConfig = {
...state.config,
...newConfig,
};
// 保存到localStorage
localStorage.setItem('editorConfig', JSON.stringify(updatedConfig));
return { config: updatedConfig };
}),
resetConfig: () => set((state) => {
localStorage.removeItem('editorConfig');
return { config: state.defaultConfig };
}),
};
});