mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-31 13:39:48 +08:00
⚡️ perf(webview): 降低首屏加载与 WebView2 内存占用
- Monaco Editor 改为首次使用时按需初始化 - AI 面板改为懒加载,延后加载 Markdown 和图表渲染依赖 - 增加 Windows 低内存视觉模式,支持关闭透明 WebView 和 Acrylic - 补充低内存启动说明与模式解析测试
This commit is contained in:
@@ -11,7 +11,6 @@ import ConnectionPackagePasswordModal from './components/ConnectionPackagePasswo
|
||||
import DataSyncModal from './components/DataSyncModal';
|
||||
import DriverManagerModal from './components/DriverManagerModal';
|
||||
import LogPanel from './components/LogPanel';
|
||||
import AIChatPanel from './components/AIChatPanel';
|
||||
import AISettingsModal from './components/AISettingsModal';
|
||||
import SecurityUpdateBanner from './components/SecurityUpdateBanner';
|
||||
import SecurityUpdateIntroModal from './components/SecurityUpdateIntroModal';
|
||||
@@ -85,6 +84,8 @@ import {
|
||||
import { ApplyDataRootDirectory, GetDataRootDirectoryInfo, GetSavedConnections, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
|
||||
import './App.css';
|
||||
|
||||
const AIChatPanel = React.lazy(() => import('./components/AIChatPanel'));
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
const MIN_UI_SCALE = 0.8;
|
||||
const MAX_UI_SCALE = 1.25;
|
||||
@@ -3007,9 +3008,11 @@ function App() {
|
||||
{renderAIEdgeHandle()}
|
||||
</div>
|
||||
)}
|
||||
<AIChatPanel darkMode={darkMode} bgColor={bgContent} onClose={() => setAIPanelVisible(false)} onOpenSettings={() => {
|
||||
handleOpenAISettings();
|
||||
}} overlayTheme={overlayTheme} />
|
||||
<React.Suspense fallback={<div style={{ width: 360, display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Spin size="small" /></div>}>
|
||||
<AIChatPanel darkMode={darkMode} bgColor={bgContent} onClose={() => setAIPanelVisible(false)} onOpenSettings={() => {
|
||||
handleOpenAISettings();
|
||||
}} overlayTheme={overlayTheme} />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Table, message, Input, Button, Dropdown, MenuProps, Form, Pagination, S
|
||||
import dayjs from 'dayjs';
|
||||
import type { SortOrder, ColumnType } from 'antd/es/table/interface';
|
||||
import { ReloadOutlined, ImportOutlined, ExportOutlined, DownOutlined, PlusOutlined, DeleteOutlined, SaveOutlined, UndoOutlined, FilterOutlined, CloseOutlined, ConsoleSqlOutlined, FileTextOutlined, CopyOutlined, ClearOutlined, EditOutlined, VerticalAlignBottomOutlined, LeftOutlined, RightOutlined, RobotOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import Editor from './MonacoEditor';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import Editor from './MonacoEditor';
|
||||
import { Spin, Alert } from 'antd';
|
||||
import { TabData } from '../types';
|
||||
import { useStore } from '../store';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Editor, { type BeforeMount, type OnMount } from "@monaco-editor/react";
|
||||
import Editor, { type BeforeMount, type OnMount } from "./MonacoEditor";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import Editor from "./MonacoEditor";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
|
||||
106
frontend/src/components/MonacoEditor.tsx
Normal file
106
frontend/src/components/MonacoEditor.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import Editor, { loader, type BeforeMount, type EditorProps } from '@monaco-editor/react';
|
||||
|
||||
export type { BeforeMount, OnMount } from '@monaco-editor/react';
|
||||
|
||||
let monacoConfiguredPromise: Promise<void> | null = null;
|
||||
let transparentThemesRegistered = false;
|
||||
|
||||
const isTestRuntime = (): boolean => {
|
||||
const env = (import.meta as unknown as { env?: Record<string, unknown> }).env || {};
|
||||
return env.MODE === 'test' || env.VITEST === true || env.VITEST === 'true';
|
||||
};
|
||||
|
||||
export const registerGonaviMonacoThemes: BeforeMount = (monaco) => {
|
||||
if (transparentThemesRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
monaco.editor.defineTheme('transparent-dark', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
'editor.lineHighlightBackground': '#ffffff10',
|
||||
'editorGutter.background': '#00000000',
|
||||
'editorStickyScroll.background': '#1e1e1e',
|
||||
'editorStickyScrollHover.background': '#2a2a2a',
|
||||
},
|
||||
});
|
||||
monaco.editor.defineTheme('transparent-light', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#00000000',
|
||||
'editor.lineHighlightBackground': '#00000010',
|
||||
'editorGutter.background': '#00000000',
|
||||
'editorStickyScroll.background': '#ffffff',
|
||||
'editorStickyScrollHover.background': '#f5f5f5',
|
||||
},
|
||||
});
|
||||
|
||||
transparentThemesRegistered = true;
|
||||
};
|
||||
|
||||
const ensureMonacoConfigured = (): Promise<void> => {
|
||||
if (isTestRuntime()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (!monacoConfiguredPromise) {
|
||||
monacoConfiguredPromise = import('monaco-editor/esm/nls.messages.zh-cn')
|
||||
.then(() => import('monaco-editor'))
|
||||
.then((monaco) => {
|
||||
loader.config({ monaco });
|
||||
});
|
||||
}
|
||||
|
||||
return monacoConfiguredPromise;
|
||||
};
|
||||
|
||||
const MonacoEditor: React.FC<EditorProps> = ({ beforeMount, loading, ...props }) => {
|
||||
const [ready, setReady] = useState(isTestRuntime);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void ensureMonacoConfigured()
|
||||
.then(() => {
|
||||
if (!cancelled) {
|
||||
setReady(true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to configure Monaco Editor', error);
|
||||
if (!cancelled) {
|
||||
setReady(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
|
||||
registerGonaviMonacoThemes(monaco);
|
||||
beforeMount?.(monaco);
|
||||
}, [beforeMount]);
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<div
|
||||
data-monaco-editor-loading="true"
|
||||
style={{ height: props.height || '100%', width: props.width || '100%' }}
|
||||
>
|
||||
{loading || null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Editor {...props} loading={loading} beforeMount={handleBeforeMount} />;
|
||||
};
|
||||
|
||||
export default MonacoEditor;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import Editor, { OnMount } from '@monaco-editor/react';
|
||||
import Editor, { type OnMount } from './MonacoEditor';
|
||||
import { Button, message, Modal, Input, Form, Dropdown, MenuProps, Tooltip, Select, Tabs } from 'antd';
|
||||
import { PlayCircleOutlined, SaveOutlined, FormatPainterOutlined, SettingOutlined, CloseOutlined, StopOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import { format } from 'sql-formatter';
|
||||
@@ -924,7 +924,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
editorRef.current = editor;
|
||||
monacoRef.current = monaco;
|
||||
|
||||
// 应用透明主题(主题已在 main.tsx 全局注册)
|
||||
// 应用透明主题(主题由 MonacoEditor 包装组件按需注册)
|
||||
monaco.editor.setTheme(darkMode ? 'transparent-dark' : 'transparent-light');
|
||||
|
||||
// 注册 AI 右键菜单操作
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, Space, message } from 'antd';
|
||||
import { PlayCircleOutlined, ClearOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import Editor, { OnMount } from '@monaco-editor/react';
|
||||
import Editor, { type OnMount } from './MonacoEditor';
|
||||
|
||||
interface RedisCommandEditorProps {
|
||||
connectionId: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { RadioChangeEvent } from 'antd';
|
||||
import { ReloadOutlined, DeleteOutlined, PlusOutlined, EditOutlined, SearchOutlined, ClockCircleOutlined, CopyOutlined, FolderOpenOutlined, KeyOutlined, RightOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { RedisKeyInfo, RedisValue, StreamEntry } from '../types';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import Editor from './MonacoEditor';
|
||||
import type { DataNode } from 'antd/es/tree';
|
||||
import {
|
||||
blurToFilter,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ReloadOutlined, SaveOutlined, PlusOutlined, DeleteOutlined, MenuOutline
|
||||
import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragOverlay } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import Editor, { loader } from '@monaco-editor/react';
|
||||
import Editor from './MonacoEditor';
|
||||
import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, TriggerDefinition } from '../types';
|
||||
import { useStore } from '../store';
|
||||
import { DBGetColumns, DBGetIndexes, DBQuery, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App';
|
||||
@@ -460,7 +460,7 @@ const TableDesigner: React.FC<{ tab: TabData }> = ({ tab }) => {
|
||||
setCommentEditorValue('');
|
||||
}, []);
|
||||
|
||||
// 透明 Monaco Editor 主题已在 main.tsx 全局注册(含 stickyScroll 不透明背景)
|
||||
// 透明 Monaco Editor 主题由 MonacoEditor 包装组件按需注册(含 stickyScroll 不透明背景)
|
||||
|
||||
// 监听字段 Tab 容器高度,为所有 Tab 内表格计算 scroll.y
|
||||
// 当 Tab 切换时,字段 Tab 被 display:none 导致 height=0,跳过该次更新保持有效值
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import Editor, { type BeforeMount, type OnMount } from '@monaco-editor/react';
|
||||
import Editor, { type BeforeMount, type OnMount } from './MonacoEditor';
|
||||
|
||||
interface TableDesignerSqlPreviewProps {
|
||||
sql: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import Editor from './MonacoEditor';
|
||||
import { Spin, Alert } from 'antd';
|
||||
import { TabData } from '../types';
|
||||
import { useStore } from '../store';
|
||||
@@ -20,7 +20,7 @@ const TriggerViewer: React.FC<TriggerViewerProps> = ({ tab }) => {
|
||||
const theme = useStore(state => state.theme);
|
||||
const darkMode = theme === 'dark';
|
||||
|
||||
// 透明 Monaco Editor 主题已在 main.tsx 全局注册(含 stickyScroll 不透明背景)
|
||||
// 透明 Monaco Editor 主题由 MonacoEditor 包装组件按需注册(含 stickyScroll 不透明背景)
|
||||
|
||||
const escapeSQLLiteral = (raw: string): string => String(raw || '').replace(/'/g, "''");
|
||||
const quoteSqlServerIdentifier = (raw: string): string => `[${String(raw || '').replace(/]/g, ']]')}]`;
|
||||
|
||||
@@ -9,14 +9,7 @@ import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 全局配置 Monaco Editor 使用本地打包的文件,避免从 CDN (jsdelivr) 加载。
|
||||
// Windows WebView2 环境下访问外部 CDN 可能失败,导致编辑器一直显示 Loading。
|
||||
// 中文语言包必须在 monaco-editor 主包之前导入,否则右键菜单等 UI 仍为英文。
|
||||
import 'monaco-editor/esm/nls.messages.zh-cn'
|
||||
import { loader } from '@monaco-editor/react'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import { cloneBrowserMockValue, duplicateBrowserMockConnection, resolveBrowserMockSecretFlag } from './utils/browserMockConnections'
|
||||
loader.config({ monaco })
|
||||
|
||||
if (typeof window !== 'undefined' && !(window as any).go) {
|
||||
const mockConnections: any[] = [];
|
||||
@@ -168,16 +161,6 @@ if (typeof window !== 'undefined' && !(window as any).go) {
|
||||
}
|
||||
};
|
||||
}
|
||||
// 全局注册透明主题,避免每个 Editor 组件 beforeMount 中重复定义
|
||||
monaco.editor.defineTheme('transparent-dark', {
|
||||
base: 'vs-dark', inherit: true, rules: [],
|
||||
colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#ffffff10', 'editorGutter.background': '#00000000', 'editorStickyScroll.background': '#1e1e1e', 'editorStickyScrollHover.background': '#2a2a2a' }
|
||||
})
|
||||
monaco.editor.defineTheme('transparent-light', {
|
||||
base: 'vs', inherit: true, rules: [],
|
||||
colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#00000010', 'editorGutter.background': '#00000000', 'editorStickyScroll.background': '#ffffff', 'editorStickyScrollHover.background': '#f5f5f5' }
|
||||
})
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
2
frontend/src/vite-env.d.ts
vendored
2
frontend/src/vite-env.d.ts
vendored
@@ -1,5 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module 'monaco-editor/esm/nls.messages.zh-cn';
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_GONAVI_ENABLE_MAC_WINDOW_DIAGNOSTICS?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user