diff --git a/src/App.tsx b/src/App.tsx index e47f3a8..6d57b4d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useMemo } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import TitleBar from './components/TitleBar' import Sidebar from './components/Sidebar' import MainContent from './components/MainContent' @@ -6,954 +6,529 @@ import ConnectionModal from './components/ConnectionModal' import CreateDatabaseModal from './components/CreateDatabaseModal' import CreateTableModal from './components/CreateTableModal' import InputDialog from './components/InputDialog' -import TableDesigner from './components/TableDesigner' import { Connection, QueryTab, DatabaseType, TableInfo, ColumnInfo, TableTab } from './types' import api from './lib/electron-api' -import { useDebouncedCallback } from './lib/hooks' -import { Edit3, Copy } from 'lucide-react' +import { useConnections, useDatabaseOperations, useTableOperations, useTabOperations, useQueryOperations, useImportExport } from './lib/hooks' +import { Database, Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react' -// 统一的标签页类型 -type Tab = QueryTab | TableTab - -export default function App() { - const [connections, setConnections] = useState([]) +function App() { const [activeConnection, setActiveConnection] = useState(null) - const [connectedIds, setConnectedIds] = useState>(new Set()) - // 每个连接的数据库列表独立存储: connectionId -> databases[] - const [databasesMap, setDatabasesMap] = useState>(new Map()) const [selectedDatabase, setSelectedDatabase] = useState(null) - // 每个数据库的表列表独立存储: "connectionId:database" -> tables[] - const [tablesMap, setTablesMap] = useState>(new Map()) - const [loadingDbSet, setLoadingDbSet] = useState>(new Set()) - const [loadingTables, setLoadingTables] = useState>(new Set()) // 正在加载的表标签 - const [allColumns, setAllColumns] = useState>(new Map()) - const [showModal, setShowModal] = useState(false) + const [showConnectionModal, setShowConnectionModal] = useState(false) const [editingConnection, setEditingConnection] = useState(null) - const [defaultDbType, setDefaultDbType] = useState(undefined) - const [tabs, setTabs] = useState([]) - const [activeTab, setActiveTab] = useState('welcome') - const [status, setStatus] = useState({ text: '就绪', type: 'success' as 'success' | 'error' | 'warning' | 'info' }) - - // 数据库/表管理相关状态 + const [newConnectionType, setNewConnectionType] = useState() const [showCreateDbModal, setShowCreateDbModal] = useState(false) const [createDbConnectionId, setCreateDbConnectionId] = useState(null) const [showCreateTableModal, setShowCreateTableModal] = useState(false) - const [createTableInfo, setCreateTableInfo] = useState<{ connectionId: string; database: string } | null>(null) - const [showRenameDialog, setShowRenameDialog] = useState(false) - const [renameInfo, setRenameInfo] = useState<{ connectionId: string; database: string; table: string } | null>(null) - const [showDuplicateDialog, setShowDuplicateDialog] = useState(false) - const [duplicateInfo, setDuplicateInfo] = useState<{ connectionId: string; database: string; table: string } | null>(null) - - // 表设计器状态 - const [showTableDesigner, setShowTableDesigner] = useState(false) - const [tableDesignerInfo, setTableDesignerInfo] = useState<{ - mode: 'create' | 'edit' - connectionId: string - database: string - tableName?: string + const [createTableContext, setCreateTableContext] = useState<{ connectionId: string; database: string } | 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) - useEffect(() => { - api.loadConnections().then(data => { - if (data) setConnections(data) - }) + // 显示通知 + const showNotification = useCallback((type: 'success' | 'error' | 'info', message: string) => { + setNotification({ type, message }) + setTimeout(() => setNotification(null), 3000) }, []) - // 防抖保存连接配置 - const debouncedSaveConnections = useDebouncedCallback((conns: Connection[]) => { - api.saveConnections(conns) - }, 500) + // 连接管理 + const { + connections, setConnections, + connectedIds, setConnectedIds, + addConnection, deleteConnection, updateConnection + } = useConnections() - useEffect(() => { - if (connections.length > 0) { - debouncedSaveConnections(connections) - } - }, [connections, debouncedSaveConnections]) + // 数据库操作 + const { + databasesMap, setDatabasesMap, + loadingDbSet, setLoadingDbSet, + fetchDatabases + } = useDatabaseOperations(showNotification) - // 全局快捷键 - useEffect(() => { - const handleGlobalKeyDown = (e: KeyboardEvent) => { - // Ctrl+Q 新建查询 - if ((e.ctrlKey || e.metaKey) && e.key === 'q') { - e.preventDefault() - const id = `query-${Date.now()}` - const newTab: QueryTab = { id, title: '查询', sql: '', results: null } - setTabs(prev => [...prev, newTab]) - setActiveTab(id) - } - } - - window.addEventListener('keydown', handleGlobalKeyDown) - return () => window.removeEventListener('keydown', handleGlobalKeyDown) - }, []) + // 表操作 + const { + tablesMap, setTablesMap, + columnsMap, setColumnsMap, + fetchTables, fetchColumns + } = useTableOperations(showNotification) - const handleSaveConnection = (conn: Connection) => { - if (editingConnection) { - setConnections(prev => prev.map(c => c.id === conn.id ? conn : c)) - } else { - setConnections(prev => [...prev, conn]) - } - setShowModal(false) - } + // Tab操作 + const { + tabs, setTabs, + activeTab, setActiveTab, + loadingTables, setLoadingTables + } = useTabOperations() - const handleConnect = async (conn: Connection) => { - setStatus({ text: `正在连接 ${conn.name}...`, type: 'info' }) - - const result = await api.connect(conn) - - if (result?.success) { + // 查询操作 + 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 { + await api.connect(conn) setConnectedIds(prev => new Set(prev).add(conn.id)) setActiveConnection(conn.id) - setStatus({ text: `已连接: ${conn.name}`, type: 'success' }) - - const dbs = await api.getDatabases(conn.id) - // 存储该连接的数据库列表 - setDatabasesMap(prev => new Map(prev).set(conn.id, dbs || [])) - } else { - setStatus({ text: result?.message || '连接失败', type: 'error' }) + await fetchDatabases(conn.id) + showNotification('success', `已连接到 ${conn.name}`) + } catch (err) { + showNotification('error', '连接失败:' + (err as Error).message) } - } + }, [connectedIds, setConnectedIds, fetchDatabases, showNotification]) - const handleDisconnect = async (id: string) => { - 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 - }) - // 清理该连接相关的表数据 - setTablesMap(prev => { - const next = new Map(prev) - for (const key of next.keys()) { - if (key.startsWith(`${id}:`)) { - next.delete(key) - } - } - return next - }) - if (activeConnection === id) { - setActiveConnection(null) - setSelectedDatabase(null) - } - } - - // 切换选中的连接,如果已连接且尚未加载数据库列表则加载 - const handleSelectConnection = async (id: string) => { - setActiveConnection(id) - - // 如果该连接已经连接,且尚未加载过数据库列表 - if (connectedIds.has(id) && !databasesMap.has(id)) { - setStatus({ text: '正在加载数据库列表...', type: 'info' }) - - try { - const dbs = await api.getDatabases(id) - setDatabasesMap(prev => new Map(prev).set(id, dbs || [])) - setStatus({ text: `${dbs?.length || 0} 个数据库`, type: 'success' }) - } catch (err: any) { - setStatus({ text: err.message, type: 'error' }) - } - } - } - - const handleSelectDatabase = useCallback(async (db: string, connectionId: string) => { - // 如果已经加载过该数据库的表,只更新选中状态 - if (tablesMap.has(db)) { - setSelectedDatabase(db) - return - } - - setSelectedDatabase(db) - // 标记该数据库正在加载 - setLoadingDbSet(prev => new Set(prev).add(db)) - setStatus({ text: `正在加载 ${db} 的表...`, type: 'info' }) - + // 断开连接 + const handleDisconnect = useCallback(async (id: string) => { try { - const tableList = await api.getTables(connectionId, db) - // 更新该数据库的表列表(不影响其他数据库) - setTablesMap(prev => { - const next = new Map(prev) - next.set(db, tableList || []) - return next - }) - setLoadingDbSet(prev => { - const next = new Set(prev) - next.delete(db) - return next - }) - - // 获取所有表的字段信息用于代码提示 - 并行加载以提高性能 - if (tableList && tableList.length > 0) { - const columnsPromises = tableList.map(async (table) => { - const cols = await api.getColumns(connectionId, db, table.name) - return { name: table.name, cols: cols || [] } - }) - - const columnsResults = await Promise.all(columnsPromises) - setAllColumns(prev => { - const next = new Map(prev) - columnsResults.forEach(({ name, cols }) => { - if (cols.length > 0) next.set(name, cols) - }) - return next - }) - } - - setStatus({ text: `${db}: ${tableList?.length || 0} 个表`, type: 'success' }) - } catch (err: any) { - setLoadingDbSet(prev => { - const next = new Set(prev) - next.delete(db) - return next - }) - setStatus({ text: err.message, type: 'error' }) - } - }, [tablesMap]) - - const handleNewQuery = (name?: string) => { - const id = `query-${Date.now()}` - const newTab: QueryTab = { id, title: name || '查询', sql: '', results: null } - setTabs(prev => [...prev, newTab]) - setActiveTab(id) - } - - const handleOpenTable = 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 - } - - // 先创建空标签页并显示 loading - const id = `table-${Date.now()}` - const emptyTab: TableTab = { - id, - type: 'table', - tableName, - database, - connectionId, - columns: [], - data: [], - total: 0, - page: 1, - pageSize: 1000, - originalData: [], - pendingChanges: new Map(), - deletedRows: new Set() - } - - setTabs(prev => [...prev, emptyTab]) - setActiveTab(id) - setLoadingTables(prev => new Set(prev).add(id)) - setStatus({ text: `正在加载 ${tableName}...`, type: 'info' }) - - try { - // 使用传入的 connectionId 而不是 activeConnection,默认1000条 - const result = await api.getTableData(connectionId, database, tableName, 1, 1000) - // 使用返回的 columns,确保列顺序和数据一致 - const columns = result?.columns || [] - - // 更新标签页数据 - setTabs(prev => prev.map(t => - t.id === id - ? { - ...t, - columns, - data: result?.data || [], - total: result?.total || 0, - originalData: result?.data || [], - } - : t - )) - setStatus({ text: `${tableName}: ${result?.total || 0} 行`, type: 'success' }) - } catch (err: any) { - setStatus({ text: err.message, type: 'error' }) - // 加载失败时关闭标签页 - setTabs(prev => prev.filter(t => t.id !== id)) - setActiveTab('welcome') - } finally { - setLoadingTables(prev => { + 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 + }) + if (activeConnection === id) setActiveConnection(null) + showNotification('info', '连接已断开') + } catch (err) { + showNotification('error', '断开失败:' + (err as Error).message) } - } + }, [activeConnection, setConnectedIds, setDatabasesMap, showNotification]) - const handleLoadTablePage = async (tabId: string, page: number) => { - const tab = tabs.find(t => t.id === tabId) as TableTab | undefined - if (!tab || !('tableName' in tab)) return - - // 设置加载状态 - setLoadingTables(prev => new Set(prev).add(tabId)) - setStatus({ text: `加载第 ${page} 页...`, type: 'info' }) - + // 选择数据库 + const handleSelectDatabase = useCallback(async (db: string, connectionId: string) => { + setSelectedDatabase(db) + setActiveConnection(connectionId) + setLoadingDbSet(prev => new Set(prev).add(db)) try { - const result = await api.getTableData( - tab.connectionId, tab.database, tab.tableName, page, tab.pageSize - ) - - setTabs(prev => prev.map(t => - t.id === tabId && 'tableName' in t - ? { ...t, data: result?.data || [], page, originalData: result?.data || [], pendingChanges: new Map(), deletedRows: new Set() } - : t - )) - setStatus({ text: `${tab.tableName}: 第 ${page} 页`, type: 'success' }) - } catch (err: any) { - setStatus({ text: err.message, type: 'error' }) + await fetchTables(connectionId, db) + const tables = tablesMap.get(db) || [] + await Promise.all(tables.map(t => fetchColumns(connectionId, db, t.name))) } finally { - setLoadingTables(prev => { + setLoadingDbSet(prev => { const next = new Set(prev) - next.delete(tabId) + next.delete(db) return next }) } - } + }, [fetchTables, fetchColumns, tablesMap, setLoadingDbSet]) - // 修改每页显示条数 - const handleChangeTablePageSize = async (tabId: string, pageSize: number) => { - const tab = tabs.find(t => t.id === tabId) as TableTab | undefined - if (!tab || !('tableName' in tab)) return - - // 设置加载状态 - setLoadingTables(prev => new Set(prev).add(tabId)) - setStatus({ text: `切换为每页 ${pageSize} 条...`, type: 'info' }) - - try { - // 重新从第1页加载 - const result = await api.getTableData( - tab.connectionId, tab.database, tab.tableName, 1, pageSize - ) - - setTabs(prev => prev.map(t => - t.id === tabId && 'tableName' in t - ? { ...t, data: result?.data || [], page: 1, pageSize, originalData: result?.data || [], pendingChanges: new Map(), deletedRows: new Set() } - : t - )) - setStatus({ text: `${tab.tableName}: 每页 ${pageSize} 条`, type: 'success' }) - } catch (err: any) { - setStatus({ text: err.message, type: 'error' }) - } finally { - setLoadingTables(prev => { - const next = new Set(prev) - next.delete(tabId) - return next - }) - } - } - - // 更新表格单元格 - const handleUpdateTableCell = (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 - - // 更新数据 - const newData = [...tab.data] - newData[rowIndex] = { ...newData[rowIndex], [colName]: value } - - // 记录修改 - const pendingChanges = new Map(tab.pendingChanges || new Map()) - const rowChanges = pendingChanges.get(String(rowIndex)) || {} - rowChanges[colName] = value - pendingChanges.set(String(rowIndex), rowChanges) - - return { ...tab, data: newData, pendingChanges } - })) - } - - // 删除表格行 - const handleDeleteTableRow = (tabId: string, rowIndex: number) => { - setTabs(prev => prev.map(t => { - if (t.id !== tabId || !('tableName' in t)) return t - const tab = t as TableTab - - const deletedRows = new Set(tab.deletedRows || new Set()) - deletedRows.add(rowIndex) - - return { ...tab, deletedRows } - })) - } - - // 批量删除表格行 - const handleDeleteTableRows = (tabId: string, rowIndices: number[]) => { - setTabs(prev => prev.map(t => { - if (t.id !== tabId || !('tableName' in t)) return t - const tab = t as TableTab - - const deletedRows = new Set(tab.deletedRows || new Set()) - rowIndices.forEach(idx => deletedRows.add(idx)) - - return { ...tab, deletedRows } - })) - } - - // 保存表格修改 - const handleSaveTableChanges = async (tabId: string) => { - const tab = tabs.find(t => t.id === tabId) as TableTab | undefined - if (!tab || !('tableName' in tab)) return - - const primaryKeyCol = tab.columns.find(c => c.key === 'PRI')?.name || tab.columns[0]?.name - if (!primaryKeyCol) { - setStatus({ text: '无法确定主键列', type: 'error' }) + // 打开表 + const handleOpenTable = useCallback(async (connectionId: string, database: string, tableName: string) => { + const existingTab = tabs.find(t => 'tableName' in t && t.tableName === tableName && t.database === database) + if (existingTab) { + setActiveTab(existingTab.id) return } - - setStatus({ text: '保存中...', type: 'info' }) + + const newTabId = `table-${Date.now()}` + setLoadingTables(prev => new Set(prev).add(newTabId)) try { - // 保存修改的行 - if (tab.pendingChanges) { - for (const [rowIndexStr, changes] of tab.pendingChanges) { - const rowIndex = parseInt(rowIndexStr) - const originalRow = tab.originalData?.[rowIndex] || tab.data[rowIndex] - const primaryKeyValue = originalRow[primaryKeyCol] - - const result = await api.updateRow( - tab.connectionId, - tab.database, - tab.tableName, - { column: primaryKeyCol, value: primaryKeyValue }, - changes - ) - - if (result?.error) { - setStatus({ text: `保存失败: ${result.error}`, type: 'error' }) - return - } - } - } + const cols = await api.getTableColumns(connectionId, database, tableName) + const pageSize = 100 + const { rows, total } = await api.getTableData(connectionId, database, tableName, 1, pageSize) - // 删除行 - if (tab.deletedRows) { - for (const rowIndex of tab.deletedRows) { - const originalRow = tab.originalData?.[rowIndex] || tab.data[rowIndex] - const primaryKeyValue = originalRow[primaryKeyCol] - - const result = await api.deleteRow( - tab.connectionId, - tab.database, - tab.tableName, - { column: primaryKeyCol, value: primaryKeyValue } - ) - - if (result?.error) { - setStatus({ text: `删除失败: ${result.error}`, type: 'error' }) - return - } - } - } - - // 插入新增行 - if (tab.newRows && tab.newRows.length > 0) { - for (const newRow of tab.newRows) { - // 过滤掉所有值为null的列(只保留有值的列) - const columns: string[] = [] - const values: any[] = [] - - for (const [colName, value] of Object.entries(newRow)) { - if (value !== null && value !== undefined && value !== '') { - columns.push(colName) - values.push(value) - } - } - - // 如果所有列都是空的,跳过这行 - if (columns.length === 0) { - continue - } - - const result = await api.insertRow( - tab.connectionId, - tab.database, - tab.tableName, - columns, - values - ) - - if (result?.error) { - setStatus({ text: `插入失败: ${result.error}`, type: 'error' }) - return - } - } - } - - // 重新加载数据 - const result = await api.getTableData( - tab.connectionId, tab.database, tab.tableName, tab.page, tab.pageSize - ) - const totalResult = await api.getTableData( - tab.connectionId, tab.database, tab.tableName, 1, 1 - ) - - setTabs(prev => prev.map(t => - t.id === tabId && 'tableName' in t - ? { - ...t, - data: result?.data || [], - total: totalResult?.total || tab.total, - originalData: result?.data || [], - pendingChanges: new Map(), - deletedRows: new Set(), - newRows: [] - } - : t - )) - - setStatus({ text: '保存成功', type: 'success' }) - } catch (err: any) { - setStatus({ text: `保存失败: ${err.message}`, type: 'error' }) - } - } - - // 放弃修改 - const handleDiscardTableChanges = (tabId: string) => { - setTabs(prev => prev.map(t => { - if (t.id !== tabId || !('tableName' in t)) return t - const tab = t as TableTab - - return { - ...tab, - data: tab.originalData || tab.data, - pendingChanges: new Map(), + const newTab: TableTab = { + id: newTabId, + tableName, + database, + connectionId, + columns: cols, + data: rows, + total, + page: 1, + pageSize, + pendingChanges: new Map(), deletedRows: new Set(), - newRows: [] + newRows: [] } - })) - setStatus({ text: '已放弃修改', type: 'warning' }) - } - - // 新增表格行 - const handleAddTableRow = (tabId: string) => { - setTabs(prev => prev.map(t => { - if (t.id !== tabId || !('tableName' in t)) return t - const tab = t as TableTab - - // 创建一个空行,所有列的值设为null - const newRow: Record = {} - tab.columns.forEach(col => { - newRow[col.name] = null + setTabs(prev => [...prev, newTab]) + setActiveTab(newTabId) + } catch (err) { + showNotification('error', '打开表失败:' + (err as Error).message) + } finally { + setLoadingTables(prev => { + const next = new Set(prev) + next.delete(newTabId) + return next }) - - const newRows = [...(tab.newRows || []), newRow] - - return { ...tab, newRows } - })) - setStatus({ text: '已添加新行', type: 'info' }) - } + } + }, [tabs, setTabs, setActiveTab, setLoadingTables, showNotification]) - // 更新新增行的数据 - const handleUpdateNewRow = (tabId: string, rowIndex: number, colName: string, value: any) => { + // 加载表页 + 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 { rows, total } = await api.getTableData(tab.connectionId, tab.database, tab.tableName, page, tab.pageSize) + setTabs(prev => prev.map(t => t.id === tabId ? { ...t, data: rows, 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 - + const tab = t as TableTab & { pendingChanges: Map } + const changes = new Map(tab.pendingChanges) + const rowKey = String(rowIndex) + const rowChanges = changes.get(rowKey) || {} + rowChanges[colName] = value + changes.set(rowKey, rowChanges) + return { ...t, 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 { ...tab, newRows } + return { ...t, newRows } })) - } + }, [setTabs]) - // 删除新增行 - const handleDeleteNewRow = (tabId: string, rowIndex: number) => { + // 删除新行 + 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 - + const tab = t as TableTab & { newRows: any[] } const newRows = [...(tab.newRows || [])] newRows.splice(rowIndex, 1) - - return { ...tab, newRows } + return { ...t, newRows } })) - setStatus({ text: '已删除新增行', type: 'info' }) - } + }, [setTabs]) - // 刷新表数据 - const handleRefreshTable = async (tabId: string) => { - const tab = tabs.find(t => t.id === tabId) as TableTab | undefined - if (!tab || !('tableName' in tab)) return - - // 设置加载状态 - setLoadingTables(prev => new Set(prev).add(tabId)) - setStatus({ text: `刷新 ${tab.tableName}...`, type: 'info' }) - - try { - const result = await api.getTableData( - tab.connectionId, tab.database, tab.tableName, tab.page, tab.pageSize - ) - const totalResult = await api.getTableData( - tab.connectionId, tab.database, tab.tableName, 1, 1 - ) - - setTabs(prev => prev.map(t => - t.id === tabId && 'tableName' in t - ? { - ...t, - data: result?.data || [], - total: totalResult?.total || tab.total, - originalData: result?.data || [], - pendingChanges: new Map(), - deletedRows: new Set(), - newRows: [] // 刷新时清空新增行 - } - : t - )) - setStatus({ text: `${tab.tableName}: ${totalResult?.total || 0} 行`, type: 'success' }) - } catch (err: any) { - setStatus({ text: err.message, type: 'error' }) - } finally { - setLoadingTables(prev => { - const next = new Set(prev) - next.delete(tabId) - return next - }) - } - } + // 新建查询 + const handleNewQuery = useCallback(() => { + const newTab: QueryTab = { id: `query-${Date.now()}`, title: `查询 ${tabs.filter(t => !('tableName' in t)).length + 1}`, sql: '', results: null } + setTabs(prev => [...prev, newTab]) + setActiveTab(newTab.id) + }, [tabs, setTabs, setActiveTab]) - // 使用防抖防止快速重复点击 - const handleRunQuery = useDebouncedCallback(async (tabId: string, sql: string) => { + // 执行查询 + const handleRunQuery = useCallback(async (tabId: string, sql: string) => { if (!activeConnection) { - setStatus({ text: '请先连接数据库', type: 'warning' }) + showNotification('error', '请先连接数据库') return } + await runQuery(tabId, activeConnection, sql) + }, [activeConnection, runQuery, showNotification]) - setStatus({ text: '执行中...', type: 'info' }) - const start = Date.now() - - const result = await api.query(activeConnection, sql) - const elapsed = ((Date.now() - start) / 1000).toFixed(2) - - if (result?.error) { - setStatus({ text: result.error, type: 'error' }) + // 更新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 { - setTabs(prev => prev.map(t => t.id === tabId ? { ...t, results: result, sql } : t)) - setStatus({ text: `${result?.rows?.length || 0} 行 (${elapsed}s)`, type: 'success' }) + addConnection(conn) } - }, 300) + }, [addConnection, updateConnection]) - // 数据库备份 - const handleBackupDatabase = async (database: string) => { - if (!activeConnection) { - setStatus({ text: '请先连接数据库', type: 'warning' }) - return + // 删除连接 + const handleDeleteConnection = useCallback(async (id: string) => { + if (connectedIds.has(id)) { + await handleDisconnect(id) } - - setStatus({ text: `正在备份 ${database}...`, type: 'info' }) - - const result = await api.backupDatabase(activeConnection, database) - - if (result?.error) { - setStatus({ text: `备份失败: ${result.error}`, type: 'error' }) - } else if (result?.cancelled) { - setStatus({ text: '备份已取消', type: 'warning' }) - } else if (result?.success) { - setStatus({ text: `备份成功: ${result.path}`, type: 'success' }) - } - } + deleteConnection(id) + }, [connectedIds, handleDisconnect, deleteConnection]) - // 导出表 - const handleExportTable = async (database: string, tableName: string, format: 'excel' | 'sql' | 'csv') => { - if (!activeConnection) { - setStatus({ text: '请先连接数据库', type: 'warning' }) - return + // 批量删除连接 + const handleDeleteConnections = useCallback(async (ids: string[]) => { + for (const id of ids) { + await handleDeleteConnection(id) } - - setStatus({ text: `正在导出 ${tableName}...`, type: 'info' }) - - const result = await api.exportTable(activeConnection, database, tableName, format) - - if (result?.error) { - setStatus({ text: `导出失败: ${result.error}`, type: 'error' }) - } else if (result?.cancelled) { - setStatus({ text: '导出已取消', type: 'warning' }) - } else if (result?.success) { - setStatus({ text: `导出成功: ${result.path}`, type: 'success' }) - } - } + }, [handleDeleteConnection]) - // ============ 数据库管理 ============ - const handleCreateDatabase = (connectionId: string) => { + // 编辑连接 + const handleEditConnection = useCallback((conn: Connection) => { + setEditingConnection(conn) + setNewConnectionType(undefined) + setShowConnectionModal(true) + }, []) + + // 创建数据库 + const handleCreateDatabase = useCallback((connectionId: string) => { setCreateDbConnectionId(connectionId) setShowCreateDbModal(true) - } + }, []) - const handleSubmitCreateDatabase = async (name: string, charset: string, collation: string) => { - if (!createDbConnectionId) return - - setShowCreateDbModal(false) - setStatus({ text: `正在创建数据库 ${name}...`, type: 'info' }) - - const result = await api.createDatabase(createDbConnectionId, name, charset, collation) - - if (result?.success) { - setStatus({ text: `数据库 ${name} 创建成功`, type: 'success' }) - // 刷新数据库列表 - const dbs = await api.getDatabases(createDbConnectionId) - setDatabasesMap(prev => new Map(prev).set(createDbConnectionId!, dbs || [])) - } else { - setStatus({ text: result?.message || '创建失败', type: 'error' }) - } - setCreateDbConnectionId(null) - } - - const handleDropDatabase = async (connectionId: string, database: string) => { - setStatus({ text: `正在删除数据库 ${database}...`, type: 'info' }) - - const result = await api.dropDatabase(connectionId, database) - - if (result?.success) { - setStatus({ text: `数据库 ${database} 已删除`, type: 'success' }) - // 刷新数据库列表 - const dbs = await api.getDatabases(connectionId) - setDatabasesMap(prev => new Map(prev).set(connectionId, dbs || [])) - // 清理该数据库的表数据 - setTablesMap(prev => { - const next = new Map(prev) - next.delete(database) - return next - }) - if (selectedDatabase === database) { - setSelectedDatabase(null) - } - } else { - setStatus({ text: result?.message || '删除失败', type: 'error' }) - } - } - - // ============ 表管理 ============ - const handleCreateTable = (connectionId: string, database: string) => { - // 使用表设计器创建表 - handleCreateTableWithDesigner(connectionId, database) - } - - const handleSubmitCreateTable = async (tableName: string, columns: any[]) => { - if (!createTableInfo) return - - const { connectionId, database } = createTableInfo - setShowCreateTableModal(false) - setStatus({ text: `正在创建表 ${tableName}...`, type: 'info' }) - - // 转换列定义格式 - const formattedColumns = columns.map(col => ({ - name: col.name, - type: col.length ? `${col.type}(${col.length})` : col.type, - nullable: col.nullable, - primaryKey: col.primaryKey, - autoIncrement: col.autoIncrement, - defaultValue: col.defaultValue, - comment: col.comment, - })) - - const result = await api.createTable(connectionId, database, tableName, formattedColumns) - - if (result?.success) { - setStatus({ text: `表 ${tableName} 创建成功`, type: 'success' }) - // 刷新表列表 - handleRefreshTables(connectionId, database) - } else { - setStatus({ text: result?.message || '创建失败', type: 'error' }) - } - setCreateTableInfo(null) - } - - const handleDropTable = async (connectionId: string, database: string, table: string) => { - setStatus({ text: `正在删除表 ${table}...`, type: 'info' }) - - const result = await api.dropTable(connectionId, database, table) - - if (result?.success) { - setStatus({ text: `表 ${table} 已删除`, type: 'success' }) - // 刷新表列表 - handleRefreshTables(connectionId, database) - // 关闭相关的表标签页 - setTabs(prev => prev.filter(t => !('tableName' in t) || t.tableName !== table || t.database !== database)) - } else { - setStatus({ text: result?.message || '删除失败', type: 'error' }) - } - } - - const handleTruncateTable = async (connectionId: string, database: string, table: string) => { - setStatus({ text: `正在清空表 ${table}...`, type: 'info' }) - - const result = await api.truncateTable(connectionId, database, table) - - if (result?.success) { - setStatus({ text: `表 ${table} 已清空`, type: 'success' }) - // 刷新打开的表标签页数据 - const tableTab = tabs.find(t => 'tableName' in t && t.tableName === table && t.database === database) - if (tableTab) { - handleRefreshTable(tableTab.id) - } - } else { - setStatus({ text: result?.message || '清空失败', type: 'error' }) - } - } - - const handleRenameTable = (connectionId: string, database: string, table: string) => { - setRenameInfo({ connectionId, database, table }) - setShowRenameDialog(true) - } - - const handleSubmitRenameTable = async (newName: string) => { - if (!renameInfo) return - - const { connectionId, database, table } = renameInfo - setShowRenameDialog(false) - setStatus({ text: `正在重命名表 ${table} -> ${newName}...`, type: 'info' }) - - const result = await api.renameTable(connectionId, database, table, newName) - - if (result?.success) { - setStatus({ text: `表已重命名为 ${newName}`, type: 'success' }) - // 刷新表列表 - handleRefreshTables(connectionId, database) - // 更新打开的表标签页 - setTabs(prev => prev.map(t => - ('tableName' in t && t.tableName === table && t.database === database) - ? { ...t, tableName: newName } - : t - )) - } else { - setStatus({ text: result?.message || '重命名失败', type: 'error' }) - } - setRenameInfo(null) - } - - const handleDuplicateTable = (connectionId: string, database: string, table: string) => { - setDuplicateInfo({ connectionId, database, table }) - setShowDuplicateDialog(true) - } - - const handleSubmitDuplicateTable = async (newName: string, withData: boolean) => { - if (!duplicateInfo) return - - const { connectionId, database, table } = duplicateInfo - setShowDuplicateDialog(false) - setStatus({ text: `正在复制表 ${table} -> ${newName}...`, type: 'info' }) - - const result = await api.duplicateTable(connectionId, database, table, newName, withData) - - if (result?.success) { - setStatus({ text: `表已复制为 ${newName}`, type: 'success' }) - // 刷新表列表 - handleRefreshTables(connectionId, database) - } else { - setStatus({ text: result?.message || '复制失败', type: 'error' }) - } - setDuplicateInfo(null) - } - - const handleRefreshTables = async (connectionId: string, database: string) => { - setLoadingDbSet(prev => new Set(prev).add(database)) - setStatus({ text: `刷新 ${database} 表列表...`, type: 'info' }) - + // 删除数据库 + const handleDropDatabase = useCallback(async (connectionId: string, database: string) => { try { - const tableList = await api.getTables(connectionId, database) - setTablesMap(prev => { - const next = new Map(prev) - next.set(database, tableList || []) - return next - }) - setStatus({ text: `${database}: ${tableList?.length || 0} 个表`, type: 'success' }) - } catch (err: any) { - setStatus({ text: err.message, type: 'error' }) - } finally { - setLoadingDbSet(prev => { - const next = new Set(prev) - next.delete(database) - return next - }) + await api.dropDatabase(connectionId, database) + showNotification('success', `数据库 ${database} 已删除`) + await fetchDatabases(connectionId) + } catch (err) { + showNotification('error', '删除失败:' + (err as Error).message) } - } + }, [fetchDatabases, showNotification]) - // ============ 表设计器 ============ - const handleDesignTable = (connectionId: string, database: string, tableName: string) => { - setTableDesignerInfo({ - mode: 'edit', - connectionId, - database, - tableName, + // 创建表 + const handleCreateTable = useCallback((connectionId: string, database: string) => { + setCreateTableContext({ connectionId, database }) + setShowCreateTableModal(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) + } }) - setShowTableDesigner(true) - } + }, [fetchTables, showNotification]) - const handleCreateTableWithDesigner = (connectionId: string, database: string) => { - setTableDesignerInfo({ - mode: 'create', - connectionId, - database, + // 复制表 + 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) + } }) - setShowTableDesigner(true) - } + }, [fetchTables, showNotification]) - const handleSaveTableDesign = async (sql: string): Promise<{ success: boolean; message: string }> => { - if (!tableDesignerInfo) return { success: false, message: '无效的操作' } - - const { connectionId, database } = tableDesignerInfo - setStatus({ text: '正在保存表结构...', type: 'info' }) - - const result = await api.executeMultiSQL(connectionId, sql) - - if (result?.success) { - setStatus({ text: '表结构保存成功', type: 'success' }) - // 刷新表列表 - handleRefreshTables(connectionId, database) - return { success: true, message: '保存成功' } - } else { - setStatus({ text: result?.message || '保存失败', type: 'error' }) - return { success: false, message: result?.message || '保存失败' } + // 刷新表列表 + const handleRefreshTables = useCallback(async (connectionId: string, database: string) => { + await fetchTables(connectionId, database) + showNotification('success', '已刷新') + }, [fetchTables, showNotification]) + + // 设计表 + const handleDesignTable = useCallback(async (connectionId: string, database: string, table: string) => { + showNotification('info', '表设计器开发中...') + }, [showNotification]) + + // 键盘快捷键 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.ctrlKey && e.key === 'q') { + e.preventDefault() + handleNewQuery() + } } - } - - const handleGetTableInfo = useCallback(async () => { - if (!tableDesignerInfo || tableDesignerInfo.mode !== 'edit' || !tableDesignerInfo.tableName) { - return { columns: [], indexes: [], foreignKeys: [], options: {} as any } - } - const { connectionId, database, tableName } = tableDesignerInfo - return await api.getTableInfo(connectionId, database, tableName) - }, [tableDesignerInfo]) - - const handleGetDatabasesForDesigner = async () => { - if (!tableDesignerInfo) return [] - return await api.getDatabases(tableDesignerInfo.connectionId) - } - - const handleGetTablesForDesigner = async (database: string) => { - if (!tableDesignerInfo) return [] - const tables = await api.getTables(tableDesignerInfo.connectionId, database) - return tables.map(t => t.name) - } - - const handleGetColumnsForDesigner = async (database: string, table: string) => { - if (!tableDesignerInfo) return [] - return await api.getColumnNames(tableDesignerInfo.connectionId, database, table) - } - - // 获取当前连接的数据库类型 - const getConnectionDbType = (connectionId: string): string => { - const conn = connections.find(c => c.id === connectionId) - return conn?.type || 'mysql' - } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [handleNewQuery]) return ( -
+
- -
+
{ setEditingConnection(null); setDefaultDbType(undefined); setShowModal(true) }} - onSelectConnection={handleSelectConnection} + onNewConnection={() => { setEditingConnection(null); setNewConnectionType(undefined); setShowConnectionModal(true) }} + onSelectConnection={setActiveConnection} onConnect={handleConnect} onDisconnect={handleDisconnect} - onEditConnection={(c) => { setEditingConnection(c); setShowModal(true) }} - onDeleteConnection={(id) => setConnections(prev => prev.filter(c => c.id !== id))} - onDeleteConnections={(ids) => { - // 先断开所有选中的已连接数据库 - ids.forEach(id => { - if (connectedIds.has(id)) { - api.disconnect(id) - } - }) - setConnectedIds(prev => { - const next = new Set(prev) - ids.forEach(id => next.delete(id)) - return next - }) - setConnections(prev => prev.filter(c => !ids.includes(c.id))) - setStatus({ text: `已删除 ${ids.length} 个连接`, type: 'success' }) - }} + onEditConnection={handleEditConnection} + onDeleteConnection={handleDeleteConnection} + onDeleteConnections={handleDeleteConnections} onSelectDatabase={handleSelectDatabase} onOpenTable={handleOpenTable} - onBackupDatabase={handleBackupDatabase} - onExportTable={handleExportTable} + onExportConnections={doExportConnections} + onImportConnections={doImportConnections} onCreateDatabase={handleCreateDatabase} onDropDatabase={handleDropDatabase} onCreateTable={handleCreateTable} @@ -996,69 +557,22 @@ export default function App() { onDuplicateTable={handleDuplicateTable} onRefreshTables={handleRefreshTables} onDesignTable={handleDesignTable} - onExportConnections={async (format) => { - const result = await api.exportConnections(connections, format) - if (result?.success) { - setStatus({ text: `已导出 ${result.count} 个连接到 ${result.path}`, type: 'success' }) - } else if (result?.error) { - setStatus({ text: result.error, type: 'error' }) - } - }} - onImportConnections={async () => { - const result = await api.importConnections() - if (result?.success && result.connections) { - // 合并连接(检查重名) - const existingNames = new Set(connections.map(c => c.name)) - const newConnections = result.connections.map(conn => { - let name = conn.name - let counter = 1 - while (existingNames.has(name)) { - name = `${conn.name} (${counter++})` - } - existingNames.add(name) - return { ...conn, name } - }) - setConnections(prev => [...prev, ...newConnections]) - setStatus({ - text: `已从 ${result.source} 导入 ${result.count} 个连接`, - type: 'success' - }) - } else if (result?.error) { - setStatus({ text: result.error, type: 'error' }) - } - }} /> - { - setTabs(prev => { - const remaining = prev.filter(t => t.id !== id) - // 如果关闭的是当前标签页,跳转到最近的标签页 - if (activeTab === id) { - const closedIndex = prev.findIndex(t => t.id === id) - if (remaining.length > 0) { - // 优先跳转到右边的标签页,如果没有则跳转到左边的 - const nextIndex = Math.min(closedIndex, remaining.length - 1) - setActiveTab(remaining[nextIndex].id) - } else { - setActiveTab('welcome') - } - } - return remaining - }) - }} - onNewQuery={() => handleNewQuery()} + databases={databasesMap.get(activeConnection || '') || []} + tables={tablesMap.get(selectedDatabase || '') || []} + columns={columnsMap} + onTabChange={handleTabChange} + onCloseTab={handleCloseTab} + onNewQuery={handleNewQuery} onRunQuery={handleRunQuery} - onUpdateSql={(id, sql) => setTabs(prev => prev.map(t => t.id === id && !('tableName' in t) ? { ...t, sql } : t))} - onUpdateTabTitle={(id, title) => setTabs(prev => prev.map(t => t.id === id && !('tableName' in t) ? { ...t, title } : t))} + onUpdateSql={handleUpdateSql} + onUpdateTabTitle={handleUpdateTabTitle} onLoadTablePage={handleLoadTablePage} onChangeTablePageSize={handleChangeTablePageSize} + onNewConnectionWithType={handleNewConnectionWithType} onUpdateTableCell={handleUpdateTableCell} onDeleteTableRow={handleDeleteTableRow} onDeleteTableRows={handleDeleteTableRows} @@ -1069,96 +583,69 @@ export default function App() { onUpdateNewRow={handleUpdateNewRow} onDeleteNewRow={handleDeleteNewRow} loadingTables={loadingTables} - onNewConnectionWithType={(type) => { - setEditingConnection(null) - setDefaultDbType(type) - setShowModal(true) - }} />
- - {/* Metro 风格状态栏 */} -
-
- - {status.text} -
- EasySQL -
- {showModal && ( - { setShowModal(false); setDefaultDbType(undefined) }} - /> + {/* 状态栏 */} +
+ 0 ? 'bg-success-500' : 'bg-text-disabled'}`} /> + {connectedIds.size > 0 ? `${connectedIds.size} 个连接` : '未连接'} + EasySQL v2.0 +
+ + {/* 通知 */} + {notification && ( +
+ {notification.type === 'success' && } + {notification.type === 'error' && } + {notification.type === 'info' && } + {notification.message} +
)} - {/* 创建数据库对话框 */} + {/* 模态框 */} + { setShowConnectionModal(false); setEditingConnection(null); setNewConnectionType(undefined) }} + onSave={handleSaveConnection} + /> + { setShowCreateDbModal(false); setCreateDbConnectionId(null) }} - onSubmit={handleSubmitCreateDatabase} + onCreated={async () => { + if (createDbConnectionId) await fetchDatabases(createDbConnectionId) + }} /> - {/* 创建表对话框 */} { setShowCreateTableModal(false); setCreateTableInfo(null) }} - onSubmit={handleSubmitCreateTable} + connectionId={createTableContext?.connectionId || null} + database={createTableContext?.database || null} + onClose={() => { setShowCreateTableModal(false); setCreateTableContext(null) }} + onCreated={async () => { + if (createTableContext) await fetchTables(createTableContext.connectionId, createTableContext.database) + }} /> - {/* 重命名表对话框 */} - } - onClose={() => { setShowRenameDialog(false); setRenameInfo(null) }} - onSubmit={handleSubmitRenameTable} - /> - - {/* 复制表对话框 */} - } - showDataOption - onClose={() => { setShowDuplicateDialog(false); setDuplicateInfo(null) }} - onSubmit={(name) => handleSubmitDuplicateTable(name, false)} - onSubmitWithData={handleSubmitDuplicateTable} - /> - - {/* 表设计器 */} - {showTableDesigner && tableDesignerInfo && ( - { setShowTableDesigner(false); setTableDesignerInfo(null) }} - onSave={handleSaveTableDesign} - onGetTableInfo={tableDesignerInfo.mode === 'edit' ? handleGetTableInfo : undefined} - onGetDatabases={handleGetDatabasesForDesigner} - onGetTables={handleGetTablesForDesigner} - onGetColumns={handleGetColumnsForDesigner} + {inputDialog && ( + setInputDialog(null)} + onConfirm={inputDialog.onConfirm} /> )}
) } + +export default App diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 86cd381..b4a528c 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -1,353 +1,426 @@ -import { useState, useEffect } from 'react' -import { X, Loader2, Shield, FolderOpen } from 'lucide-react' -import { Connection, DatabaseType, DB_INFO } from '../types' +import { X, Database, Check, AlertCircle, ChevronDown, ChevronRight, Shield, Globe, Server, Key, User, Folder, FileText } from 'lucide-react' +import { Connection, DB_INFO, DatabaseType } from '../types' +import { useState, useEffect, useRef } from 'react' import api from '../lib/electron-api' interface Props { - connection: Connection | null - defaultType?: DatabaseType - onSave: (conn: Connection) => void + isOpen: boolean + editingConnection?: Connection | null + initialType?: DatabaseType onClose: () => void + onSave: (conn: Omit & { id?: string }) => void } -export default function ConnectionModal({ connection, defaultType, onSave, onClose }: Props) { - const initialType = defaultType || 'mysql' - const initialPort = DB_INFO[initialType]?.port || 3306 - - const [form, setForm] = useState({ - id: '', - name: '', - type: initialType, - host: 'localhost', - port: initialPort, - username: '', - password: '', - database: '', - sshEnabled: false, - sshHost: '', - sshPort: 22, - sshUser: '', - sshPassword: '', - sshKey: '', - }) - const [testing, setTesting] = useState(false) - const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null) +export default function ConnectionModal({ isOpen, editingConnection, initialType, onClose, onSave }: Props) { + const [selectedType, setSelectedType] = useState(editingConnection?.type || initialType || 'mysql') + const [name, setName] = useState(editingConnection?.name || '') + const [host, setHost] = useState(editingConnection?.host || 'localhost') + const [port, setPort] = useState(editingConnection?.port || DB_INFO[selectedType].defaultPort) + const [username, setUsername] = useState(editingConnection?.username || '') + const [password, setPassword] = useState(editingConnection?.password || '') + const [database, setDatabase] = useState(editingConnection?.database || '') + const [file, setFile] = useState(editingConnection?.file || '') + const [useSSH, setUseSSH] = useState(editingConnection?.ssh?.enabled || false) + const [sshHost, setSshHost] = useState(editingConnection?.ssh?.host || '') + const [sshPort, setSshPort] = useState(editingConnection?.ssh?.port || 22) + const [sshUser, setSshUser] = useState(editingConnection?.ssh?.username || '') + const [sshPassword, setSshPassword] = useState(editingConnection?.ssh?.password || '') + const [sshKeyFile, setSshKeyFile] = useState(editingConnection?.ssh?.privateKeyPath || '') + const [showAdvanced, setShowAdvanced] = useState(false) + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) + + const nameInputRef = useRef(null) useEffect(() => { - if (connection) { - setForm(connection) - } else { - const type = defaultType || 'mysql' - const port = DB_INFO[type]?.port || 3306 - setForm(prev => ({ - ...prev, - id: `conn-${Date.now()}`, - type, - port, - name: DB_INFO[type]?.name || '' - })) + if (isOpen) { + const timer = setTimeout(() => nameInputRef.current?.focus(), 100) + return () => clearTimeout(timer) } - }, [connection, defaultType]) + }, [isOpen]) + + useEffect(() => { + if (editingConnection) { + setSelectedType(editingConnection.type) + setName(editingConnection.name) + setHost(editingConnection.host || 'localhost') + setPort(editingConnection.port || DB_INFO[editingConnection.type].defaultPort) + setUsername(editingConnection.username || '') + setPassword(editingConnection.password || '') + setDatabase(editingConnection.database || '') + setFile(editingConnection.file || '') + setUseSSH(editingConnection.ssh?.enabled || false) + setSshHost(editingConnection.ssh?.host || '') + setSshPort(editingConnection.ssh?.port || 22) + setSshUser(editingConnection.ssh?.username || '') + setSshPassword(editingConnection.ssh?.password || '') + setSshKeyFile(editingConnection.ssh?.privateKeyPath || '') + } else { + const type = initialType || 'mysql' + setSelectedType(type) + setName('') + setHost('localhost') + setPort(DB_INFO[type].defaultPort) + setUsername('') + setPassword('') + setDatabase('') + setFile('') + setUseSSH(false) + setSshHost('') + setSshPort(22) + setSshUser('') + setSshPassword('') + setSshKeyFile('') + } + setMessage(null) + }, [editingConnection, isOpen, initialType]) const handleTypeChange = (type: DatabaseType) => { - const info = DB_INFO[type] - setForm(prev => ({ ...prev, type, port: info?.port || prev.port })) + setSelectedType(type) + setPort(DB_INFO[type].defaultPort) + setMessage(null) } const handleTest = async () => { - setTesting(true) - setMessage(null) - - const result = await api.testConnection(form) - setMessage({ - text: result?.message || '测试失败', - type: result?.success ? 'success' : 'error' - }) - setTesting(false) + try { + const connData = buildConnection() + const result = await api.testConnection(connData) + if (result.success) { + setMessage({ type: 'success', text: '连接成功!' }) + } else { + setMessage({ type: 'error', text: result.error || '连接失败' }) + } + } catch (err) { + setMessage({ type: 'error', text: '测试失败:' + (err as Error).message }) + } + setTimeout(() => setMessage(null), 3000) + } + + const buildConnection = (): Omit & { id?: string } => { + const info = DB_INFO[selectedType] + return { + ...(editingConnection?.id ? { id: editingConnection.id } : {}), + type: selectedType, + name: name || `${info.name} 连接`, + host: info.needsHost ? host : undefined, + port: info.needsHost ? port : undefined, + username: info.needsAuth ? username : undefined, + password: info.needsAuth ? password : undefined, + database: database || undefined, + file: info.needsFile ? file : undefined, + ssh: useSSH && info.needsHost ? { enabled: true, host: sshHost, port: sshPort, username: sshUser, password: sshPassword || undefined, privateKeyPath: sshKeyFile || undefined } : undefined, + } } const handleSave = () => { - if (!form.name.trim()) { - setMessage({ text: '请输入连接名称', type: 'error' }) + if (!name.trim()) { + setMessage({ type: 'error', text: '请输入连接名称' }) + setTimeout(() => setMessage(null), 3000) return } - onSave(form) + onSave(buildConnection()) + onClose() } + const handleSelectFile = async () => { + const filePath = await api.selectFile([{ name: 'SQLite', extensions: ['db', 'sqlite', 'sqlite3'] }]) + if (filePath) setFile(filePath) + } + + const handleSelectKeyFile = async () => { + const filePath = await api.selectFile([{ name: 'PEM', extensions: ['pem', 'key', 'ppk'] }]) + if (filePath) setSshKeyFile(filePath) + } + + if (!isOpen) return null + + const info = DB_INFO[selectedType] + const isEditing = !!editingConnection + return ( -
-
- - {/* Metro 风格弹窗 */} -
- {/* 标题栏 */} -
- {connection ? '编辑连接' : '新建连接'} -
{/* 内容 */} -
- {/* 连接名称 */} -
- - setForm(prev => ({ ...prev, name: e.target.value }))} - placeholder="输入名称" - className="w-full h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
- - {/* 数据库类型 - Metro 磁贴选择 */} -
- -
- {(Object.entries(DB_INFO) as [DatabaseType, typeof DB_INFO[DatabaseType]][]).map(([key, info]) => ( - - ))} -
-
- - {/* SQLite 文件选择 */} - {form.type === 'sqlite' ? ( +
+
+ {/* 数据库类型选择 */}
- -
- setForm(prev => ({ ...prev, database: e.target.value }))} - placeholder="选择或输入 .db 文件路径" - className="flex-1 h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> - + +
+ {(Object.entries(DB_INFO) as [DatabaseType, typeof DB_INFO[DatabaseType]][]) + .filter(([, i]) => i.supported) + .map(([type, i]) => ( + + ))}
-

如果文件不存在,将创建新的数据库

- ) : ( - <> - {/* 主机和端口 */} -
-
- + + {/* 连接名称 */} +
+ + setName(e.target.value)} + placeholder={`我的${info.name}连接`} + className="w-full h-10 px-3 bg-light-surface border border-border-default rounded-lg focus:border-primary-500 focus:shadow-focus transition-all" + /> +
+ + {/* SQLite 文件路径 */} + {info.needsFile && ( +
+ +
setForm(prev => ({ ...prev, host: e.target.value }))} - placeholder="localhost" - className="w-full h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" + value={file} + onChange={(e) => setFile(e.target.value)} + placeholder="选择或输入 .db 文件路径" + className="flex-1 h-10 px-3 bg-light-surface border border-border-default rounded-lg focus:border-primary-500 focus:shadow-focus transition-all" /> -
-
- - setForm(prev => ({ ...prev, port: parseInt(e.target.value) || 0 }))} - className="w-full h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
-
- - {/* 用户名密码 - Redis 只需要密码 */} - {form.type === 'redis' ? ( -
- - setForm(prev => ({ ...prev, password: e.target.value }))} - placeholder="无密码时留空" - className="w-full h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
- ) : ( -
-
- - setForm(prev => ({ ...prev, username: e.target.value }))} - placeholder={form.type === 'mongodb' ? '无认证时留空' : 'root'} - className="w-full h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
-
- - setForm(prev => ({ ...prev, password: e.target.value }))} - placeholder={form.type === 'mongodb' ? '无认证时留空' : ''} - className="w-full h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
-
- )} - - {/* 数据库 */} -
- - setForm(prev => ({ ...prev, database: e.target.value }))} - placeholder={form.type === 'mongodb' ? '默认 admin' : '留空表示连接所有数据库'} - className="w-full h-10 px-4 bg-metro-surface border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
- - )} - - {/* SSH */} -
- - - {form.sshEnabled && ( -
-
-
- - setForm(prev => ({ ...prev, sshHost: e.target.value }))} - className="w-full h-9 px-3 bg-metro-bg border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
-
- - setForm(prev => ({ ...prev, sshPort: parseInt(e.target.value) || 22 }))} - className="w-full h-9 px-3 bg-metro-bg border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
-
-
-
- - setForm(prev => ({ ...prev, sshUser: e.target.value }))} - className="w-full h-9 px-3 bg-metro-bg border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
-
- - setForm(prev => ({ ...prev, sshPassword: e.target.value }))} - className="w-full h-9 px-3 bg-metro-bg border-2 border-transparent - focus:border-accent-blue text-sm transition-all rounded-sm" - /> -
+
)} -
- {/* 消息 */} - {message && ( -
- {message.text} -
- )} + {/* 主机和端口 */} + {info.needsHost && ( +
+
+ + setHost(e.target.value)} + placeholder="localhost" + className="w-full h-10 px-3 bg-light-surface border border-border-default rounded-lg focus:border-primary-500 focus:shadow-focus transition-all" + /> +
+
+ + setPort(parseInt(e.target.value) || 0)} + className="w-full h-10 px-3 bg-light-surface border border-border-default rounded-lg focus:border-primary-500 focus:shadow-focus transition-all" + /> +
+
+ )} + + {/* 认证信息 */} + {info.needsAuth && ( +
+
+ + setUsername(e.target.value)} + placeholder="root" + className="w-full h-10 px-3 bg-light-surface border border-border-default rounded-lg focus:border-primary-500 focus:shadow-focus transition-all" + /> +
+
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full h-10 px-3 bg-light-surface border border-border-default rounded-lg focus:border-primary-500 focus:shadow-focus transition-all" + /> +
+
+ )} + + {/* 数据库名称 */} + {info.needsHost && ( +
+ + setDatabase(e.target.value)} + placeholder="连接后自动选择的数据库" + className="w-full h-10 px-3 bg-light-surface border border-border-default rounded-lg focus:border-primary-500 focus:shadow-focus transition-all" + /> +
+ )} + + {/* SSH 设置 */} + {info.needsHost && ( +
+ + + {showAdvanced && ( +
+ + + {useSSH && ( +
+
+
+ + setSshHost(e.target.value)} + className="w-full h-9 px-3 bg-white border border-border-default rounded-lg text-sm focus:border-primary-500 focus:shadow-focus" + /> +
+
+ + setSshPort(parseInt(e.target.value) || 22)} + className="w-full h-9 px-3 bg-white border border-border-default rounded-lg text-sm focus:border-primary-500 focus:shadow-focus" + /> +
+
+
+
+ + setSshUser(e.target.value)} + className="w-full h-9 px-3 bg-white border border-border-default rounded-lg text-sm focus:border-primary-500 focus:shadow-focus" + /> +
+
+ + setSshPassword(e.target.value)} + className="w-full h-9 px-3 bg-white border border-border-default rounded-lg text-sm focus:border-primary-500 focus:shadow-focus" + /> +
+
+
+ +
+ setSshKeyFile(e.target.value)} + placeholder="~/.ssh/id_rsa" + className="flex-1 h-9 px-3 bg-white border border-border-default rounded-lg text-sm focus:border-primary-500 focus:shadow-focus" + /> + +
+
+
+ )} +
+ )} +
+ )} + + {/* 消息提示 */} + {message && ( +
+ {message.type === 'success' ? : } + {message.text} +
+ )} +
{/* 底部按钮 */} -
- - - +
diff --git a/src/components/CreateDatabaseModal.tsx b/src/components/CreateDatabaseModal.tsx index 2f93555..70a3ad9 100644 --- a/src/components/CreateDatabaseModal.tsx +++ b/src/components/CreateDatabaseModal.tsx @@ -1,10 +1,12 @@ -import { useState } from 'react' -import { X, Database } from 'lucide-react' +import { useState, useEffect } from 'react' +import { X, Database, Settings } from 'lucide-react' +import api from '../lib/electron-api' interface Props { isOpen: boolean + connectionId: string | null onClose: () => void - onSubmit: (name: string, charset: string, collation: string) => void + onCreated: () => void } // MySQL 字符集和排序规则 @@ -16,10 +18,21 @@ const CHARSETS = [ { name: 'gb2312', collations: ['gb2312_chinese_ci', 'gb2312_bin'] }, ] -export default function CreateDatabaseModal({ isOpen, onClose, onSubmit }: Props) { +export default function CreateDatabaseModal({ isOpen, connectionId, onClose, onCreated }: Props) { const [name, setName] = useState('') const [charset, setCharset] = useState('utf8mb4') const [collation, setCollation] = useState('utf8mb4_general_ci') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + if (isOpen) { + setName('') + setCharset('utf8mb4') + setCollation('utf8mb4_general_ci') + setError('') + } + }, [isOpen]) if (!isOpen) return null @@ -34,57 +47,76 @@ export default function CreateDatabaseModal({ isOpen, onClose, onSubmit }: Props } } - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (name.trim()) { - onSubmit(name.trim(), charset, collation) - setName('') - setCharset('utf8mb4') - setCollation('utf8mb4_general_ci') + if (!name.trim() || !connectionId) return + + setLoading(true) + setError('') + + try { + await api.createDatabase(connectionId, name.trim(), charset, collation) + onCreated() + onClose() + } catch (err) { + setError((err as Error).message) + } finally { + setLoading(false) } } return ( -
-
+
+
+ +
{/* 标题栏 */} -
-
- - 新建数据库 +
+
+
+ +
+
+

新建数据库

+

创建新的数据库

+
{/* 表单 */} -
+
-
- + setCollation(e.target.value)} - className="w-full h-9 px-3 bg-metro-surface border border-metro-border text-sm - focus:border-accent-blue focus:outline-none transition-colors" + className="w-full h-10 px-3 bg-light-surface border border-border-default rounded-lg + focus:border-primary-500 text-sm transition-all cursor-pointer" > {collations.map(col => ( @@ -106,22 +138,30 @@ export default function CreateDatabaseModal({ isOpen, onClose, onSubmit }: Props
+ {error && ( +
+ {error} +
+ )} + {/* 按钮 */}
@@ -129,4 +169,3 @@ export default function CreateDatabaseModal({ isOpen, onClose, onSubmit }: Props
) } - diff --git a/src/components/CreateTableModal.tsx b/src/components/CreateTableModal.tsx index d6af6db..faa29e4 100644 --- a/src/components/CreateTableModal.tsx +++ b/src/components/CreateTableModal.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react' -import { X, Table2, Plus, Trash2, Key, ArrowUp, ArrowDown } from 'lucide-react' +import { useState, useEffect } from 'react' +import { X, Table2, Plus, Trash2, Key, ArrowUp, ArrowDown, Check } from 'lucide-react' +import api from '../lib/electron-api' interface ColumnDef { id: string @@ -15,9 +16,10 @@ interface ColumnDef { interface Props { isOpen: boolean - database: string + connectionId: string | null + database: string | null onClose: () => void - onSubmit: (tableName: string, columns: ColumnDef[]) => void + onCreated: () => void } // 常用数据类型 @@ -41,11 +43,21 @@ const DEFAULT_COLUMN: Omit = { comment: '', } -export default function CreateTableModal({ isOpen, database, onClose, onSubmit }: Props) { +export default function CreateTableModal({ isOpen, connectionId, database, onClose, onCreated }: Props) { const [tableName, setTableName] = useState('') const [columns, setColumns] = useState([ { ...DEFAULT_COLUMN, id: crypto.randomUUID(), name: 'id', primaryKey: true, autoIncrement: true, nullable: false } ]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + if (isOpen) { + setTableName('') + setColumns([{ ...DEFAULT_COLUMN, id: crypto.randomUUID(), name: 'id', primaryKey: true, autoIncrement: true, nullable: false }]) + setError('') + } + }, [isOpen]) if (!isOpen) return null @@ -63,11 +75,9 @@ export default function CreateTableModal({ isOpen, database, onClose, onSubmit } setColumns(columns.map(col => { if (col.id !== id) return col const updated = { ...col, [field]: value } - // 主键不能为空 if (field === 'primaryKey' && value) { updated.nullable = false } - // 自增必须是主键 if (field === 'autoIncrement' && value) { updated.primaryKey = true updated.nullable = false @@ -86,63 +96,81 @@ export default function CreateTableModal({ isOpen, database, onClose, onSubmit } setColumns(newColumns) } - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (tableName.trim() && columns.some(c => c.name.trim())) { - onSubmit(tableName.trim(), columns.filter(c => c.name.trim())) - setTableName('') - setColumns([{ ...DEFAULT_COLUMN, id: crypto.randomUUID(), name: 'id', primaryKey: true, autoIncrement: true, nullable: false }]) + if (!tableName.trim() || !columns.some(c => c.name.trim()) || !connectionId || !database) return + + setLoading(true) + setError('') + + try { + await api.createTable(connectionId, database, tableName.trim(), columns.filter(c => c.name.trim())) + onCreated() + onClose() + } catch (err) { + setError((err as Error).message) + } finally { + setLoading(false) } } - // 检查是否需要长度 const needsLength = (type: string) => { return ['VARCHAR', 'CHAR', 'DECIMAL', 'FLOAT', 'DOUBLE', 'BINARY', 'VARBINARY'].includes(type) } return ( -
-
+
+
+ +
{/* 标题栏 */} -
-
- - 新建表 - {database} +
+
+
+ +
+
+

新建表

+

+ 数据库: {database} +

+
{/* 表单 */}
{/* 表名 */} -
-