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

@@ -6,7 +6,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{ ignores: ['dist', 'out'] },
jsxA11y.flatConfigs.recommended,
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
@@ -25,10 +25,7 @@ export default tseslint.config(
acc[key] = 'warn'
return acc
}, {}),
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
// 关闭或降级其他规则
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': 'warn',
@@ -37,4 +34,11 @@ export default tseslint.config(
'jsx-a11y/no-autofocus': 'warn',
},
},
{
files: ['**/*.d.ts'],
rules: {
// Ambient global declarations use `var` in TypeScript declaration files.
'no-var': 'off',
},
}
)

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()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
}: Pick<CodeEditorProps, 'height' | 'minHeight' | 'maxHeight' | 'className'>) {
return (
<div
className={`rounded-md border bg-muted animate-pulse ${className}`}
className={`bg-muted animate-pulse rounded-md border ${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}
<Suspense
fallback={
<CodeEditorFallback
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,
}}
className={className}
/>
</div>
}
>
<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'

View File

@@ -1,31 +1,13 @@
import { createRootRoute, createRoute, createRouter, Outlet, redirect } from '@tanstack/react-router'
import {
createRootRoute,
createRoute,
createRouter,
lazyRouteComponent,
Outlet,
redirect,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { IndexPage } from './routes/index'
import { SettingsPage } from './routes/settings'
import { AuthPage } from './routes/auth'
import { SetupPage } from './routes/setup'
import { NotFoundPage } from './routes/404'
import { BotConfigPage } from './routes/config/bot'
import { ModelProviderConfigPage } from './routes/config/modelProvider'
import { ModelConfigPage } from './routes/config/model'
import { AdapterConfigPage } from './routes/config/adapter'
import { EmojiManagementPage } from './routes/resource/emoji'
import { ExpressionManagementPage } from './routes/resource/expression'
import { JargonManagementPage } from './routes/resource/jargon'
import { PersonManagementPage } from './routes/person'
import { KnowledgeGraphPage } from './routes/resource/knowledge-graph'
import { KnowledgeBasePage } from './routes/resource/knowledge-base'
import { LogViewerPage } from './routes/logs'
import { PlannerMonitorPage } from './routes/monitor'
import { PluginsPage } from './routes/plugins'
import { ModelPresetsPage } from './routes/model-presets'
import { PluginConfigPage } from './routes/plugin-config'
import { PluginMirrorsPage } from './routes/plugin-mirrors'
import { PluginDetailPage } from './routes/plugin-detail'
import { ChatPage } from './routes/chat/index'
import { WebUIFeedbackSurveyPage, MaiBotFeedbackSurveyPage } from './routes/survey'
import PackMarketPage from './routes/config/pack-market'
import PackDetailPage from './routes/config/pack-detail'
import { Layout } from './components/layout'
import { checkAuth } from './hooks/use-auth'
import { RouteErrorBoundary } from './components/error-boundary'
@@ -50,14 +32,14 @@ const rootRoute = createRootRoute({
const authRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/auth',
component: AuthPage,
component: lazyRouteComponent(() => import('./routes/auth'), 'AuthPage'),
})
// 首次配置路由(无 Layout
const setupRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/setup',
component: SetupPage,
component: lazyRouteComponent(() => import('./routes/setup/index.tsx'), 'SetupPage'),
})
// 受保护的路由 Root带 Layout
@@ -76,168 +58,192 @@ const protectedRoute = createRoute({
const indexRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/',
component: IndexPage,
component: lazyRouteComponent(() => import('./routes/index'), 'IndexPage'),
})
// 配置路由 - 麦麦主程序配置
const botConfigRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/config/bot',
component: BotConfigPage,
component: lazyRouteComponent(() => import('./routes/config/bot'), 'BotConfigPage'),
})
// 配置路由 - 麦麦模型提供商配置
const modelProviderConfigRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/config/modelProvider',
component: ModelProviderConfigPage,
component: lazyRouteComponent(
() => import('./routes/config/modelProvider/index.tsx'),
'ModelProviderConfigPage'
),
})
// 配置路由 - 麦麦模型配置
const modelConfigRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/config/model',
component: ModelConfigPage,
component: lazyRouteComponent(() => import('./routes/config/model'), 'ModelConfigPage'),
})
// 配置路由 - 麦麦适配器配置
const adapterConfigRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/config/adapter',
component: AdapterConfigPage,
component: lazyRouteComponent(() => import('./routes/config/adapter'), 'AdapterConfigPage'),
})
// 资源管理路由 - 表情包管理
const emojiManagementRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/resource/emoji',
component: EmojiManagementPage,
component: lazyRouteComponent(
() => import('./routes/resource/emoji/index.tsx'),
'EmojiManagementPage'
),
})
// 资源管理路由 - 表达方式管理
const expressionManagementRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/resource/expression',
component: ExpressionManagementPage,
component: lazyRouteComponent(
() => import('./routes/resource/expression/index.tsx'),
'ExpressionManagementPage'
),
})
// 资源管理路由 - 人物信息管理
const personManagementRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/resource/person',
component: PersonManagementPage,
component: lazyRouteComponent(() => import('./routes/person'), 'PersonManagementPage'),
})
// 资源管理路由 - 黑话管理
const jargonManagementRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/resource/jargon',
component: JargonManagementPage,
component: lazyRouteComponent(
() => import('./routes/resource/jargon/index.tsx'),
'JargonManagementPage'
),
})
// 资源管理路由 - 知识库图谱可视化
const knowledgeGraphRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/resource/knowledge-graph',
component: KnowledgeGraphPage,
component: lazyRouteComponent(
() => import('./routes/resource/knowledge-graph/index.tsx'),
'KnowledgeGraphPage'
),
})
// 资源管理路由 - 麦麦知识库管理
const knowledgeBaseRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/resource/knowledge-base',
component: KnowledgeBasePage,
component: lazyRouteComponent(
() => import('./routes/resource/knowledge-base'),
'KnowledgeBasePage'
),
})
// 日志查看器路由
const logsRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/logs',
component: LogViewerPage,
component: lazyRouteComponent(() => import('./routes/logs'), 'LogViewerPage'),
})
// MaiSaka 聊天流监控路由
const plannerMonitorRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/planner-monitor',
component: PlannerMonitorPage,
component: lazyRouteComponent(() => import('./routes/monitor/index.tsx'), 'PlannerMonitorPage'),
})
// 本地聊天室路由
const chatRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/chat',
component: ChatPage,
component: lazyRouteComponent(() => import('./routes/chat/index'), 'ChatPage'),
})
// 插件市场路由
const pluginsRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/plugins',
component: PluginsPage,
component: lazyRouteComponent(() => import('./routes/plugins/index'), 'PluginsPage'),
})
// 插件详情路由
const pluginDetailRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/plugin-detail',
component: PluginDetailPage,
component: lazyRouteComponent(() => import('./routes/plugin-detail'), 'PluginDetailPage'),
})
// 模型分配预设市场路由
const modelPresetsRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/model-presets',
component: ModelPresetsPage,
component: lazyRouteComponent(() => import('./routes/model-presets'), 'ModelPresetsPage'),
})
// 插件配置路由
const pluginConfigRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/plugin-config',
component: PluginConfigPage,
component: lazyRouteComponent(() => import('./routes/plugin-config'), 'PluginConfigPage'),
})
// 插件镜像源配置路由
const pluginMirrorsRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/plugin-mirrors',
component: PluginMirrorsPage,
component: lazyRouteComponent(() => import('./routes/plugin-mirrors'), 'PluginMirrorsPage'),
})
// 设置页路由
const settingsRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/settings',
component: SettingsPage,
component: lazyRouteComponent(() => import('./routes/settings/index.tsx'), 'SettingsPage'),
})
// 配置模板市场路由
const packMarketRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/config/pack-market',
component: PackMarketPage,
component: lazyRouteComponent(() => import('./routes/config/pack-market')),
})
// 配置模板详情路由
export const packDetailRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/config/pack-market/$packId',
component: PackDetailPage,
component: lazyRouteComponent(() => import('./routes/config/pack-detail')),
})
// 问卷调查路由 - WebUI 反馈
const webuiFeedbackSurveyRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/survey/webui-feedback',
component: WebUIFeedbackSurveyPage,
component: lazyRouteComponent(
() => import('./routes/survey/webui-feedback'),
'WebUIFeedbackSurveyPage'
),
})
// 问卷调查路由 - 麦麦体验反馈
const maibotFeedbackSurveyRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/survey/maibot-feedback',
component: MaiBotFeedbackSurveyPage,
component: lazyRouteComponent(
() => import('./routes/survey/maibot-feedback'),
'MaiBotFeedbackSurveyPage'
),
})
// 404 路由

View File

@@ -16,7 +16,7 @@ import {
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { CodeEditor } from '@/components'
import { CodeEditor } from '@/components/CodeEditor'
import { DynamicConfigForm } from '@/components/dynamic-form'
import { RestartOverlay } from '@/components/restart-overlay'
import { useToast } from '@/hooks/use-toast'

View File

@@ -12,7 +12,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import { ListFieldEditor } from '@/components/ListFieldEditor'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { CodeEditor } from '@/components'
import { CodeEditor } from '@/components/CodeEditor'
import { parse as parseToml } from 'smol-toml'
import {
Select,

View File

@@ -37,8 +37,8 @@ import {
type GitStatus,
type MaimaiVersion,
} from '@/lib/plugin-api'
import { MarkdownRenderer } from '@/components/markdown-renderer'
import { PluginStats } from '@/components/plugin-stats'
import { MarkdownRenderer } from '@/components'
import { recordPluginDownload } from '@/lib/plugin-stats'
// 分类名称映射

View File

@@ -16,7 +16,7 @@ import {
Upload,
} from 'lucide-react'
import { CodeEditor } from '@/components'
import { CodeEditor } from '@/components/CodeEditor'
import { MemoryDeleteDialog } from '@/components/memory/MemoryDeleteDialog'
import { MemoryConfigEditor } from '@/components/memory/MemoryConfigEditor'
import { Alert, AlertDescription } from '@/components/ui/alert'

View File

@@ -171,10 +171,14 @@ export function NodeDetailDialog({
</Button>
{onDeleteEntity ? (
<div className="flex flex-col items-end gap-2 rounded-lg border bg-background p-3">
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox checked={includeParagraphs} onCheckedChange={(checked) => setIncludeParagraphs(Boolean(checked))} />
</label>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox
checked={includeParagraphs}
onCheckedChange={(checked) => setIncludeParagraphs(Boolean(checked))}
aria-label="删除该实体相关证据段落"
/>
<span></span>
</div>
<Button variant="outline" onClick={() => onDeleteEntity({ includeParagraphs })}>
<Trash2 className="mr-2 h-4 w-4" />
@@ -280,10 +284,14 @@ export function EdgeDetailDialog({
</Button>
{onDeleteEdgeGroup ? (
<div className="flex flex-col items-end gap-2 rounded-lg border bg-background p-3">
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox checked={includeParagraphs} onCheckedChange={(checked) => setIncludeParagraphs(Boolean(checked))} />
</label>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox
checked={includeParagraphs}
onCheckedChange={(checked) => setIncludeParagraphs(Boolean(checked))}
aria-label="同时删除支撑段落"
/>
<span></span>
</div>
<Button variant="outline" onClick={() => onDeleteEdgeGroup({ includeParagraphs })}>
<Trash2 className="mr-2 h-4 w-4" />
@@ -371,10 +379,14 @@ export function RelationDetailDialog({
{onDeleteRelation ? (
<div className="rounded-lg border bg-background p-3">
<label className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox checked={includeParagraphs} onCheckedChange={(checked) => setIncludeParagraphs(Boolean(checked))} />
</label>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Checkbox
checked={includeParagraphs}
onCheckedChange={(checked) => setIncludeParagraphs(Boolean(checked))}
aria-label="同时删除支撑该关系的段落"
/>
<span></span>
</div>
<Button className="mt-3" variant="outline" onClick={() => onDeleteRelation(relation, includeParagraphs)}>
<Trash2 className="mr-2 h-4 w-4" />