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 { Toaster } from "sonner";
import type { Metadata } from "next";
import { getLocale } from "next-intl/server";
import { NextIntlClientProvider } from "next-intl";
import { ThemeProvider } from "@/components/theme-provider";
import { SettingsDialog } from "@/components/settings-dialog";
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/theme-provider';
import { ThemeToggle } from '@/components/theme-toggle';
import { EditorConfigPanel } from '@/components/editor-config-panel';
import {useState} from "react";
export const metadata: Metadata = {
title: "Judge4c",
description:
"A full-stack, open-source online judge platform designed to elevate college programming education.",
};
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
interface RootLayoutProps {
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}
export default async function RootLayout({ children }: RootLayoutProps) {
const locale = await getLocale();
}) {
const [showConfigPanel, setShowConfigPanel] = useState(false);
return (
<html lang={locale} className="h-full" suppressHydrationWarning>
<body className="flex min-h-full antialiased">
<NextIntlClientProvider>
<html lang="zh-CN" suppressHydrationWarning>
<head>
<title>Judge4C</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/icon.svg" />
</head>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="w-full">{children}</div>
<SettingsDialog />
<Toaster position="top-right" />
{/* 新增的配置面板按钮 */}
<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>
</NextIntlClientProvider>
</body>
</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,9 +11,43 @@ import { useCallback, useEffect, useRef } from "react";
import { connectToLanguageServer } from "@/lib/language-server";
import type { MonacoLanguageClient } from "monaco-languageclient";
import { DefaultEditorOptionConfig } from "@/config/editor-option";
import { useEditorConfigStore } from '@/lib/store'; // 新增导入
import * as monaco from 'monaco-editor';
// Dynamically import Monaco Editor with SSR disabled
const Editor = dynamic(
export const ProblemEditor = () => {
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<typeof monaco | null>(null);
// 使用配置状态
const { config } = useEditorConfigStore();
useEffect(() => {
if (!monacoRef.current || !editorRef.current) return;
// 设置语言和主题
const { languages } = monaco;
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
// target保持使用ScriptTarget
target: monaco.languages.typescript.ScriptTarget.ESNext,
// module使用正确的ModuleKind类型
module: monaco.languages.typescript.ModuleKind.ESNext,
strict: true,
jsx: monaco.languages.typescript.JsxEmit.React,
esModuleInterop: true,
isolatedModules: true,
experimentalDecorators: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
allowJs: true,
allowSyntheticDefaultImports: true,
typeRoots: ["../node_modules/@types"],
});
// 应用用户配置
editorRef.current.updateOptions(config);
}, []);
// Dynamically import Monaco Editor with SSR disabled
const Editor = dynamic(
async () => {
await import("vscode");
const monaco = await import("monaco-editor");
@ -53,9 +87,8 @@ const Editor = dynamic(
ssr: false,
loading: () => <Loading />,
}
);
);
export function ProblemEditor() {
const {
hydrated,
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 };
}),
};
});