mirror of
				https://github.com/massbug/judge4c.git
				synced 2025-10-31 07:34:05 +00:00 
			
		
		
		
	feat(editor): 添加编辑器配置功能并优化布局
- 在根布局中添加编辑器配置面板按钮- 实现编辑器配置面板组件,支持字体、字号、行高等设置 - 新增 useEditorConfigStore 钩子,用于管理编辑器配置状态 - 在问题编辑器中应用用户配置,并支持 TypeScript 语法
This commit is contained in:
		
							parent
							
								
									443adf055b
								
							
						
					
					
						commit
						b36c4af282
					
				| @ -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> | ||||||
|   ); |   ); | ||||||
|  | |||||||
							
								
								
									
										71
									
								
								src/components/editor-config-panel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/components/editor-config-panel.tsx
									
									
									
									
									
										Normal 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> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
| @ -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
									
								
							
							
						
						
									
										37
									
								
								src/lib/store.ts
									
									
									
									
									
										Normal 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 }; | ||||||
|  |     }), | ||||||
|  |   }; | ||||||
|  | }); | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user