import { useState, useEffect, useCallback, useRef } from 'react' import TitleBar from './components/TitleBar' import Sidebar from './components/Sidebar' import MainContent from './components/MainContent' import ConnectionModal from './components/ConnectionModal' import CreateDatabaseModal from './components/CreateDatabaseModal' import CreateTableModal from './components/CreateTableModal' import TableDesigner from './components/TableDesigner' import InputDialog from './components/InputDialog' import { Connection, QueryTab, DatabaseType, TableInfo, ColumnInfo, TableTab } from './types' import api from './lib/electron-api' import { useConnections, useDatabaseOperations, useTableOperations, useTabOperations, useQueryOperations, useImportExport } from './lib/hooks' import { Database, Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react' function App() { const [activeConnection, setActiveConnection] = useState(null) const [selectedDatabase, setSelectedDatabase] = useState(null) const [showConnectionModal, setShowConnectionModal] = useState(false) const [editingConnection, setEditingConnection] = useState(null) const [newConnectionType, setNewConnectionType] = useState() const [showCreateDbModal, setShowCreateDbModal] = useState(false) const [createDbConnectionId, setCreateDbConnectionId] = useState(null) const [showCreateTableModal, setShowCreateTableModal] = useState(false) const [createTableContext, setCreateTableContext] = useState<{ connectionId: string; database: string } | null>(null) const [showTableDesigner, setShowTableDesigner] = useState(false) const [tableDesignerContext, setTableDesignerContext] = useState<{ connectionId: string; database: string; tableName?: string; mode: 'create' | 'edit' } | null>(null) const [inputDialog, setInputDialog] = useState<{ isOpen: boolean title: string label: string defaultValue: string onConfirm: (value: string) => void } | null>(null) const [notification, setNotification] = useState<{ type: 'success' | 'error' | 'info' message: string } | null>(null) // 显示通知 const showNotification = useCallback((type: 'success' | 'error' | 'info', message: string) => { setNotification({ type, message }) setTimeout(() => setNotification(null), 3000) }, []) // 连接管理 const { connections, setConnections, connectedIds, setConnectedIds, addConnection, deleteConnection, updateConnection } = useConnections() // 数据库操作 const { databasesMap, setDatabasesMap, loadingDbSet, setLoadingDbSet, loadingConnectionsSet, fetchDatabases } = useDatabaseOperations(showNotification) // 表操作 const { tablesMap, setTablesMap, columnsMap, setColumnsMap, fetchTables, fetchColumns } = useTableOperations(showNotification) // Tab操作 const { tabs, setTabs, activeTab, setActiveTab, loadingTables, setLoadingTables } = useTabOperations() // 查询操作 const { runQuery } = useQueryOperations(tabs, setTabs, showNotification) // 导入导出 const { importConnections: doImportConnections, exportConnections: doExportConnections } = useImportExport(connections, setConnections, showNotification) // 连接数据库 const handleConnect = useCallback(async (conn: Connection) => { if (connectedIds.has(conn.id)) return try { const result = await api.connect(conn) if (!result.success) { showNotification('error', '连接失败:' + result.message) return } setConnectedIds(prev => new Set(prev).add(conn.id)) setActiveConnection(conn.id) await fetchDatabases(conn.id) showNotification('success', `已连接到 ${conn.name}`) } catch (err) { showNotification('error', '连接失败:' + (err as Error).message) } }, [connectedIds, setConnectedIds, fetchDatabases, showNotification]) // 断开连接 const handleDisconnect = useCallback(async (id: string) => { try { await api.disconnect(id) setConnectedIds(prev => { const next = new Set(prev) next.delete(id) return next }) setDatabasesMap(prev => { const next = new Map(prev) next.delete(id) return next }) // 关闭该连接的所有标签页 setTabs(prev => { const remainingTabs = prev.filter(tab => { // TableTab 有 connectionId if ('connectionId' in tab && tab.connectionId === id) { return false } return true }) // 如果当前活跃标签页被关闭,切换到第一个标签页或主页 if (activeTab && !remainingTabs.find(t => t.id === activeTab)) { setActiveTab(remainingTabs.length > 0 ? remainingTabs[0].id : 'welcome') } return remainingTabs }) if (activeConnection === id) setActiveConnection(null) showNotification('info', '连接已断开') } catch (err) { showNotification('error', '断开失败:' + (err as Error).message) } }, [activeConnection, activeTab, setConnectedIds, setDatabasesMap, setTabs, setActiveTab, showNotification]) // 选择数据库 const handleSelectDatabase = useCallback(async (db: string, connectionId: string) => { setSelectedDatabase(db) setActiveConnection(connectionId) setLoadingDbSet(prev => new Set(prev).add(db)) try { await fetchTables(connectionId, db) const tables = tablesMap.get(db) || [] await Promise.all(tables.map(t => fetchColumns(connectionId, db, t.name))) } finally { setLoadingDbSet(prev => { const next = new Set(prev) next.delete(db) return next }) } }, [fetchTables, fetchColumns, tablesMap, setLoadingDbSet]) // 切换连接(在查询界面使用,会清空数据库选择) const handleConnectionChange = useCallback((connectionId: string) => { // 如果切换到不同的连接,清空数据库选择 if (connectionId !== activeConnection) { setSelectedDatabase(null) } setActiveConnection(connectionId) }, [activeConnection]) // 打开表 const handleOpenTable = useCallback(async (connectionId: string, database: string, tableName: string) => { const existingTab = tabs.find(t => 'tableName' in t && t.tableName === tableName && t.database === database && t.connectionId === connectionId) if (existingTab) { setActiveTab(existingTab.id) return } const newTabId = `table-${Date.now()}` const pageSize = 100 // 先创建标签页并显示(带 loading 状态) const newTab: TableTab = { id: newTabId, tableName, database, connectionId, columns: [], data: [], total: 0, page: 1, pageSize, pendingChanges: new Map(), deletedRows: new Set(), newRows: [] } setTabs(prev => [...prev, newTab]) setActiveTab(newTabId) setLoadingTables(prev => new Set(prev).add(newTabId)) // 然后异步加载数据 try { const cols = await api.getTableColumns(connectionId, database, tableName) const { data, total } = await api.getTableData(connectionId, database, tableName, 1, pageSize) // 更新标签页数据 setTabs(prev => prev.map(t => t.id === newTabId ? { ...t, columns: cols, data: data || [], total } : t)) } catch (err) { showNotification('error', '打开表失败:' + (err as Error).message) // 加载失败时移除标签页 setTabs(prev => prev.filter(t => t.id !== newTabId)) setActiveTab('welcome') } finally { setLoadingTables(prev => { const next = new Set(prev) next.delete(newTabId) return next }) } }, [tabs, setTabs, setActiveTab, setLoadingTables, showNotification]) // 加载表页 const handleLoadTablePage = useCallback(async (tabId: string, page: number) => { const tab = tabs.find(t => t.id === tabId) if (!tab || !('tableName' in tab)) return setLoadingTables(prev => new Set(prev).add(tabId)) try { const { data, total } = await api.getTableData(tab.connectionId, tab.database, tab.tableName, page, tab.pageSize) setTabs(prev => prev.map(t => t.id === tabId ? { ...t, data: data || [], total, page, pendingChanges: new Map(), deletedRows: new Set(), newRows: [] } : t)) } catch (err) { showNotification('error', '加载数据失败') } finally { setLoadingTables(prev => { const next = new Set(prev) next.delete(tabId) return next }) } }, [tabs, setTabs, setLoadingTables, showNotification]) // 更新表单元格 const handleUpdateTableCell = useCallback((tabId: string, rowIndex: number, colName: string, value: any) => { setTabs(prev => prev.map(t => { if (t.id !== tabId || !('tableName' in t)) return t const tab = t as TableTab & { pendingChanges: Map; data: any[] } // 更新 pendingChanges const changes = new Map(tab.pendingChanges) const rowKey = String(rowIndex) const rowChanges = changes.get(rowKey) || {} rowChanges[colName] = value changes.set(rowKey, rowChanges) // 同时更新 data 数组以便 UI 立即显示更新 const newData = [...tab.data] if (newData[rowIndex]) { newData[rowIndex] = { ...newData[rowIndex], [colName]: value } } return { ...t, data: newData, pendingChanges: changes } })) }, [setTabs]) // 删除行 const handleDeleteTableRow = useCallback((tabId: string, rowIndex: number) => { setTabs(prev => prev.map(t => { if (t.id !== tabId || !('tableName' in t)) return t const tab = t as TableTab & { deletedRows: Set } const deleted = new Set(tab.deletedRows) deleted.add(rowIndex) return { ...t, deletedRows: deleted } })) }, [setTabs]) // 批量删除行 const handleDeleteTableRows = useCallback((tabId: string, rowIndices: number[]) => { setTabs(prev => prev.map(t => { if (t.id !== tabId || !('tableName' in t)) return t const tab = t as TableTab & { deletedRows: Set } const deleted = new Set(tab.deletedRows) rowIndices.forEach(i => deleted.add(i)) return { ...t, deletedRows: deleted } })) }, [setTabs]) // 保存更改 const handleSaveTableChanges = useCallback(async (tabId: string) => { const tab = tabs.find(t => t.id === tabId) as (TableTab & { pendingChanges: Map; deletedRows: Set; newRows: any[] }) | undefined if (!tab || !('tableName' in tab)) return const primaryKeyCol = tab.columns.find(c => c.key === 'PRI')?.name || tab.columns[0]?.name if (!primaryKeyCol) { showNotification('error', '无法确定主键') return } setLoadingTables(prev => new Set(prev).add(tabId)) try { // 处理更新 for (const [rowKey, changes] of tab.pendingChanges) { const rowIndex = parseInt(rowKey) const row = tab.data[rowIndex] if (!row) continue const pkValue = row[primaryKeyCol] await api.updateTableRow(tab.connectionId, tab.database, tab.tableName, primaryKeyCol, pkValue, changes) } // 处理删除 for (const rowIndex of tab.deletedRows) { const row = tab.data[rowIndex] if (!row) continue const pkValue = row[primaryKeyCol] await api.deleteTableRow(tab.connectionId, tab.database, tab.tableName, primaryKeyCol, pkValue) } // 处理新增 for (const newRow of tab.newRows || []) { const insertData: Record = {} tab.columns.forEach(col => { if (newRow[col.name] !== undefined && newRow[col.name] !== null && newRow[col.name] !== '') { insertData[col.name] = newRow[col.name] } }) if (Object.keys(insertData).length > 0) { await api.insertTableRow(tab.connectionId, tab.database, tab.tableName, insertData) } } showNotification('success', '保存成功') await handleLoadTablePage(tabId, tab.page) } catch (err) { showNotification('error', '保存失败:' + (err as Error).message) } finally { setLoadingTables(prev => { const next = new Set(prev) next.delete(tabId) return next }) } }, [tabs, handleLoadTablePage, setLoadingTables, showNotification]) // 丢弃更改 const handleDiscardTableChanges = useCallback((tabId: string) => { setTabs(prev => prev.map(t => t.id === tabId ? { ...t, pendingChanges: new Map(), deletedRows: new Set(), newRows: [] } : t)) }, [setTabs]) // 刷新表 const handleRefreshTable = useCallback(async (tabId: string) => { const tab = tabs.find(t => t.id === tabId) if (!tab || !('tableName' in tab)) return await handleLoadTablePage(tabId, (tab as TableTab).page) }, [tabs, handleLoadTablePage]) // 添加新行 const handleAddTableRow = useCallback((tabId: string) => { setTabs(prev => prev.map(t => { if (t.id !== tabId || !('tableName' in t)) return t const tab = t as TableTab & { newRows: any[] } const newRow: Record = {} tab.columns.forEach(col => { newRow[col.name] = null }) return { ...t, newRows: [...(tab.newRows || []), newRow] } })) }, [setTabs]) // 更新新行 const handleUpdateNewRow = useCallback((tabId: string, rowIndex: number, colName: string, value: any) => { setTabs(prev => prev.map(t => { if (t.id !== tabId || !('tableName' in t)) return t const tab = t as TableTab & { newRows: any[] } const newRows = [...(tab.newRows || [])] if (newRows[rowIndex]) { newRows[rowIndex] = { ...newRows[rowIndex], [colName]: value } } return { ...t, newRows } })) }, [setTabs]) // 删除新行 const handleDeleteNewRow = useCallback((tabId: string, rowIndex: number) => { setTabs(prev => prev.map(t => { if (t.id !== tabId || !('tableName' in t)) return t const tab = t as TableTab & { newRows: any[] } const newRows = [...(tab.newRows || [])] newRows.splice(rowIndex, 1) return { ...t, newRows } })) }, [setTabs]) // 新建查询 const handleNewQuery = useCallback(() => { const newTab: QueryTab = { id: `query-${Date.now()}`, title: `查询 ${tabs.filter(t => !('tableName' in t)).length + 1}`, sql: '', connectionId: activeConnection || undefined, results: null } setTabs(prev => [...prev, newTab]) setActiveTab(newTab.id) }, [tabs, setTabs, setActiveTab, activeConnection]) // 执行查询 const handleRunQuery = useCallback(async (tabId: string, sql: string) => { if (!activeConnection) { showNotification('error', '请先连接数据库') return } await runQuery(tabId, activeConnection, sql) }, [activeConnection, runQuery, showNotification]) // 更新SQL const handleUpdateSql = useCallback((tabId: string, sql: string) => { setTabs(prev => prev.map(t => t.id === tabId ? { ...t, sql } : t)) }, [setTabs]) // 更新Tab标题 const handleUpdateTabTitle = useCallback((tabId: string, title: string) => { setTabs(prev => prev.map(t => t.id === tabId ? { ...t, title } : t)) }, [setTabs]) // 关闭Tab const handleCloseTab = useCallback((tabId: string) => { setTabs(prev => { const filtered = prev.filter(t => t.id !== tabId) if (activeTab === tabId) { setActiveTab(filtered.length > 0 ? filtered[filtered.length - 1].id : 'welcome') } return filtered }) }, [activeTab, setTabs, setActiveTab]) // 切换Tab const handleTabChange = useCallback((tabId: string) => { setActiveTab(tabId) }, [setActiveTab]) // 更改每页大小 const handleChangeTablePageSize = useCallback(async (tabId: string, pageSize: number) => { setTabs(prev => prev.map(t => t.id === tabId ? { ...t, pageSize } : t)) await handleLoadTablePage(tabId, 1) }, [setTabs, handleLoadTablePage]) // 带类型新建连接 const handleNewConnectionWithType = useCallback((type: DatabaseType) => { setNewConnectionType(type) setEditingConnection(null) setShowConnectionModal(true) }, []) // 保存连接 const handleSaveConnection = useCallback((conn: Omit & { id?: string }) => { if (conn.id) { updateConnection(conn as Connection) } else { addConnection(conn) } }, [addConnection, updateConnection]) // 删除连接 const handleDeleteConnection = useCallback(async (id: string) => { if (connectedIds.has(id)) { await handleDisconnect(id) } deleteConnection(id) }, [connectedIds, handleDisconnect, deleteConnection]) // 批量删除连接 const handleDeleteConnections = useCallback(async (ids: string[]) => { for (const id of ids) { await handleDeleteConnection(id) } }, [handleDeleteConnection]) // 编辑连接 const handleEditConnection = useCallback((conn: Connection) => { setEditingConnection(conn) setNewConnectionType(undefined) setShowConnectionModal(true) }, []) // 创建数据库 const handleCreateDatabase = useCallback((connectionId: string) => { setCreateDbConnectionId(connectionId) setShowCreateDbModal(true) }, []) // 删除数据库 const handleDropDatabase = useCallback(async (connectionId: string, database: string) => { try { await api.dropDatabase(connectionId, database) showNotification('success', `数据库 ${database} 已删除`) await fetchDatabases(connectionId) } catch (err) { showNotification('error', '删除失败:' + (err as Error).message) } }, [fetchDatabases, showNotification]) // 创建表 - 使用 TableDesigner const handleCreateTable = useCallback((connectionId: string, database: string) => { setTableDesignerContext({ connectionId, database, mode: 'create' }) setShowTableDesigner(true) }, []) // 删除表 const handleDropTable = useCallback(async (connectionId: string, database: string, table: string) => { try { await api.dropTable(connectionId, database, table) showNotification('success', `表 ${table} 已删除`) await fetchTables(connectionId, database) } catch (err) { showNotification('error', '删除失败:' + (err as Error).message) } }, [fetchTables, showNotification]) // 清空表 const handleTruncateTable = useCallback(async (connectionId: string, database: string, table: string) => { try { await api.truncateTable(connectionId, database, table) showNotification('success', `表 ${table} 已清空`) } catch (err) { showNotification('error', '清空失败:' + (err as Error).message) } }, [showNotification]) // 重命名表 const handleRenameTable = useCallback((connectionId: string, database: string, table: string) => { setInputDialog({ isOpen: true, title: '重命名表', label: '新表名', defaultValue: table, onConfirm: async (newName: string) => { if (newName && newName !== table) { try { await api.renameTable(connectionId, database, table, newName) showNotification('success', `表已重命名为 ${newName}`) await fetchTables(connectionId, database) } catch (err) { showNotification('error', '重命名失败:' + (err as Error).message) } } setInputDialog(null) } }) }, [fetchTables, showNotification]) // 复制表 const handleDuplicateTable = useCallback((connectionId: string, database: string, table: string) => { setInputDialog({ isOpen: true, title: '复制表', label: '新表名', defaultValue: `${table}_copy`, onConfirm: async (newName: string) => { if (newName) { try { await api.duplicateTable(connectionId, database, table, newName) showNotification('success', `表已复制为 ${newName}`) await fetchTables(connectionId, database) } catch (err) { showNotification('error', '复制失败:' + (err as Error).message) } } setInputDialog(null) } }) }, [fetchTables, showNotification]) // 刷新表列表 const handleRefreshTables = useCallback(async (connectionId: string, database: string) => { await fetchTables(connectionId, database) showNotification('success', '已刷新') }, [fetchTables, showNotification]) // 设计表 - 使用 TableDesigner const handleDesignTable = useCallback((connectionId: string, database: string, table: string) => { setTableDesignerContext({ connectionId, database, tableName: table, mode: 'edit' }) setShowTableDesigner(true) }, []) // 键盘快捷键 useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === 'q') { e.preventDefault() handleNewQuery() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [handleNewQuery]) return (
{ setEditingConnection(null); setNewConnectionType(undefined); setShowConnectionModal(true) }} onSelectConnection={setActiveConnection} onConnect={handleConnect} onDisconnect={handleDisconnect} onEditConnection={handleEditConnection} onDeleteConnection={handleDeleteConnection} onDeleteConnections={handleDeleteConnections} onSelectDatabase={handleSelectDatabase} onOpenTable={handleOpenTable} onExportConnections={doExportConnections} onImportConnections={doImportConnections} onCreateDatabase={handleCreateDatabase} onDropDatabase={handleDropDatabase} onCreateTable={handleCreateTable} onDropTable={handleDropTable} onTruncateTable={handleTruncateTable} onRenameTable={handleRenameTable} onDuplicateTable={handleDuplicateTable} onRefreshTables={handleRefreshTables} onDesignTable={handleDesignTable} onFetchDatabases={fetchDatabases} />
{/* 状态栏 */}
0 ? 'bg-success-500' : 'bg-text-disabled'}`} /> {connectedIds.size > 0 ? `${connectedIds.size} 个连接` : '未连接'} EasySQL v{__APP_VERSION__}
{/* 通知 */} {notification && (
{notification.type === 'success' && } {notification.type === 'error' && } {notification.type === 'info' && } {notification.message}
)} {/* 模态框 */} { setShowConnectionModal(false); setEditingConnection(null); setNewConnectionType(undefined) }} onSave={handleSaveConnection} /> { setShowCreateDbModal(false); setCreateDbConnectionId(null) }} onCreated={async () => { if (createDbConnectionId) await fetchDatabases(createDbConnectionId) }} /> { setShowCreateTableModal(false); setCreateTableContext(null) }} onCreated={async () => { if (createTableContext) await fetchTables(createTableContext.connectionId, createTableContext.database) }} /> {showTableDesigner && tableDesignerContext && ( c.id === tableDesignerContext.connectionId)?.type || 'mysql'} onClose={() => { setShowTableDesigner(false); setTableDesignerContext(null) }} onSave={async (sql: string) => { try { await api.executeQuery(tableDesignerContext.connectionId, sql) await fetchTables(tableDesignerContext.connectionId, tableDesignerContext.database) showNotification('success', tableDesignerContext.mode === 'create' ? '表创建成功' : '表结构已更新') return { success: true, message: '' } } catch (err: any) { return { success: false, message: err.message || '操作失败' } } }} onGetTableInfo={tableDesignerContext.mode === 'edit' ? async () => { const cols = await api.getTableColumns(tableDesignerContext.connectionId, tableDesignerContext.database, tableDesignerContext.tableName!) return { columns: cols.map((c, i) => ({ id: `col-${i}`, name: c.name, type: c.type.split('(')[0].toUpperCase(), length: c.type.match(/\((\d+)/)?.[1] || '', decimals: c.type.match(/,(\d+)\)/)?.[1] || '', nullable: c.nullable, primaryKey: c.key === 'PRI', autoIncrement: c.extra?.includes('auto_increment') || false, unsigned: c.type.includes('unsigned'), zerofill: c.type.includes('zerofill'), defaultValue: c.default || '', comment: c.comment || '', isVirtual: false, virtualExpression: '' })), indexes: [], foreignKeys: [], options: { engine: 'InnoDB', charset: 'utf8mb4', collation: 'utf8mb4_general_ci', comment: '', autoIncrement: '', rowFormat: '' } } } : undefined} onGetDatabases={async () => databasesMap.get(tableDesignerContext.connectionId) || []} onGetTables={async (db) => { // 如果缓存中有表列表,直接返回 const cached = tablesMap.get(db) if (cached && cached.length > 0) { return cached.map(t => t.name) } // 否则从 API 加载 try { const tables = await api.getTables(tableDesignerContext.connectionId, db) // 更新缓存 setTablesMap(prev => new Map(prev).set(db, tables)) return tables.map((t: any) => t.name || t) } catch (err) { console.error('Failed to load tables:', err) return [] } }} onGetColumns={async (db, table) => { const cols = await api.getTableColumns(tableDesignerContext.connectionId, db, table) return cols.map(c => c.name) }} /> )} {inputDialog && ( setInputDialog(null)} onConfirm={inputDialog.onConfirm} /> )}
) } export default App