feat: 更新 CodeEditor 组件,重构为懒加载并添加 CodeEditorImpl,优化导入路径

This commit is contained in:
DrSmoothl
2026-04-24 23:10:01 +08:00
parent 3b6d30cd5e
commit 201efe66a1
11 changed files with 234 additions and 188 deletions

View File

@@ -1,19 +1,8 @@
import { useEffect, useState } from 'react'
import CodeMirror from '@uiw/react-codemirror'
import { css } from '@codemirror/lang-css'
import { json, jsonParseLinter } from '@codemirror/lang-json'
import { linter } from '@codemirror/lint'
import { python } from '@codemirror/lang-python'
import { oneDark } from '@codemirror/theme-one-dark'
import { EditorView } from '@codemirror/view'
import { StreamLanguage } from '@codemirror/language'
import { toml as tomlMode } from '@codemirror/legacy-modes/mode/toml'
import { useTheme } from '@/components/use-theme'
import { lazy, Suspense } from 'react'
export type Language = 'python' | 'json' | 'toml' | 'css' | 'text'
interface CodeEditorProps {
export interface CodeEditorProps {
value: string
onChange?: (value: string) => void
@@ -27,109 +16,38 @@ interface CodeEditorProps {
className?: string
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const languageExtensions: Record<Language, any[]> = {
python: [python()],
json: [json(), linter(jsonParseLinter())],
toml: [StreamLanguage.define(tomlMode)],
css: [css()],
text: [],
}
const CodeEditorImpl = lazy(() => import('./CodeEditorImpl'))
export function CodeEditor({
value,
onChange,
language = 'text',
readOnly = false,
height = '400px',
function CodeEditorFallback({
height,
minHeight,
maxHeight,
placeholder,
theme,
className = '',
}: CodeEditorProps) {
const [mounted, setMounted] = useState(false)
const { resolvedTheme } = useTheme()
}: Pick<CodeEditorProps, 'height' | 'minHeight' | 'maxHeight' | 'className'>) {
return (
<div
className={`bg-muted animate-pulse rounded-md border ${className}`}
style={{ height, minHeight, maxHeight }}
/>
)
}
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return (
<div
className={`rounded-md border bg-muted animate-pulse ${className}`}
style={{ height, minHeight, maxHeight }}
/>
)
}
const extensions = [
...(languageExtensions[language] || []),
EditorView.lineWrapping,
// 应用 JetBrains Mono 字体
EditorView.theme({
'&': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
'.cm-content': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
'.cm-gutters': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
'.cm-scroller': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
}),
]
if (readOnly) {
extensions.push(EditorView.editable.of(false))
}
// 如果外部传了 theme prop 则使用,否则从 context 自动获取
const effectiveTheme = theme ?? resolvedTheme
export function CodeEditor(props: CodeEditorProps) {
const { height = '400px', minHeight, maxHeight, className = '' } = props
return (
<div className={`rounded-md overflow-hidden border custom-scrollbar ${className}`}>
<CodeMirror
value={value}
height={height}
minHeight={minHeight}
maxHeight={maxHeight}
theme={effectiveTheme === 'dark' ? oneDark : undefined}
extensions={extensions}
onChange={onChange}
placeholder={placeholder}
basicSetup={{
lineNumbers: true,
highlightActiveLineGutter: true,
highlightSpecialChars: true,
history: true,
foldGutter: true,
drawSelection: true,
dropCursor: true,
allowMultipleSelections: true,
indentOnInput: true,
syntaxHighlighting: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: true,
rectangularSelection: true,
crosshairCursor: true,
highlightActiveLine: true,
highlightSelectionMatches: true,
closeBracketsKeymap: true,
defaultKeymap: true,
searchKeymap: true,
historyKeymap: true,
foldKeymap: true,
completionKeymap: true,
lintKeymap: true,
}}
/>
</div>
<Suspense
fallback={
<CodeEditorFallback
height={height}
minHeight={minHeight}
maxHeight={maxHeight}
className={className}
/>
}
>
<CodeEditorImpl {...props} />
</Suspense>
)
}

View File

@@ -0,0 +1,105 @@
import { css } from '@codemirror/lang-css'
import { json, jsonParseLinter } from '@codemirror/lang-json'
import { python } from '@codemirror/lang-python'
import { StreamLanguage } from '@codemirror/language'
import { toml as tomlMode } from '@codemirror/legacy-modes/mode/toml'
import { linter } from '@codemirror/lint'
import { oneDark } from '@codemirror/theme-one-dark'
import { EditorView } from '@codemirror/view'
import CodeMirror from '@uiw/react-codemirror'
import { useTheme } from '@/components/use-theme'
import type { CodeEditorProps, Language } from './CodeEditor'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const languageExtensions: Record<Language, any[]> = {
python: [python()],
json: [json(), linter(jsonParseLinter())],
toml: [StreamLanguage.define(tomlMode)],
css: [css()],
text: [],
}
export default function CodeEditorImpl({
value,
onChange,
language = 'text',
readOnly = false,
height = '400px',
minHeight,
maxHeight,
placeholder,
theme,
className = '',
}: CodeEditorProps) {
const { resolvedTheme } = useTheme()
const extensions = [
...(languageExtensions[language] || []),
EditorView.lineWrapping,
// 应用 JetBrains Mono 字体
EditorView.theme({
'&': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
'.cm-content': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
'.cm-gutters': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
'.cm-scroller': {
fontFamily: '"JetBrains Mono", "Fira Code", "Consolas", "Monaco", monospace',
},
}),
]
if (readOnly) {
extensions.push(EditorView.editable.of(false))
}
// 如果外部传了 theme prop 则使用,否则从 context 自动获取
const effectiveTheme = theme ?? resolvedTheme
return (
<div className={`custom-scrollbar overflow-hidden rounded-md border ${className}`}>
<CodeMirror
value={value}
height={height}
minHeight={minHeight}
maxHeight={maxHeight}
theme={effectiveTheme === 'dark' ? oneDark : undefined}
extensions={extensions}
onChange={onChange}
placeholder={placeholder}
basicSetup={{
lineNumbers: true,
highlightActiveLineGutter: true,
highlightSpecialChars: true,
history: true,
foldGutter: true,
drawSelection: true,
dropCursor: true,
allowMultipleSelections: true,
indentOnInput: true,
syntaxHighlighting: true,
bracketMatching: true,
closeBrackets: true,
autocompletion: true,
rectangularSelection: true,
crosshairCursor: true,
highlightActiveLine: true,
highlightSelectionMatches: true,
closeBracketsKeymap: true,
defaultKeymap: true,
searchKeymap: true,
historyKeymap: true,
foldKeymap: true,
completionKeymap: true,
lintKeymap: true,
}}
/>
</div>
)
}

View File

@@ -1443,6 +1443,7 @@ export function ExpressionReviewer({ open, onOpenChange }: ExpressionReviewerPro
<div
className="relative w-full max-w-md h-[400px] flex items-center justify-center"
role="listbox"
tabIndex={0}
aria-label="待审核的表达方式"
aria-activedescendant={quickExpressions[quickCurrentIndex] ? `quick-expr-${quickExpressions[quickCurrentIndex].id}` : undefined}
>
@@ -1561,14 +1562,14 @@ if (isCurrent) {
</div>
{/* 情景 */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></label>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></div>
<div className="p-3 bg-muted/30 rounded-lg border border-border/50">
<p className="text-lg font-medium leading-relaxed">{expr.situation}</p>
</div>
</div>
{/* 风格 */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></label>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></div>
<div className="flex flex-wrap gap-2">
{expr.style.split(/[,]/).map((s, i) => (
<Badge key={i} variant="secondary" className="font-normal">
@@ -1614,14 +1615,14 @@ if (isCurrent) {
</div>
{/* 情景 */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></label>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></div>
<div className="p-3 bg-muted/30 rounded-lg border border-border/50">
<p className="text-lg font-medium leading-relaxed">{expr.situation}</p>
</div>
</div>
{/* 风格 */}
<div className="space-y-1.5">
<label className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></label>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"></div>
<div className="flex flex-wrap gap-2">
{expr.style.split(/[,]/).map((s, i) => (
<Badge key={i} variant="secondary" className="font-normal">

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react'
import { ListFieldEditor } from '@/components'
import { ListFieldEditor } from '@/components/ListFieldEditor'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'