feat: 更新 CodeEditor 组件,重构为懒加载并添加 CodeEditorImpl,优化导入路径
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
105
dashboard/src/components/CodeEditorImpl.tsx
Normal file
105
dashboard/src/components/CodeEditorImpl.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user