790 lines
30 KiB
TypeScript
790 lines
30 KiB
TypeScript
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<string | null>(null)
|
|
const [selectedDatabase, setSelectedDatabase] = useState<string | null>(null)
|
|
const [showConnectionModal, setShowConnectionModal] = useState(false)
|
|
const [editingConnection, setEditingConnection] = useState<Connection | null>(null)
|
|
const [newConnectionType, setNewConnectionType] = useState<DatabaseType | undefined>()
|
|
const [showCreateDbModal, setShowCreateDbModal] = useState(false)
|
|
const [createDbConnectionId, setCreateDbConnectionId] = useState<string | null>(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<string, any>; 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<number> }
|
|
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<number> }
|
|
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<string, any>; deletedRows: Set<number>; 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<string, any> = {}
|
|
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<string, any> = {}
|
|
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<Connection, 'id'> & { 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 (
|
|
<div className="h-screen flex flex-col bg-white overflow-hidden font-sans">
|
|
<TitleBar />
|
|
<div className="flex-1 flex min-h-0">
|
|
<Sidebar
|
|
connections={connections}
|
|
activeConnection={activeConnection}
|
|
connectedIds={connectedIds}
|
|
databasesMap={databasesMap}
|
|
tablesMap={tablesMap}
|
|
selectedDatabase={selectedDatabase}
|
|
loadingDbSet={loadingDbSet}
|
|
loadingConnectionsSet={loadingConnectionsSet}
|
|
onNewConnection={() => { 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}
|
|
/>
|
|
<MainContent
|
|
tabs={tabs}
|
|
activeTab={activeTab}
|
|
activeConnection={activeConnection}
|
|
selectedDatabase={selectedDatabase}
|
|
connections={connections}
|
|
connectedIds={connectedIds}
|
|
databasesMap={databasesMap}
|
|
databases={databasesMap.get(activeConnection || '') || []}
|
|
tables={tablesMap.get(selectedDatabase || '') || []}
|
|
columns={columnsMap}
|
|
onTabChange={handleTabChange}
|
|
onCloseTab={handleCloseTab}
|
|
onNewQuery={handleNewQuery}
|
|
onRunQuery={handleRunQuery}
|
|
onUpdateSql={handleUpdateSql}
|
|
onUpdateTabTitle={handleUpdateTabTitle}
|
|
onLoadTablePage={handleLoadTablePage}
|
|
onChangeTablePageSize={handleChangeTablePageSize}
|
|
onNewConnectionWithType={handleNewConnectionWithType}
|
|
onUpdateTableCell={handleUpdateTableCell}
|
|
onDeleteTableRow={handleDeleteTableRow}
|
|
onDeleteTableRows={handleDeleteTableRows}
|
|
onSaveTableChanges={handleSaveTableChanges}
|
|
onDiscardTableChanges={handleDiscardTableChanges}
|
|
onRefreshTable={handleRefreshTable}
|
|
onAddTableRow={handleAddTableRow}
|
|
onUpdateNewRow={handleUpdateNewRow}
|
|
onDeleteNewRow={handleDeleteNewRow}
|
|
onSelectConnection={handleConnectionChange}
|
|
onSelectDatabase={handleSelectDatabase}
|
|
loadingTables={loadingTables}
|
|
/>
|
|
</div>
|
|
|
|
{/* 状态栏 */}
|
|
<div className="h-6 bg-light-surface flex items-center px-3 text-xs border-t border-border-default text-text-tertiary">
|
|
<span className={`w-2 h-2 rounded-full mr-2 ${connectedIds.size > 0 ? 'bg-success-500' : 'bg-text-disabled'}`} />
|
|
<span>{connectedIds.size > 0 ? `${connectedIds.size} 个连接` : '未连接'}</span>
|
|
<span className="ml-auto font-mono text-text-muted">EasySQL v{__APP_VERSION__}</span>
|
|
</div>
|
|
|
|
{/* 通知 */}
|
|
{notification && (
|
|
<div className={`fixed bottom-12 right-4 px-4 py-3 rounded-xl shadow-lg flex items-center gap-2 animate-slide-up z-50
|
|
${notification.type === 'success' ? 'bg-white text-success-600 border border-success-200' :
|
|
notification.type === 'error' ? 'bg-white text-danger-600 border border-danger-200' :
|
|
'bg-white text-primary-600 border border-primary-200'}`}>
|
|
{notification.type === 'success' && <CheckCircle size={16} />}
|
|
{notification.type === 'error' && <XCircle size={16} />}
|
|
{notification.type === 'info' && <AlertCircle size={16} />}
|
|
<span className="text-sm">{notification.message}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 模态框 */}
|
|
<ConnectionModal
|
|
isOpen={showConnectionModal}
|
|
editingConnection={editingConnection}
|
|
initialType={newConnectionType}
|
|
onClose={() => { setShowConnectionModal(false); setEditingConnection(null); setNewConnectionType(undefined) }}
|
|
onSave={handleSaveConnection}
|
|
/>
|
|
|
|
<CreateDatabaseModal
|
|
isOpen={showCreateDbModal}
|
|
connectionId={createDbConnectionId}
|
|
onClose={() => { setShowCreateDbModal(false); setCreateDbConnectionId(null) }}
|
|
onCreated={async () => {
|
|
if (createDbConnectionId) await fetchDatabases(createDbConnectionId)
|
|
}}
|
|
/>
|
|
|
|
<CreateTableModal
|
|
isOpen={showCreateTableModal}
|
|
connectionId={createTableContext?.connectionId || null}
|
|
database={createTableContext?.database || null}
|
|
onClose={() => { setShowCreateTableModal(false); setCreateTableContext(null) }}
|
|
onCreated={async () => {
|
|
if (createTableContext) await fetchTables(createTableContext.connectionId, createTableContext.database)
|
|
}}
|
|
/>
|
|
|
|
{showTableDesigner && tableDesignerContext && (
|
|
<TableDesigner
|
|
isOpen={showTableDesigner}
|
|
mode={tableDesignerContext.mode}
|
|
database={tableDesignerContext.database}
|
|
tableName={tableDesignerContext.tableName}
|
|
connectionId={tableDesignerContext.connectionId}
|
|
dbType={connections.find(c => 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 && (
|
|
<InputDialog
|
|
isOpen={inputDialog.isOpen}
|
|
title={inputDialog.title}
|
|
label={inputDialog.label}
|
|
defaultValue={inputDialog.defaultValue}
|
|
onClose={() => setInputDialog(null)}
|
|
onConfirm={inputDialog.onConfirm}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default App
|