mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-15 20:37:52 +08:00
- 保存逻辑:外部 SQL 文件标签页携带 filePath,保存时写回原始磁盘文件 - 后端接口:新增 WriteSQLFile 能力,支持覆盖已有 SQL 文件并保留原文件权限 - 状态隔离:外部文件保存失败时不创建 savedQuery,避免写入 localStorage 副本 - 兼容行为:非文件标签页继续沿用原有 savedQuery 快速保存逻辑 - 文案优化:将数据库下入口改为“外部 SQL 目录”,减少与单文件打开入口的歧义 - 测试覆盖:补充前端保存分支、后端写文件边界和外部 SQL 目录文案测试 Refs #422
189 lines
11 KiB
TypeScript
189 lines
11 KiB
TypeScript
import React from 'react'
|
||
import ReactDOM from 'react-dom/client'
|
||
import App from './App'
|
||
// import './index.css' // Optional global styles
|
||
|
||
// 全局配置 dayjs 使用中文 locale,使 Ant Design 的 DatePicker/TimePicker 等组件
|
||
// 的月份、星期等文本显示为中文。必须在 Ant Design 组件渲染前完成配置。
|
||
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[] = [];
|
||
let mockGlobalProxy: any = { enabled: false, type: 'socks5', host: '', port: 1080, user: '', password: '', hasPassword: false };
|
||
let mockDataRootInfo: any = {
|
||
path: 'C:/mock/.gonavi',
|
||
defaultPath: 'C:/mock/.gonavi',
|
||
driverPath: 'C:/mock/.gonavi/drivers',
|
||
isDefaultPath: true,
|
||
bootstrapPath: 'C:/mock/.gonavi/storage_root.json',
|
||
};
|
||
|
||
const upsertMockConnection = (view: any) => {
|
||
const index = mockConnections.findIndex((item) => item.id === view.id);
|
||
if (index >= 0) {
|
||
mockConnections[index] = view;
|
||
return;
|
||
}
|
||
mockConnections.push(view);
|
||
};
|
||
|
||
const saveMockConnection = (input: any) => {
|
||
const existing = mockConnections.find((item) => item.id === input?.id);
|
||
const config = (input?.config && typeof input.config === 'object') ? input.config : {};
|
||
const ssh = (config.ssh && typeof config.ssh === 'object') ? config.ssh : {};
|
||
const proxy = (config.proxy && typeof config.proxy === 'object') ? config.proxy : {};
|
||
const httpTunnel = (config.httpTunnel && typeof config.httpTunnel === 'object') ? config.httpTunnel : {};
|
||
const nextId = String(input?.id || existing?.id || `mock-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
||
const view = {
|
||
id: nextId,
|
||
name: String(input?.name || existing?.name || '未命名连接'),
|
||
config: {
|
||
...config,
|
||
id: nextId,
|
||
password: '',
|
||
ssh: { ...ssh, password: '' },
|
||
proxy: { ...proxy, password: '' },
|
||
httpTunnel: { ...httpTunnel, password: '' },
|
||
uri: '',
|
||
dsn: '',
|
||
mysqlReplicaPassword: '',
|
||
mongoReplicaPassword: '',
|
||
},
|
||
includeDatabases: Array.isArray(input?.includeDatabases) ? [...input.includeDatabases] : existing?.includeDatabases,
|
||
includeRedisDatabases: Array.isArray(input?.includeRedisDatabases) ? [...input.includeRedisDatabases] : existing?.includeRedisDatabases,
|
||
iconType: typeof input?.iconType === 'string' ? input.iconType : (existing?.iconType || ''),
|
||
iconColor: typeof input?.iconColor === 'string' ? input.iconColor : (existing?.iconColor || ''),
|
||
hasPrimaryPassword: resolveBrowserMockSecretFlag(config.password, !!input?.clearPrimaryPassword, existing?.hasPrimaryPassword),
|
||
hasSSHPassword: resolveBrowserMockSecretFlag(ssh.password, !!input?.clearSSHPassword, existing?.hasSSHPassword),
|
||
hasProxyPassword: resolveBrowserMockSecretFlag(proxy.password, !!input?.clearProxyPassword, existing?.hasProxyPassword),
|
||
hasHttpTunnelPassword: resolveBrowserMockSecretFlag(httpTunnel.password, !!input?.clearHttpTunnelPassword, existing?.hasHttpTunnelPassword),
|
||
hasMySQLReplicaPassword: resolveBrowserMockSecretFlag(config.mysqlReplicaPassword, !!input?.clearMySQLReplicaPassword, existing?.hasMySQLReplicaPassword),
|
||
hasMongoReplicaPassword: resolveBrowserMockSecretFlag(config.mongoReplicaPassword, !!input?.clearMongoReplicaPassword, existing?.hasMongoReplicaPassword),
|
||
hasOpaqueURI: resolveBrowserMockSecretFlag(config.uri, !!input?.clearOpaqueURI, existing?.hasOpaqueURI),
|
||
hasOpaqueDSN: resolveBrowserMockSecretFlag(config.dsn, !!input?.clearOpaqueDSN, existing?.hasOpaqueDSN),
|
||
};
|
||
upsertMockConnection(view);
|
||
return cloneBrowserMockValue(view);
|
||
};
|
||
|
||
const saveMockGlobalProxy = (input: any) => {
|
||
const nextPassword = String(input?.password ?? '');
|
||
mockGlobalProxy = {
|
||
...mockGlobalProxy,
|
||
...input,
|
||
password: '',
|
||
hasPassword: nextPassword !== '' ? true : !!mockGlobalProxy.hasPassword,
|
||
};
|
||
return cloneBrowserMockValue(mockGlobalProxy);
|
||
};
|
||
|
||
(window as any).go = {
|
||
app: {
|
||
App: {
|
||
CheckUpdate: async () => ({ success: false }),
|
||
DownloadUpdate: async () => ({ success: false }),
|
||
GetSavedConnections: async () => cloneBrowserMockValue(mockConnections),
|
||
SaveConnection: async (input: any) => saveMockConnection(input),
|
||
DeleteConnection: async (id: string) => {
|
||
const index = mockConnections.findIndex((item) => item.id === id);
|
||
if (index >= 0) {
|
||
mockConnections.splice(index, 1);
|
||
}
|
||
return null;
|
||
},
|
||
DuplicateConnection: async (id: string) => {
|
||
const existing = mockConnections.find((item) => item.id === id);
|
||
if (!existing) return null;
|
||
const duplicated = duplicateBrowserMockConnection({
|
||
existing,
|
||
items: mockConnections,
|
||
nextId: `mock-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||
});
|
||
mockConnections.push(duplicated);
|
||
return cloneBrowserMockValue(duplicated);
|
||
},
|
||
ImportLegacyConnections: async (items: any[]) => items.map((item) => saveMockConnection(item)),
|
||
OpenConnection: async () => null,
|
||
CloseConnection: async () => null,
|
||
GetDatabases: async () => [],
|
||
GetTables: async () => [],
|
||
GetTableData: async () => ({ columns: [], rows: [], total: 0 }),
|
||
GetTableColumns: async () => [],
|
||
ExecuteQuery: async () => ({ columns: [], rows: [], time: 0 }),
|
||
GetSavedQueries: async () => [],
|
||
SaveQuery: async () => null,
|
||
DeleteQuery: async () => null,
|
||
GetAppInfo: async () => ({}),
|
||
GetDataRootDirectoryInfo: async () => ({ success: true, data: cloneBrowserMockValue(mockDataRootInfo) }),
|
||
CheckForUpdates: async () => ({ success: false }),
|
||
CheckForUpdatesSilently: async () => ({ success: false }),
|
||
OpenDownloadedUpdateDirectory: async () => ({ success: false }),
|
||
OpenDriverDownloadDirectory: async (path: string) => ({ success: true, data: { path } }),
|
||
OpenDataRootDirectory: async () => ({ success: true }),
|
||
SelectSQLDirectory: async (currentPath: string) => ({ success: false, message: currentPath ? '已取消' : '已取消' }),
|
||
ListSQLDirectory: async () => ({ success: true, data: [] }),
|
||
ReadSQLFile: async () => ({ success: false, message: '已取消' }),
|
||
WriteSQLFile: async (_filePath: string, _content: string) => ({ success: true }),
|
||
InstallUpdateAndRestart: async () => ({ success: false }),
|
||
ImportConfigFile: async () => ({ success: false, message: '已取消' }),
|
||
ImportConnectionsPayload: async (raw: string, _password?: string) => {
|
||
try {
|
||
const parsed = JSON.parse(raw);
|
||
if (Array.isArray(parsed)) {
|
||
return parsed.map((item) => saveMockConnection(item));
|
||
}
|
||
} catch {
|
||
throw new Error('浏览器 mock 不支持恢复包导入,仅支持历史 JSON 连接数组');
|
||
}
|
||
throw new Error('浏览器 mock 不支持恢复包导入,仅支持历史 JSON 连接数组');
|
||
},
|
||
ExportConnectionsPackage: async (_options?: { includeSecrets?: boolean; filePassword?: string }) => ({ success: false, message: '浏览器 mock 不支持恢复包导出' }),
|
||
ExportData: async () => ({ success: false }),
|
||
GetGlobalProxyConfig: async () => ({ success: true, data: cloneBrowserMockValue(mockGlobalProxy) }),
|
||
SaveGlobalProxy: async (input: any) => saveMockGlobalProxy(input),
|
||
ImportLegacyGlobalProxy: async (input: any) => saveMockGlobalProxy(input),
|
||
SelectDataRootDirectory: async (currentPath: string) => ({ success: true, data: { ...mockDataRootInfo, path: currentPath || mockDataRootInfo.path } }),
|
||
ApplyDataRootDirectory: async (path: string) => {
|
||
const nextPath = String(path || mockDataRootInfo.defaultPath);
|
||
mockDataRootInfo = {
|
||
...mockDataRootInfo,
|
||
path: nextPath,
|
||
driverPath: `${nextPath}/drivers`,
|
||
isDefaultPath: nextPath === mockDataRootInfo.defaultPath,
|
||
};
|
||
return { success: true, message: '数据目录已更新', data: cloneBrowserMockValue(mockDataRootInfo) };
|
||
},
|
||
}
|
||
}
|
||
};
|
||
}
|
||
// 全局注册透明主题,避免每个 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 />
|
||
</React.StrictMode>,
|
||
)
|
||
|
||
|
||
|