import React, { useState, useEffect } from 'react'; import { Layout, Button, ConfigProvider, theme, Dropdown, MenuProps, message } from 'antd'; import { PlusOutlined, BulbOutlined, BulbFilled, ConsoleSqlOutlined, BugOutlined, SettingOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import ConnectionModal from './components/ConnectionModal'; import LogPanel from './components/LogPanel'; import { useStore } from './store'; import { SavedConnection } from './types'; import './App.css'; const { Sider, Content } = Layout; function App() { const [isModalOpen, setIsModalOpen] = useState(false); const [editingConnection, setEditingConnection] = useState(null); const { darkMode, toggleDarkMode, addTab, activeContext, connections, addConnection, tabs, activeTabId } = useStore(); const handleNewQuery = () => { let connId = activeContext?.connectionId || ''; let db = activeContext?.dbName || ''; // Priority: Active Tab Context > Sidebar Selection if (activeTabId) { const currentTab = tabs.find(t => t.id === activeTabId); if (currentTab && currentTab.connectionId) { connId = currentTab.connectionId; db = currentTab.dbName || ''; } } addTab({ id: `query-${Date.now()}`, title: '新建查询', type: 'query', connectionId: connId, dbName: db }); }; 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++; } }); message.success(`成功导入 ${count} 个连接`); } else { message.error("文件格式错误:需要 JSON 数组"); } } catch (e) { message.error("解析 JSON 失败"); } } else if (res.message !== "Cancelled") { message.error("导入失败: " + res.message); } }; const handleExportConnections = async () => { if (connections.length === 0) { message.warning("没有连接可导出"); return; } const res = await (window as any).go.app.App.ExportData(connections, [], "connections", "json"); if (res.success) { message.success("导出成功"); } else if (res.message !== "Cancelled") { message.error("导出失败: " + res.message); } }; const settingsMenu: MenuProps['items'] = [ { key: 'import', label: '导入连接配置', icon: , onClick: handleImportConnections }, { key: 'export', label: '导出连接配置', icon: , onClick: handleExportConnections } ]; // Log Panel const [logPanelHeight, setLogPanelHeight] = useState(200); 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(100, Math.min(800, 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); }; // Sidebar Resizing const [sidebarWidth, setSidebarWidth] = useState(300); 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(() => { if (darkMode) { document.body.style.backgroundColor = '#141414'; document.body.style.color = '#ffffff'; } else { document.body.style.backgroundColor = '#ffffff'; document.body.style.color = '#000000'; } }, [darkMode]); return (
GoNavi
{/* Sidebar Footer for Log Toggle */}
{/* Sidebar Resize Handle */}
{isLogPanelOpen && ( setIsLogPanelOpen(false)} onResizeStart={handleLogResizeStart} /> )}
{/* Ghost Resize Line for Sidebar */}
{/* Ghost Resize Line for Log Panel */}
); } export default App;