import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { Layout, Button, ConfigProvider, theme, message, Modal, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Tooltip } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined } from '@ant-design/icons'; import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import ConnectionModal from './components/ConnectionModal'; 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 { useStore } from './store'; import { SavedConnection } from './types'; import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance'; import { getMacNativeTitlebarPaddingLeft, getMacNativeTitlebarPaddingRight, shouldHandleMacNativeFullscreenShortcut, shouldSuppressMacNativeEscapeExit } from './utils/macWindow'; import { buildOverlayWorkbenchTheme } from './utils/overlayWorkbenchTheme'; import { getConnectionWorkbenchState } from './utils/startupReadiness'; import { SHORTCUT_ACTION_META, SHORTCUT_ACTION_ORDER, ShortcutAction, eventToShortcut, getShortcutDisplay, hasModifierKey, isEditableElement, isShortcutMatch, normalizeShortcutCombo, } from './utils/shortcuts'; import { SIDEBAR_UTILITY_ITEM_KEYS, resolveAIEntryPlacement, resolveAIEdgeHandleAttachment, resolveAIEdgeHandleDockStyle, resolveAIEdgeHandleStyle, } from './utils/aiEntryLayout'; import { ConfigureGlobalProxy, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App'; import './App.css'; const { Sider, Content } = Layout; const MIN_UI_SCALE = 0.8; const MAX_UI_SCALE = 1.25; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 20; const DEFAULT_UI_SCALE = 1.0; const DEFAULT_FONT_SIZE = 14; const detectNavigatorPlatform = (): string => { if (typeof navigator === 'undefined') { return ''; } const uaDataPlatform = (navigator as Navigator & { userAgentData?: { platform?: string }; }).userAgentData?.platform; if (uaDataPlatform) { return uaDataPlatform; } return navigator.userAgent || ''; }; function App() { const [isModalOpen, setIsModalOpen] = useState(false); const [isSyncModalOpen, setIsSyncModalOpen] = useState(false); const [isDriverModalOpen, setIsDriverModalOpen] = useState(false); const [editingConnection, setEditingConnection] = useState(null); const themeMode = useStore(state => state.theme); const setTheme = useStore(state => state.setTheme); const appearance = useStore(state => state.appearance); const setAppearance = useStore(state => state.setAppearance); const uiScale = useStore(state => state.uiScale); const setUiScale = useStore(state => state.setUiScale); const fontSize = useStore(state => state.fontSize); const setFontSize = useStore(state => state.setFontSize); const startupFullscreen = useStore(state => state.startupFullscreen); const setStartupFullscreen = useStore(state => state.setStartupFullscreen); const globalProxy = useStore(state => state.globalProxy); const setGlobalProxy = useStore(state => state.setGlobalProxy); const shortcutOptions = useStore(state => state.shortcutOptions); const updateShortcut = useStore(state => state.updateShortcut); const resetShortcutOptions = useStore(state => state.resetShortcutOptions); const darkMode = themeMode === 'dark'; const effectiveUiScale = Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Number(uiScale) || DEFAULT_UI_SCALE)); const effectiveFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(Number(fontSize) || DEFAULT_FONT_SIZE))); const tokenFontSize = Math.round(effectiveFontSize * effectiveUiScale); const tokenFontSizeSM = Math.max(10, Math.round(tokenFontSize * 0.86)); const tokenFontSizeLG = Math.max(tokenFontSize + 1, Math.round(tokenFontSize * 1.14)); const tokenControlHeight = Math.max(24, Math.round(32 * effectiveUiScale)); const tokenControlHeightSM = Math.max(20, Math.round(24 * effectiveUiScale)); const tokenControlHeightLG = Math.max(30, Math.round(40 * effectiveUiScale)); const appComponentSize: 'small' | 'middle' | 'large' = effectiveUiScale <= 0.92 ? 'small' : (effectiveUiScale >= 1.12 ? 'large' : 'middle'); const titleBarHeight = Math.max(28, Math.round(32 * effectiveUiScale)); const titleBarButtonWidth = Math.max(40, Math.round(46 * effectiveUiScale)); const floatingLogButtonHeight = Math.max(30, Math.round(34 * effectiveUiScale)); const resolvedAppearance = resolveAppearanceValues(appearance); const effectiveOpacity = normalizeOpacityForPlatform(resolvedAppearance.opacity); const effectiveBlur = normalizeBlurForPlatform(resolvedAppearance.blur); const blurFilter = blurToFilter(effectiveBlur); const windowCornerRadius = 14; const [runtimePlatform, setRuntimePlatform] = useState(''); const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); const [hasAppliedInitialGlobalProxy, setHasAppliedInitialGlobalProxy] = useState(false); const sidebarWidth = useStore(state => state.sidebarWidth); const setSidebarWidth = useStore(state => state.setSidebarWidth); const aiPanelVisible = useStore(state => state.aiPanelVisible); const toggleAIPanel = useStore(state => state.toggleAIPanel); const setAIPanelVisible = useStore(state => state.setAIPanelVisible); const globalProxyInvalidHintShownRef = React.useRef(false); const connectionWorkbenchState = getConnectionWorkbenchState(isStoreHydrated, hasAppliedInitialGlobalProxy); // 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView, // 避免 GPU 持续计算窗口背后的模糊合成 useEffect(() => { try { void SetWindowTranslucency(resolvedAppearance.opacity, resolvedAppearance.blur).catch(() => undefined); } catch(e) { /* ignore */ } }, [resolvedAppearance.blur, resolvedAppearance.opacity]); useEffect(() => { let cancelled = false; try { Environment() .then((env) => { if (cancelled) return; const platform = String(env?.platform || '').toLowerCase(); setRuntimePlatform(platform); setIsLinuxRuntime(platform === 'linux'); }) .catch(() => { if (cancelled) return; const platform = detectNavigatorPlatform(); const normalized = /linux/i.test(platform) ? 'linux' : (/mac/i.test(platform) ? 'darwin' : (/win/i.test(platform) ? 'windows' : '')); setRuntimePlatform(normalized); setIsLinuxRuntime(normalized === 'linux'); }); } catch(e) { if (cancelled) return; const platform = detectNavigatorPlatform(); const normalized = /linux/i.test(platform) ? 'linux' : (/mac/i.test(platform) ? 'darwin' : (/win/i.test(platform) ? 'windows' : '')); setRuntimePlatform(normalized); setIsLinuxRuntime(normalized === 'linux'); } return () => { cancelled = true; }; }, []); useEffect(() => { if (isStoreHydrated) { return; } const unsubscribe = useStore.persist.onFinishHydration(() => { setIsStoreHydrated(true); }); return () => { unsubscribe(); }; }, [isStoreHydrated]); useEffect(() => { if (!isStoreHydrated) { return; } const host = String(globalProxy.host || '').trim(); const port = Number(globalProxy.port); const portValid = Number.isFinite(port) && port > 0 && port <= 65535; const invalidWhenEnabled = globalProxy.enabled && (!host || !portValid); if (invalidWhenEnabled) { if (!globalProxyInvalidHintShownRef.current) { void message.warning({ content: '全局代理已开启,但地址或端口无效,当前按未启用处理', key: 'global-proxy-invalid', }); globalProxyInvalidHintShownRef.current = true; } } else { globalProxyInvalidHintShownRef.current = false; void message.destroy('global-proxy-invalid'); } const enabledForBackend = globalProxy.enabled && !invalidWhenEnabled; let cancelled = false; try { ConfigureGlobalProxy(enabledForBackend, { type: globalProxy.type, host, port: portValid ? port : (globalProxy.type === 'http' ? 8080 : 1080), user: String(globalProxy.user || '').trim(), password: globalProxy.password || '', }) .then((res) => { if (cancelled || res?.success) { return; } void message.error({ content: '全局代理配置失败: ' + (res?.message || '未知错误'), key: 'global-proxy-sync-error', }); }) .catch((err) => { if (cancelled) { return; } const errMsg = err instanceof Error ? err.message : String(err || '未知错误'); void message.error({ content: '全局代理配置失败: ' + errMsg, key: 'global-proxy-sync-error', }); }) .finally(() => { if (!cancelled) { setHasAppliedInitialGlobalProxy(true); } }); } catch (e) { if (!cancelled) { setHasAppliedInitialGlobalProxy(true); } console.warn("Wails API: ConfigureGlobalProxy unavailable", e); } return () => { cancelled = true; }; }, [ isStoreHydrated, globalProxy.enabled, globalProxy.type, globalProxy.host, globalProxy.port, globalProxy.user, globalProxy.password, ]); useEffect(() => { let cancelled = false; let startupWindowTimer: number | null = null; const maxApplyAttempts = 6; const applyRetryDelayMs = 400; const settleDelayMs = 160; const useMaximiseForStartup = isWindowsPlatform(); const checkStartupPreferenceApplied = async (): Promise => { try { if (await WindowIsFullscreen()) { return true; } } catch (_) { // ignore } try { if (await WindowIsMaximised()) { return true; } } catch (_) { // ignore } return false; }; const applyStartupWindowPreference = (attempt: number) => { if (startupWindowTimer !== null) { window.clearTimeout(startupWindowTimer); } startupWindowTimer = window.setTimeout(() => { if (cancelled) { return; } if (!useStore.getState().startupFullscreen) { return; } void Promise.resolve() .then(async () => { if (await checkStartupPreferenceApplied()) { return; } // Windows 使用最大化,避免进入真正全屏后无法通过标题栏交互退出。 // 其他平台保持全屏优先、最大化兜底。 try { if (useMaximiseForStartup) { await WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); } else { await WindowFullscreen(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); if (await checkStartupPreferenceApplied()) { return; } await WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); } } catch (e) { console.warn("Wails Window APIs unavailable", e); } if (await checkStartupPreferenceApplied()) { return; } if (attempt < maxApplyAttempts) { applyStartupWindowPreference(attempt + 1); } }); }, applyRetryDelayMs); }; const restoreWindowState = async () => { if (cancelled) return; const state = useStore.getState(); // startupFullscreen 设置优先 if (state.startupFullscreen) { applyStartupWindowPreference(1); return; } // 根据上次保存的窗口状态恢复 const savedState = state.windowState; if (savedState === 'fullscreen') { applyStartupWindowPreference(1); return; } if (savedState === 'maximized') { try { await WindowMaximise(); } catch (_) {} return; } // 普通窗口:恢复尺寸和位置 const bounds = state.windowBounds; if (!bounds || bounds.width < 400 || bounds.height < 300) return; try { WindowSetSize(bounds.width, bounds.height); WindowSetPosition(bounds.x, bounds.y); } catch (e) { console.warn('Failed to restore window bounds', e); } }; if (useStore.persist.hasHydrated()) { void restoreWindowState(); } const unsubscribeHydration = useStore.persist.onFinishHydration(() => { if (cancelled) { return; } void restoreWindowState(); }); return () => { cancelled = true; if (startupWindowTimer !== null) { window.clearTimeout(startupWindowTimer); } unsubscribeHydration(); }; }, []); // 定时保存窗口状态、尺寸与位置 useEffect(() => { const SAVE_INTERVAL_MS = 2000; let lastSaved = ''; const saveWindowState = async () => { try { const [isFs, isMax] = await Promise.all([ WindowIsFullscreen().catch(() => false), WindowIsMaximised().catch(() => false), ]); // 保存窗口状态 const store = useStore.getState(); const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal'); if (store.windowState !== newState) { store.setWindowState(newState); } // 只在普通窗口模式下保存尺寸和位置 if (isFs || isMax) return; const [size, pos] = await Promise.all([ WindowGetSize().catch(() => null), WindowGetPosition().catch(() => null), ]); if (!size || !pos) return; const w = Math.trunc(Number(size.w || 0)); const h = Math.trunc(Number(size.h || 0)); const x = Math.trunc(Number(pos.x || 0)); const y = Math.trunc(Number(pos.y || 0)); if (w < 400 || h < 300) return; const key = `${w},${h},${x},${y}`; if (key === lastSaved) return; lastSaved = key; store.setWindowBounds({ width: w, height: h, x, y }); } catch (e) { // 静默忽略 } }; const timer = window.setInterval(saveWindowState, SAVE_INTERVAL_MS); return () => window.clearInterval(timer); }, []); useEffect(() => { if (!isWindowsPlatform()) { return; } let cancelled = false; let inFlight = false; let lastRatio = Number(window.devicePixelRatio) || 1; let lastFixAt = 0; let activationTimer: number | null = null; const wait = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); const fixWindowScaleIfNeeded = async () => { if (cancelled || inFlight) return; const now = Date.now(); if (now - lastFixAt < 700) return; inFlight = true; try { const [isFullscreen, isMaximised] = await Promise.all([ WindowIsFullscreen().catch(() => false), WindowIsMaximised().catch(() => false), ]); // 避免在全屏/最大化状态下强制改尺寸;这两种状态通常能自行保持 DPI 同步。 if (isFullscreen || isMaximised) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } const size = await WindowGetSize().catch(() => null); const width = Math.trunc(Number(size?.w || 0)); const height = Math.trunc(Number(size?.h || 0)); if (width <= 0 || height <= 0) { window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); return; } const nudgedWidth = width > 480 ? width - 1 : width + 1; try { WindowSetSize(nudgedWidth, height); await wait(28); WindowSetSize(width, height); } catch(e) {} window.dispatchEvent(new Event('resize')); lastFixAt = Date.now(); } catch(e) { console.warn("Wails Window APIs unavailable in fixWindowScaleIfNeeded", e); } finally { inFlight = false; } }; const checkDevicePixelRatio = () => { if (cancelled) return; const currentRatio = Number(window.devicePixelRatio) || 1; if (Math.abs(currentRatio - lastRatio) < 0.02) { return; } lastRatio = currentRatio; void fixWindowScaleIfNeeded(); }; const scheduleActivationFix = () => { if (cancelled) return; if (activationTimer !== null) { window.clearTimeout(activationTimer); } activationTimer = window.setTimeout(() => { activationTimer = null; if (cancelled) return; void fixWindowScaleIfNeeded(); }, 80); }; const handleWindowFocus = () => { if (cancelled) return; checkDevicePixelRatio(); scheduleActivationFix(); }; const handleVisibilityChange = () => { if (cancelled) return; if (document.visibilityState !== 'visible') { return; } checkDevicePixelRatio(); scheduleActivationFix(); }; const handlePageShow = () => { if (cancelled) return; checkDevicePixelRatio(); scheduleActivationFix(); }; const pollTimer = window.setInterval(checkDevicePixelRatio, 900); window.addEventListener('resize', checkDevicePixelRatio); window.addEventListener('focus', handleWindowFocus); window.addEventListener('pageshow', handlePageShow); document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; if (activationTimer !== null) { window.clearTimeout(activationTimer); } window.clearInterval(pollTimer); window.removeEventListener('resize', checkDevicePixelRatio); window.removeEventListener('focus', handleWindowFocus); window.removeEventListener('pageshow', handlePageShow); document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); // Background Helper const getBg = (darkHex: string) => { if (!darkMode) return `rgba(255, 255, 255, ${effectiveOpacity})`; // Light mode usually white // Parse hex to rgb const hex = darkHex.replace('#', ''); const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return `rgba(${r}, ${g}, ${b}, ${effectiveOpacity})`; }; // Specific colors const bgMain = getBg('#141414'); const bgContent = getBg('#1d1d1d'); const floatingLogButtonBorderColor = darkMode ? 'rgba(255,255,255,0.20)' : 'rgba(0,0,0,0.16)'; const floatingLogButtonTextColor = darkMode ? 'rgba(255,255,255,0.92)' : 'rgba(0,0,0,0.82)'; const floatingLogButtonBgColor = darkMode ? `rgba(34, 34, 34, ${Math.max(effectiveOpacity, 0.82)})` : `rgba(255, 255, 255, ${Math.max(effectiveOpacity, 0.9)})`; const floatingLogButtonShadow = darkMode ? '0 8px 22px rgba(0,0,0,0.38)' : '0 8px 20px rgba(0,0,0,0.16)'; const isOpaqueUtilityMode = resolvedAppearance.opacity >= 0.999 && resolvedAppearance.blur <= 0; const utilityButtonBgAlpha = darkMode ? Math.max(0.28, Math.min(0.76, effectiveOpacity * 0.72)) : Math.max(0.52, Math.min(0.92, effectiveOpacity * 0.9)); const utilityButtonBgColor = isOpaqueUtilityMode ? 'transparent' : (darkMode ? `rgba(20, 26, 38, ${utilityButtonBgAlpha})` : `rgba(255, 255, 255, ${utilityButtonBgAlpha})`); const utilityButtonBorderColor = isOpaqueUtilityMode ? (darkMode ? 'rgba(255,255,255,0.12)' : 'rgba(16,24,40,0.10)') : (darkMode ? `rgba(255,255,255,${Math.max(0.08, Math.min(0.18, effectiveOpacity * 0.16))})` : `rgba(16,24,40,${Math.max(0.06, Math.min(0.14, effectiveOpacity * 0.12))})`); const utilityButtonShadow = isOpaqueUtilityMode ? 'none' : (darkMode ? `0 8px 18px rgba(0,0,0,${Math.max(0.10, Math.min(0.22, effectiveOpacity * 0.24))})` : `0 8px 18px rgba(15,23,42,${Math.max(0.04, Math.min(0.12, effectiveOpacity * 0.12))})`); const isSidebarNarrow = sidebarWidth < 360; const isSidebarCompact = sidebarWidth < 320; const isSidebarUltraCompact = sidebarWidth < 260; const utilityButtonStyle = useMemo(() => ({ height: Math.max(30, Math.round(32 * effectiveUiScale)), width: '100%', paddingInline: isSidebarCompact ? Math.max(8, Math.round(9 * effectiveUiScale)) : Math.max(10, Math.round(12 * effectiveUiScale)), borderRadius: 10, border: `1px solid ${utilityButtonBorderColor}`, background: utilityButtonBgColor, color: darkMode ? 'rgba(255,255,255,0.94)' : '#162033', boxShadow: utilityButtonShadow, backdropFilter: isOpaqueUtilityMode ? 'none' : blurFilter, WebkitBackdropFilter: isOpaqueUtilityMode ? 'none' : blurFilter, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: isSidebarCompact ? 4 : 6, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: isSidebarCompact ? 13 : 14, }), [blurFilter, darkMode, effectiveUiScale, isOpaqueUtilityMode, isSidebarCompact, utilityButtonBgColor, utilityButtonBorderColor, utilityButtonShadow]); const overlayTheme = useMemo(() => buildOverlayWorkbenchTheme(darkMode), [darkMode]); const sidebarQuickActionBaseStyle = useMemo(() => ({ height: Math.max(34, Math.round(36 * effectiveUiScale)), borderRadius: 12, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8, paddingInline: Math.max(12, Math.round(14 * effectiveUiScale)), fontWeight: 700, boxShadow: darkMode ? '0 8px 18px rgba(0,0,0,0.16)' : '0 8px 16px rgba(15,23,42,0.08)', backdropFilter: blurFilter, WebkitBackdropFilter: blurFilter, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }), [blurFilter, darkMode, effectiveUiScale]); const sidebarQueryActionStyle = useMemo(() => ({ ...sidebarQuickActionBaseStyle, flex: '1 1 0', border: `1px solid ${darkMode ? 'rgba(255,255,255,0.12)' : 'rgba(16,24,40,0.10)'}`, background: darkMode ? `rgba(255,255,255,0.05)` : 'rgba(255,255,255,0.88)', color: darkMode ? 'rgba(255,255,255,0.92)' : '#162033', }), [darkMode, sidebarQuickActionBaseStyle]); const sidebarCreateConnectionActionStyle = useMemo(() => ({ ...sidebarQuickActionBaseStyle, flex: '1 1 0', border: 'none', background: 'linear-gradient(135deg, rgba(255,214,102,0.96) 0%, rgba(240,183,39,0.92) 100%)', color: '#2a1f00', }), [sidebarQuickActionBaseStyle]); const utilityModalShellStyle = useMemo(() => ({ background: overlayTheme.shellBg, border: overlayTheme.shellBorder, boxShadow: overlayTheme.shellShadow, backdropFilter: overlayTheme.shellBackdropFilter, }), [overlayTheme]); const utilityPanelStyle = useMemo(() => ({ padding: 16, borderRadius: 14, border: overlayTheme.sectionBorder, background: overlayTheme.sectionBg, }), [overlayTheme]); const utilityMutedTextStyle = useMemo(() => ({ color: overlayTheme.mutedText, fontSize: 12, lineHeight: 1.6, }), [overlayTheme]); const renderUtilityModalTitle = (icon: React.ReactNode, title: string, description: string) => (
{icon}
{title}
{description}
); const utilityActionCardStyle = useMemo(() => ({ width: '100%', minHeight: 68, borderRadius: 14, border: overlayTheme.sectionBorder, background: overlayTheme.sectionBg, color: overlayTheme.titleText, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 14, paddingInline: 16, boxShadow: 'none', fontSize: 15, fontWeight: 600, }), [overlayTheme]); const utilityActionHintStyle = useMemo(() => ({ fontSize: 12, color: overlayTheme.mutedText, fontWeight: 400, marginTop: 2, }), [overlayTheme]); const sidebarHorizontalPadding = isSidebarCompact ? 8 : 10; const addTab = useStore(state => state.addTab); const activeContext = useStore(state => state.activeContext); const connections = useStore(state => state.connections); const addConnection = useStore(state => state.addConnection); const tabs = useStore(state => state.tabs); const activeTabId = useStore(state => state.activeTabId); const updateCheckInFlightRef = React.useRef(false); const updateDownloadInFlightRef = React.useRef(false); const updateUserDismissedRef = React.useRef(false); const updateDownloadedVersionRef = React.useRef(null); const updateInstallTriggeredVersionRef = React.useRef(null); const updateDownloadMetaRef = React.useRef(null); const updateNotifiedVersionRef = React.useRef(null); const updateMutedVersionRef = React.useRef(null); const [isAboutOpen, setIsAboutOpen] = useState(false); const isAboutOpenRef = React.useRef(false); const [aboutLoading, setAboutLoading] = useState(false); const [aboutInfo, setAboutInfo] = useState<{ version: string; author: string; buildTime?: string; repoUrl?: string; issueUrl?: string; releaseUrl?: string; communityUrl?: string } | null>(null); const [aboutUpdateStatus, setAboutUpdateStatus] = useState(''); const [lastUpdateInfo, setLastUpdateInfo] = useState(null); const [updateDownloadProgress, setUpdateDownloadProgress] = useState<{ open: boolean; version: string; status: 'idle' | 'start' | 'downloading' | 'done' | 'error'; percent: number; downloaded: number; total: number; message: string; }>({ open: false, version: '', status: 'idle', percent: 0, downloaded: 0, total: 0, message: '' }); type UpdateInfo = { hasUpdate: boolean; currentVersion: string; latestVersion: string; releaseName?: string; releaseNotesUrl?: string; assetName?: string; assetUrl?: string; assetSize?: number; sha256?: string; downloaded?: boolean; downloadPath?: string; }; type UpdateDownloadProgressEvent = { status?: 'start' | 'downloading' | 'done' | 'error'; percent?: number; downloaded?: number; total?: number; message?: string; }; type UpdateDownloadResultData = { info?: UpdateInfo; downloadPath?: string; installLogPath?: string; installTarget?: string; platform?: string; autoRelaunch?: boolean; }; const isMacRuntime = runtimePlatform === 'darwin' || (runtimePlatform === '' && /mac/i.test(detectNavigatorPlatform())); const isWindowsRuntime = runtimePlatform === 'windows' || (runtimePlatform === '' && isWindowsPlatform()); const useNativeMacWindowControls = isMacRuntime && appearance.useNativeMacWindowControls === true; useEffect(() => { if (!isStoreHydrated || !isMacRuntime) { return; } try { void SetMacNativeWindowControls(useNativeMacWindowControls).catch(() => undefined); } catch (e) { console.warn('Wails API: SetMacNativeWindowControls unavailable', e); } }, [isMacRuntime, isStoreHydrated, useNativeMacWindowControls]); const formatBytes = (bytes?: number) => { if (!bytes || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytes; let idx = 0; while (value >= 1024 && idx < units.length - 1) { value /= 1024; idx++; } return `${value.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`; }; const downloadUpdate = React.useCallback(async (info: UpdateInfo, silent: boolean) => { if (updateDownloadInFlightRef.current) return; if (updateDownloadedVersionRef.current === info.latestVersion) { if (!silent) { const cachedDownloadPath = updateDownloadMetaRef.current?.downloadPath; void message.info(cachedDownloadPath ? `更新包已就绪(${info.latestVersion}),路径:${cachedDownloadPath}` : `更新包已就绪(${info.latestVersion})`); showUpdateDownloadProgress(); } return; } updateDownloadInFlightRef.current = true; updateUserDismissedRef.current = false; updateDownloadMetaRef.current = null; setUpdateDownloadProgress({ open: true, version: info.latestVersion, status: 'start', percent: 0, downloaded: 0, total: info.assetSize || 0, message: '' }); let res: any = null; try { res = await (window as any).go.app.App.DownloadUpdate(); } catch (e) { console.warn("Wails API: DownloadUpdate unavailable", e); } updateDownloadInFlightRef.current = false; if (res?.success) { const resultData = (res?.data || {}) as UpdateDownloadResultData; updateDownloadMetaRef.current = resultData; updateDownloadedVersionRef.current = info.latestVersion; setUpdateDownloadProgress(prev => { const total = prev.total > 0 ? prev.total : (info.assetSize || 0); return { ...prev, status: 'done', percent: 100, downloaded: total, total, message: '', open: false }; }); setLastUpdateInfo((prev) => { if (!prev || prev.latestVersion !== info.latestVersion) { return { ...info, downloaded: true, downloadPath: resultData?.downloadPath || info.downloadPath, }; } return { ...prev, downloaded: true, downloadPath: resultData?.downloadPath || prev.downloadPath || info.downloadPath, }; }); if (resultData?.downloadPath) { void message.success({ content: `更新下载完成,更新包路径:${resultData.downloadPath}`, duration: 5 }); } else { void message.success({ content: '更新下载完成', duration: 2 }); } setAboutUpdateStatus(`发现新版本 ${info.latestVersion}(已下载,请点击"下载进度"后安装)`); // macOS:如果用户没有主动隐藏进度弹窗,则下载完成后自动打开下载目录 if (isMacRuntime && !updateUserDismissedRef.current) { try { const openRes = await (window as any).go.app.App.OpenDownloadedUpdateDirectory(); if (openRes?.success) { void message.success(openRes?.message || '已打开安装目录,请手动完成替换'); } } catch (e) { console.warn('自动打开下载目录失败', e); } } } else { setUpdateDownloadProgress(prev => ({ ...prev, status: 'error', message: res?.message || '未知错误' })); void message.error({ content: '更新下载失败: ' + (res?.message || '未知错误'), duration: 4 }); } }, []); const showUpdateDownloadProgress = React.useCallback(() => { setUpdateDownloadProgress((prev) => { if (prev.status === 'idle') return prev; return { ...prev, open: true }; }); }, []); const hideUpdateDownloadProgress = React.useCallback(() => { setUpdateDownloadProgress((prev) => ({ ...prev, open: false })); }, []); const isLatestUpdateDownloaded = Boolean(lastUpdateInfo?.hasUpdate) && ( Boolean(lastUpdateInfo?.downloaded) || (Boolean(lastUpdateInfo?.latestVersion) && updateDownloadedVersionRef.current === lastUpdateInfo?.latestVersion) ); const isBackgroundProgressForLatestUpdate = Boolean(lastUpdateInfo?.hasUpdate) && Boolean(lastUpdateInfo?.latestVersion) && updateDownloadProgress.version === lastUpdateInfo?.latestVersion && (updateDownloadProgress.status === 'start' || updateDownloadProgress.status === 'downloading' || updateDownloadProgress.status === 'done' || updateDownloadProgress.status === 'error'); const canShowProgressEntry = (isLatestUpdateDownloaded || isBackgroundProgressForLatestUpdate) && updateInstallTriggeredVersionRef.current !== (lastUpdateInfo?.latestVersion || null); const handleInstallFromProgress = React.useCallback(async () => { // 允许从下载进度弹窗(status=done)或关于弹窗(isLatestUpdateDownloaded=true)触发 const canInstall = updateDownloadProgress.status === 'done' || (Boolean(lastUpdateInfo?.hasUpdate) && (Boolean(lastUpdateInfo?.downloaded) || updateDownloadedVersionRef.current === lastUpdateInfo?.latestVersion)); if (!canInstall) { return; } if (isMacRuntime) { const res = await (window as any).go.app.App.OpenDownloadedUpdateDirectory(); if (!res?.success) { void message.error('打开安装目录失败: ' + (res?.message || '未知错误')); // 文件可能已被用户删除,清除已下载状态以允许重新下载 updateDownloadedVersionRef.current = null; updateDownloadMetaRef.current = null; setUpdateDownloadProgress(prev => ({ ...prev, status: 'idle', percent: 0, downloaded: 0, open: false, })); setLastUpdateInfo(prev => prev ? { ...prev, downloaded: false, downloadPath: undefined } : prev); setAboutUpdateStatus(prev => prev.replace('已下载', '未下载')); return; } updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null; hideUpdateDownloadProgress(); void message.success(res?.message || '已打开安装目录,请手动完成替换'); return; } const res = await (window as any).go.app.App.InstallUpdateAndRestart(); if (!res?.success) { void message.error('更新安装失败: ' + (res?.message || '未知错误')); return; } updateInstallTriggeredVersionRef.current = updateDownloadProgress.version || lastUpdateInfo?.latestVersion || null; hideUpdateDownloadProgress(); }, [hideUpdateDownloadProgress, isMacRuntime, lastUpdateInfo?.latestVersion, lastUpdateInfo?.hasUpdate, lastUpdateInfo?.downloaded, updateDownloadProgress.status, updateDownloadProgress.version]); const checkForUpdates = React.useCallback(async (silent: boolean) => { if (updateCheckInFlightRef.current) return; updateCheckInFlightRef.current = true; if (!silent) { setAboutUpdateStatus('正在检查更新...'); } const res = await (window as any).go.app.App.CheckForUpdates(); updateCheckInFlightRef.current = false; if (!res?.success) { if (!silent) { void message.error('检查更新失败: ' + (res?.message || '未知错误')); setAboutUpdateStatus('检查更新失败: ' + (res?.message || '未知错误')); } return; } const info: UpdateInfo = res.data; if (!info) return; const aboutOpen = isAboutOpenRef.current; if (info.hasUpdate) { // 以后端校验为准:如果后端确认文件不存在(downloaded=false),清除本地 ref if (!info.downloaded && updateDownloadedVersionRef.current === info.latestVersion) { updateDownloadedVersionRef.current = null; updateDownloadMetaRef.current = null; } const localDownloaded = updateDownloadedVersionRef.current === info.latestVersion; const hasDownloaded = Boolean(info.downloaded) || localDownloaded; if (hasDownloaded) { const downloadPath = info.downloadPath || updateDownloadMetaRef.current?.downloadPath || ''; updateDownloadedVersionRef.current = info.latestVersion; updateDownloadMetaRef.current = { ...(updateDownloadMetaRef.current || {}), info, downloadPath: downloadPath || undefined, }; setUpdateDownloadProgress((prev) => { if (prev.status === 'start' || prev.status === 'downloading') { return prev; } const total = info.assetSize || prev.total || 0; return { ...prev, open: prev.open && prev.version === info.latestVersion, version: info.latestVersion, status: 'done', percent: 100, downloaded: total, total, message: '', }; }); setLastUpdateInfo({ ...info, downloaded: true, downloadPath: downloadPath || undefined, }); } else { if (updateDownloadedVersionRef.current !== info.latestVersion) { updateDownloadMetaRef.current = null; } setUpdateDownloadProgress((prev) => { if (prev.status === 'start' || prev.status === 'downloading') { return prev; } return { ...prev, open: false, version: info.latestVersion, status: 'idle', percent: 0, downloaded: 0, total: info.assetSize || 0, message: '', }; }); setLastUpdateInfo(info); } const statusText = hasDownloaded ? `发现新版本 ${info.latestVersion}(已下载,请点击“下载进度”后安装)` : `发现新版本 ${info.latestVersion}(未下载)`; if (!silent) { void message.info(`发现新版本 ${info.latestVersion}`); setAboutUpdateStatus(statusText); } if (silent && aboutOpen) { setAboutUpdateStatus(statusText); } if (silent && !aboutOpen && updateMutedVersionRef.current !== info.latestVersion && updateNotifiedVersionRef.current !== info.latestVersion) { updateNotifiedVersionRef.current = info.latestVersion; setIsAboutOpen(true); } } else if (!silent) { setUpdateDownloadProgress((prev) => { if (prev.status === 'start' || prev.status === 'downloading') { return prev; } return { open: false, version: '', status: 'idle', percent: 0, downloaded: 0, total: 0, message: '', }; }); setLastUpdateInfo(info); const text = `当前已是最新版本(${info.currentVersion || '未知'})`; void message.success(text); setAboutUpdateStatus(text); } else if (silent && aboutOpen) { setUpdateDownloadProgress((prev) => { if (prev.status === 'start' || prev.status === 'downloading') { return prev; } return { open: false, version: '', status: 'idle', percent: 0, downloaded: 0, total: 0, message: '', }; }); setLastUpdateInfo(info); const text = `当前已是最新版本(${info.currentVersion || '未知'})`; setAboutUpdateStatus(text); } else { setLastUpdateInfo(info); } }, []); const loadAboutInfo = React.useCallback(async () => { setAboutLoading(true); const res = await (window as any).go.app.App.GetAppInfo(); if (res?.success) { setAboutInfo(res.data); } else { void message.error('获取应用信息失败: ' + (res?.message || '未知错误')); } setAboutLoading(false); }, []); const handleNewQuery = useCallback(() => { let connId = ''; let db = ''; // Priority: Active Tab Context (if connection still valid) > Sidebar Selection (activeContext) if (activeTabId) { const currentTab = tabs.find(t => t.id === activeTabId); if (currentTab && currentTab.connectionId && connections.some(c => c.id === currentTab.connectionId)) { connId = currentTab.connectionId; db = currentTab.dbName || ''; } } // Fallback: Sidebar selection context (only if connection still valid) if (!connId && activeContext?.connectionId && connections.some(c => c.id === activeContext.connectionId)) { connId = activeContext.connectionId; db = activeContext.dbName || ''; } addTab({ id: `query-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, title: '新建查询', type: 'query', connectionId: connId, dbName: db, query: '' }); }, [activeTabId, tabs, connections, activeContext, addTab]); const handleImportConnections = async () => { const res = await (window as any).go.app.App.ImportConfigFile(); if (res.success) { try { const imported = JSON.parse(res.data); if (Array.isArray(imported)) { let count = 0; imported.forEach((conn: any) => { if (!connections.some(c => c.id === conn.id)) { addConnection(conn); count++; } }); void message.success(`成功导入 ${count} 个连接`); } else { void message.error("文件格式错误:需要 JSON 数组"); } } catch (e) { void message.error("解析 JSON 失败"); } } else if (res.message !== "已取消") { void message.error("导入失败: " + res.message); } }; const handleExportConnections = async () => { if (connections.length === 0) { void message.warning("没有连接可导出"); return; } const res = await (window as any).go.app.App.ExportData(connections, ['id','name','config','includeDatabases','includeRedisDatabases'], "connections", "json"); if (res.success) { void message.success("导出成功"); } else if (res.message !== "已取消") { void message.error("导出失败: " + res.message); } }; const [isToolsModalOpen, setIsToolsModalOpen] = useState(false); const [isThemeModalOpen, setIsThemeModalOpen] = useState(false); const [themeModalSection, setThemeModalSection] = useState<'theme' | 'appearance'>('theme'); const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false); const [capturingShortcutAction, setCapturingShortcutAction] = useState(null); const [isProxyModalOpen, setIsProxyModalOpen] = useState(false); const [isAISettingsOpen, setIsAISettingsOpen] = useState(false); const aiEntryPlacement = resolveAIEntryPlacement(); const aiEdgeHandleAttachment = resolveAIEdgeHandleAttachment(aiPanelVisible); const aiEdgeHandleDockStyle = useMemo( () => resolveAIEdgeHandleDockStyle(aiEdgeHandleAttachment), [aiEdgeHandleAttachment], ); const aiEdgeHandleStyle = useMemo(() => ( resolveAIEdgeHandleStyle({ darkMode, aiPanelVisible, effectiveUiScale, }) ), [aiPanelVisible, darkMode, effectiveUiScale]); const sidebarUtilityItems = useMemo(() => { const itemMap = { tools: { key: 'tools', title: '工具', icon: , onClick: () => setIsToolsModalOpen(true), }, proxy: { key: 'proxy', title: '代理', icon: , onClick: () => setIsProxyModalOpen(true), }, theme: { key: 'theme', title: '主题', icon: , onClick: () => setIsThemeModalOpen(true), }, about: { key: 'about', title: '关于', icon: , onClick: () => setIsAboutOpen(true), }, } as const; return SIDEBAR_UTILITY_ITEM_KEYS.map((key) => itemMap[key]); }, []); const renderAIEdgeHandle = () => ( ); // Log Panel: 最小高度按“工具栏 + 1 条日志行(微增)”限制 const LOG_PANEL_TOOLBAR_HEIGHT = 32; const LOG_PANEL_SINGLE_ROW_HEIGHT = 39; const LOG_PANEL_MIN_VISIBLE_ROWS = 1; const LOG_PANEL_MIN_HEIGHT = LOG_PANEL_TOOLBAR_HEIGHT + (LOG_PANEL_SINGLE_ROW_HEIGHT * LOG_PANEL_MIN_VISIBLE_ROWS); const LOG_PANEL_MAX_HEIGHT = 800; const [logPanelHeight, setLogPanelHeight] = useState(Math.max(200, LOG_PANEL_MIN_HEIGHT)); const [isLogPanelOpen, setIsLogPanelOpen] = useState(false); const logResizeRef = React.useRef<{ startY: number, startHeight: number } | null>(null); const logGhostRef = React.useRef(null); const handleLogResizeStart = (e: React.MouseEvent) => { e.preventDefault(); logResizeRef.current = { startY: e.clientY, startHeight: logPanelHeight }; if (logGhostRef.current) { logGhostRef.current.style.top = `${e.clientY}px`; logGhostRef.current.style.display = 'block'; } document.addEventListener('mousemove', handleLogResizeMove); document.addEventListener('mouseup', handleLogResizeUp); }; const handleLogResizeMove = (e: MouseEvent) => { if (!logResizeRef.current) return; // Just update ghost line, no state update if (logGhostRef.current) { logGhostRef.current.style.top = `${e.clientY}px`; } }; const handleLogResizeUp = (e: MouseEvent) => { if (logResizeRef.current) { const delta = logResizeRef.current.startY - e.clientY; const newHeight = Math.max( LOG_PANEL_MIN_HEIGHT, Math.min(LOG_PANEL_MAX_HEIGHT, logResizeRef.current.startHeight + delta) ); setLogPanelHeight(newHeight); } if (logGhostRef.current) { logGhostRef.current.style.display = 'none'; } logResizeRef.current = null; document.removeEventListener('mousemove', handleLogResizeMove); document.removeEventListener('mouseup', handleLogResizeUp); }; const handleEditConnection = (conn: SavedConnection) => { setEditingConnection(conn); setIsModalOpen(true); }; const handleCloseModal = () => { setIsModalOpen(false); setEditingConnection(null); }; const handleOpenDriverManagerFromConnection = () => { setIsModalOpen(false); setEditingConnection(null); setIsDriverModalOpen(true); }; const handleTitleBarWindowToggle = async () => { try { if (await WindowIsFullscreen()) { await WindowUnfullscreen(); return; } if (useNativeMacWindowControls && isMacRuntime) { await WindowFullscreen(); return; } await WindowToggleMaximise(); } catch (_) { // ignore } }; const handleTitleBarDoubleClick = (e: React.MouseEvent) => { const target = e.target as HTMLElement | null; if (target?.closest('[data-no-titlebar-toggle="true"]')) { return; } void handleTitleBarWindowToggle(); }; // Sidebar Resizing const sidebarDragRef = React.useRef<{ startX: number, startWidth: number } | null>(null); const rafRef = React.useRef(null); const ghostRef = React.useRef(null); const latestMouseX = React.useRef(0); // Store latest mouse position const handleSidebarMouseDown = (e: React.MouseEvent) => { e.preventDefault(); if (ghostRef.current) { ghostRef.current.style.left = `${sidebarWidth}px`; ghostRef.current.style.display = 'block'; } sidebarDragRef.current = { startX: e.clientX, startWidth: sidebarWidth }; latestMouseX.current = e.clientX; // Init document.addEventListener('mousemove', handleSidebarMouseMove); document.addEventListener('mouseup', handleSidebarMouseUp); }; const handleSidebarMouseMove = (e: MouseEvent) => { if (!sidebarDragRef.current) return; latestMouseX.current = e.clientX; // Always update latest pos if (rafRef.current) return; // Schedule once per frame rafRef.current = requestAnimationFrame(() => { if (!sidebarDragRef.current || !ghostRef.current) return; // Use latestMouseX.current instead of stale closure 'e.clientX' const delta = latestMouseX.current - sidebarDragRef.current.startX; const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); ghostRef.current.style.left = `${newWidth}px`; rafRef.current = null; }); }; const handleSidebarMouseUp = (e: MouseEvent) => { if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } if (sidebarDragRef.current) { // Use latest position for final commit too const delta = e.clientX - sidebarDragRef.current.startX; const newWidth = Math.max(200, Math.min(600, sidebarDragRef.current.startWidth + delta)); setSidebarWidth(newWidth); } if (ghostRef.current) { ghostRef.current.style.display = 'none'; } sidebarDragRef.current = null; document.removeEventListener('mousemove', handleSidebarMouseMove); document.removeEventListener('mouseup', handleSidebarMouseUp); }; useEffect(() => { document.body.style.backgroundColor = 'transparent'; document.body.style.color = darkMode ? '#ffffff' : '#000000'; document.body.setAttribute('data-theme', darkMode ? 'dark' : 'light'); document.body.style.fontSize = `${effectiveFontSize}px`; document.documentElement.style.setProperty('--gonavi-font-size', `${effectiveFontSize}px`); }, [darkMode, effectiveFontSize]); useEffect(() => { isAboutOpenRef.current = isAboutOpen; }, [isAboutOpen]); useEffect(() => { if (isAboutOpen) { if (lastUpdateInfo?.hasUpdate) { const localDownloaded = updateDownloadedVersionRef.current === lastUpdateInfo.latestVersion; const hasDownloaded = Boolean(lastUpdateInfo.downloaded) || localDownloaded; setAboutUpdateStatus( hasDownloaded ? `发现新版本 ${lastUpdateInfo.latestVersion}(已下载,请点击“下载进度”后安装)` : `发现新版本 ${lastUpdateInfo.latestVersion}(未下载)` ); } else if (lastUpdateInfo) { setAboutUpdateStatus(`当前已是最新版本(${lastUpdateInfo.currentVersion || '未知'})`); } else { setAboutUpdateStatus('未检查'); } void loadAboutInfo(); } }, [isAboutOpen, lastUpdateInfo, loadAboutInfo]); useEffect(() => { const startupTimer = window.setTimeout(() => { void checkForUpdates(true); }, 2000); const interval = window.setInterval(() => { void checkForUpdates(true); }, 30 * 60 * 1000); return () => { window.clearTimeout(startupTimer); window.clearInterval(interval); }; }, [checkForUpdates]); useEffect(() => { let offDownloadProgress: any = null; try { offDownloadProgress = EventsOn('update:download-progress', (event: UpdateDownloadProgressEvent) => { if (!event) return; const status = event.status || 'downloading'; const nextStatus: 'idle' | 'start' | 'downloading' | 'done' | 'error' = status === 'start' || status === 'downloading' || status === 'done' || status === 'error' ? status : 'downloading'; const downloaded = typeof event.downloaded === 'number' ? event.downloaded : 0; const total = typeof event.total === 'number' ? event.total : 0; const percentRaw = typeof event.percent === 'number' ? event.percent : (total > 0 ? (downloaded / total) * 100 : 0); const percent = Math.max(0, Math.min(100, percentRaw)); setUpdateDownloadProgress(prev => ({ open: prev.open, version: prev.version, status: nextStatus, percent, downloaded, total, message: String(event.message || '') })); }); } catch (e) { console.warn("Wails API: EventsOn unavailable", e); } return () => { if (offDownloadProgress) offDownloadProgress(); }; }, []); useEffect(() => { const handleOpenShortcutSettingsEvent = () => { setIsShortcutModalOpen(true); }; window.addEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); return () => { window.removeEventListener('gonavi:open-shortcut-settings', handleOpenShortcutSettingsEvent as EventListener); }; }, []); useEffect(() => { if (!isMacRuntime || !useNativeMacWindowControls) { return; } const handleMacNativeEscapeCapture = (event: KeyboardEvent) => { if (!shouldSuppressMacNativeEscapeExit(isMacRuntime, useNativeMacWindowControls, useStore.getState().windowState === 'fullscreen', event)) { return; } event.preventDefault(); event.stopPropagation(); }; window.addEventListener('keydown', handleMacNativeEscapeCapture, true); return () => { window.removeEventListener('keydown', handleMacNativeEscapeCapture, true); }; }, [isMacRuntime, useNativeMacWindowControls]); useEffect(() => { const handleGlobalShortcut = (event: KeyboardEvent) => { const matchedAction = SHORTCUT_ACTION_ORDER.find((action) => { const binding = shortcutOptions[action]; if (!binding?.enabled) { return false; } if (isEditableElement(event.target) && !SHORTCUT_ACTION_META[action].allowInEditable) { return false; } return isShortcutMatch(event, binding.combo); }); if (!matchedAction) { return; } event.preventDefault(); event.stopPropagation(); switch (matchedAction) { case 'runQuery': window.dispatchEvent(new CustomEvent('gonavi:run-active-query')); break; case 'focusSidebarSearch': window.dispatchEvent(new CustomEvent('gonavi:focus-sidebar-search')); break; case 'newQueryTab': handleNewQuery(); break; case 'toggleLogPanel': setIsLogPanelOpen((prev) => !prev); break; case 'toggleTheme': setTheme(themeMode === 'dark' ? 'light' : 'dark'); break; case 'openShortcutManager': setIsShortcutModalOpen(true); break; case 'toggleMacFullscreen': if (isMacRuntime && useNativeMacWindowControls) { void handleTitleBarWindowToggle(); } break; } }; window.addEventListener('keydown', handleGlobalShortcut); return () => { window.removeEventListener('keydown', handleGlobalShortcut); }; }, [handleNewQuery, handleTitleBarWindowToggle, isMacRuntime, shortcutOptions, themeMode, setTheme, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { return; } const handleShortcutCapture = (event: KeyboardEvent) => { event.preventDefault(); event.stopPropagation(); if (event.key === 'Escape') { setCapturingShortcutAction(null); return; } const combo = eventToShortcut(event); if (!combo) { return; } if (!hasModifierKey(combo)) { void message.warning('快捷键至少包含 Ctrl / Alt / Shift / Meta 之一'); return; } const normalizedCombo = normalizeShortcutCombo(combo); const conflictAction = SHORTCUT_ACTION_ORDER.find((action) => { if (action === capturingShortcutAction) { return false; } const binding = shortcutOptions[action]; if (!binding?.enabled) { return false; } return normalizeShortcutCombo(binding.combo) === normalizedCombo; }); if (conflictAction) { void message.warning(`与「${SHORTCUT_ACTION_META[conflictAction].label}」冲突,请换一个快捷键`); return; } updateShortcut(capturingShortcutAction, { combo: normalizedCombo, enabled: true }); setCapturingShortcutAction(null); }; window.addEventListener('keydown', handleShortcutCapture, true); return () => { window.removeEventListener('keydown', handleShortcutCapture, true); }; }, [capturingShortcutAction, shortcutOptions, updateShortcut]); const linuxResizeHandleStyleBase = { position: 'fixed', zIndex: 12000, background: 'transparent', WebkitAppRegion: 'drag', '--wails-draggable': 'drag', userSelect: 'none' } as any; const showLinuxResizeHandles = isLinuxRuntime; const resizeGuideColor = darkMode ? 'rgba(246, 196, 83, 0.55)' : 'rgba(24, 144, 255, 0.5)'; return ( {/* Custom Title Bar */}
{/* Logo can be added here if available */} GoNavi
{useNativeMacWindowControls ? (
) : (
e.stopPropagation()} style={{ display: 'flex', height: '100%', WebkitAppRegion: 'no-drag', '--wails-draggable': 'no-drag' } as any} >
)}
{sidebarUtilityItems.map((item) => (
{!connectionWorkbenchState.ready && (
{connectionWorkbenchState.message}
)}
{/* Floating SQL Log Toggle */}
{/* Sidebar Resize Handle */}
{aiEntryPlacement === 'content-edge' && aiEdgeHandleAttachment === 'content-shell' && (
{renderAIEdgeHandle()}
)} {aiPanelVisible && (
{aiEntryPlacement === 'content-edge' && aiEdgeHandleAttachment === 'panel-shell' && (
{renderAIEdgeHandle()}
)} setAIPanelVisible(false)} onOpenSettings={() => setIsAISettingsOpen(true)} overlayTheme={overlayTheme} />
)}
{isLogPanelOpen && ( setIsLogPanelOpen(false)} onResizeStart={handleLogResizeStart} /> )}
, '工具中心', '集中处理连接配置、同步、驱动和快捷键相关操作。')} open={isToolsModalOpen} onCancel={() => setIsToolsModalOpen(false)} footer={null} width={560} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} >
{[ { key: 'import', icon: , title: '导入连接配置', description: '从本地文件恢复连接列表。', onClick: () => { setIsToolsModalOpen(false); void handleImportConnections(); }, }, { key: 'export', icon: , title: '导出连接配置', description: '导出当前连接与可见配置字段。', onClick: () => { setIsToolsModalOpen(false); void handleExportConnections(); }, }, { key: 'sync', icon: , title: '数据同步', description: '进入跨源同步工作流。', onClick: () => { setIsToolsModalOpen(false); setIsSyncModalOpen(true); }, }, { key: 'drivers', icon: , title: '驱动管理', description: '安装、更新或移除数据库驱动。', onClick: () => { setIsToolsModalOpen(false); setIsDriverModalOpen(true); }, }, { key: 'shortcut-settings', icon: , title: '快捷键管理', description: '查看并调整全局快捷键绑定。', onClick: () => { setIsToolsModalOpen(false); setIsShortcutModalOpen(true); }, }, ].map((item) => ( ))}
setIsSyncModalOpen(false)} /> setIsDriverModalOpen(false)} onOpenGlobalProxySettings={() => setIsProxyModalOpen(true)} /> setIsAISettingsOpen(false)} darkMode={darkMode} overlayTheme={overlayTheme} /> , '关于 GoNavi', '查看版本信息、仓库地址、更新状态与下载入口。')} open={isAboutOpen} onCancel={() => setIsAboutOpen(false)} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10, display: 'flex', flexWrap: 'wrap', gap: 10, justifyContent: 'flex-end' } }} footer={[ isBackgroundProgressForLatestUpdate && !isLatestUpdateDownloaded ? ( ) : null, lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( ) : null, , , lastUpdateInfo?.hasUpdate && !isLatestUpdateDownloaded && !isBackgroundProgressForLatestUpdate ? ( ) : null, isLatestUpdateDownloaded ? ( ) : null, ].filter(Boolean)} > {aboutLoading ? (
) : (
版本
{aboutInfo?.version || '未知'}
作者
{aboutInfo?.author || '未知'}
更新状态
{aboutUpdateStatus || '未检查'}
{aboutInfo?.communityUrl ? ( ) : null}
)}
: , themeModalSection === 'theme' ? '主题设置' : '外观设置', themeModalSection === 'theme' ? '切换亮暗主题,保持整体视觉风格统一。' : '统一调整缩放、字体、透明度与模糊效果。' )} open={isThemeModalOpen} onCancel={() => { setIsThemeModalOpen(false); setThemeModalSection('theme'); }} footer={null} width={820} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8, height: 620, overflow: 'hidden' }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} >
设置导航
{[ { key: 'theme', title: '主题模式', description: '亮色与暗色切换', icon: }, { key: 'appearance', title: '外观参数', description: '缩放、字体与透明度', icon: }, ].map((item) => { const active = themeModalSection === item.key; return ( ); })}
{themeModalSection === 'theme' ? (
主题模式
{[ { key: 'light', label: '亮色主题', description: '适合明亮环境,层次更轻。' }, { key: 'dark', label: '暗色主题', description: '适合低光环境,视觉更沉稳。' }, ].map((item) => { const active = themeMode === item.key; return ( ); })}
) : (
界面缩放 (UI Scale)
setUiScale(Number(v))} style={{ flex: 1 }} /> {Math.round(effectiveUiScale * 100)}%
* 建议小屏设备设置为 85%-95%
基础字体大小 (Font Size)
setFontSize(Number(v))} style={{ flex: 1 }} /> {effectiveFontSize}px
透明与模糊效果
启用透明与模糊
关闭后保留当前阈值,重新开启时直接恢复之前的设置。
setAppearance({ enabled: checked })} />
背景不透明度 (Opacity)
setAppearance({ opacity: v })} style={{ flex: 1 }} /> {Math.round((appearance.opacity ?? 1.0) * 100)}%
高斯模糊 (Blur)
{isWindowsPlatform() ? (
Windows 使用系统 Acrylic 效果,模糊程度由系统控制
) : ( <>
setAppearance({ blur: v })} style={{ flex: 1 }} /> {appearance.blur}px
* 仅控制应用内覆盖层的模糊效果
)}
{isMacRuntime ? (
macOS 窗口控制
使用 macOS 原生窗口控制
启用后显示左上角红黄绿按钮,并优先使用 macOS 原生全屏行为。
setAppearance({ useNativeMacWindowControls: checked })} />
* 已同步隐藏右上角自定义按钮;如系统窗口样式未立即刷新,可重启应用后再确认
) : null}
启动窗口
{isWindowsRuntime ? '启动时全屏(Windows 按最大化处理)' : '启动时全屏'} setStartupFullscreen(checked)} />
{isWindowsRuntime ? '* Windows 下该选项按“启动时最大化”处理,修改后下次启动生效' : '* 修改后下次启动生效'}
)}
, '快捷键管理', '统一查看、录制与启停常用快捷键,保持操作习惯一致。')} open={isShortcutModalOpen} onCancel={() => { setIsShortcutModalOpen(false); setCapturingShortcutAction(null); }} width={760} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} footer={[ , , ]} >
点击“录制”后按下快捷键。按 Esc 可取消录制。建议至少包含一个修饰键(Ctrl/Alt/Shift/Meta)。
{SHORTCUT_ACTION_ORDER.map((action) => { const meta = SHORTCUT_ACTION_META[action]; if (meta.platformOnly === 'mac' && !isMacRuntime) { return null; } const binding = shortcutOptions[action] ?? { combo: '', enabled: false }; const isCapturing = capturingShortcutAction === action; return (
{meta.label}
{meta.description}
updateShortcut(action, { enabled: checked })} />
); })}
, '全局代理设置', '统一配置更新检查、驱动管理与未单独指定代理的连接网络出口。')} open={isProxyModalOpen} onCancel={() => setIsProxyModalOpen(false)} footer={null} width={520} styles={{ content: utilityModalShellStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 8 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 10 } }} >
全局代理
启用全局代理 setGlobalProxy({ enabled: checked })} />
代理类型
setGlobalProxy({ host: e.target.value })} />
用户名(可选)
setGlobalProxy({ user: e.target.value })} />
密码(可选)
setGlobalProxy({ password: e.target.value })} />
* 作用于更新检查、驱动管理网络请求,以及未单独配置代理的数据库连接
{ updateUserDismissedRef.current = true; hideUpdateDownloadProgress(); }} > 隐藏到后台 ] : (updateDownloadProgress.status === 'done' ? [ , ] : (updateDownloadProgress.status === 'error' ? [ ] : null))} >
{`${formatBytes(updateDownloadProgress.downloaded)} / ${formatBytes(updateDownloadProgress.total)}`}
{updateDownloadProgress.message ? (
{updateDownloadProgress.message}
) : null}
{showLinuxResizeHandles && ( <> {/* Linux Mint 下 frameless 仅局部可缩放:补四边四角命中层 */}
)} {/* Ghost Resize Line for Sidebar */}
{/* Ghost Resize Line for Log Panel */}
); } export default App;