From 80a70d3120c21ba13c7082730a9c45bd1d99799d Mon Sep 17 00:00:00 2001 From: Ethanfly Date: Sun, 4 Jan 2026 11:27:04 +0800 Subject: [PATCH] Update package version to 2.0.12 and enhance SSH tunnel management in main.js by implementing a function to find available ports. Refactor App component to improve connection state management and caching for database tables and columns. Update Sidebar component to handle database state using connectionId and database name as keys, ensuring better separation of data across connections. --- electron/main.js | 32 +++++++++++---- package.json | 2 +- src/App.tsx | 54 +++++++++++++++++++++---- src/components/Sidebar.tsx | 83 ++++++++++++++++++++++++++------------ src/lib/hooks.ts | 4 +- 5 files changed, 133 insertions(+), 42 deletions(-) diff --git a/electron/main.js b/electron/main.js index 65dc2fc..9217708 100644 --- a/electron/main.js +++ b/electron/main.js @@ -24,24 +24,42 @@ const sshTunnels = new Map() const configPath = path.join(app.getPath('userData'), 'connections.json') // SQL.js 初始化 let SQL = null -// 用于分配本地端口 -let nextLocalPort = 33060 // ============ SSH 隧道管理 ============ +/** + * 查找可用端口 + */ +function findAvailablePort(startPort = 49152) { + return new Promise((resolve, reject) => { + const server = net.createServer() + server.unref() + server.on('error', () => { + // 端口被占用,尝试下一个 + if (startPort < 65535) { + resolve(findAvailablePort(startPort + 1)) + } else { + reject(new Error('没有可用端口')) + } + }) + server.listen(startPort, '127.0.0.1', () => { + server.close(() => resolve(startPort)) + }) + }) +} + /** * 创建 SSH 隧道 * @param {Object} config - 连接配置 * @returns {Promise<{ssh, server, localPort, localHost}>} */ async function createSSHTunnel(config) { + // 先找一个可用端口 + const localPort = await findAvailablePort() + console.log(`[SSH] 使用本地端口: ${localPort}`) + return new Promise((resolve, reject) => { const ssh = new SSHClient() - const localPort = nextLocalPort++ - - // 端口范围重置 - if (nextLocalPort > 65000) nextLocalPort = 33060 - let server = null let connected = false diff --git a/package.json b/package.json index 0d4eafb..b916196 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "easysql", - "version": "2.0.3", + "version": "2.0.12", "description": "Modern Database Management Tool", "main": "electron/main.js", "type": "module", diff --git a/src/App.tsx b/src/App.tsx index f23c3f2..43a01aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -109,6 +109,36 @@ function App() { next.delete(id) return next }) + // 清理该连接的表缓存(key 格式:connectionId_database) + setTablesMap(prev => { + const next = new Map(prev) + for (const key of prev.keys()) { + if (key.startsWith(`${id}_`)) { + next.delete(key) + } + } + return next + }) + // 清理该连接的列缓存(key 格式:connectionId_database_table) + setColumnsMap(prev => { + const next = new Map(prev) + for (const key of prev.keys()) { + if (key.startsWith(`${id}_`)) { + next.delete(key) + } + } + return next + }) + // 清理该连接的加载状态 + setLoadingDbSet(prev => { + const next = new Set(prev) + for (const key of prev) { + if (key.startsWith(`${id}_`)) { + next.delete(key) + } + } + return next + }) // 关闭该连接的所有标签页 setTabs(prev => { const remainingTabs = prev.filter(tab => { @@ -124,26 +154,32 @@ function App() { } return remainingTabs }) - if (activeConnection === id) setActiveConnection(null) + // 如果断开的是当前选中数据库的连接,清空选中状态 + if (activeConnection === id) { + setActiveConnection(null) + setSelectedDatabase(null) + } showNotification('info', '连接已断开') } catch (err) { showNotification('error', '断开失败:' + (err as Error).message) } - }, [activeConnection, activeTab, setConnectedIds, setDatabasesMap, setTabs, setActiveTab, showNotification]) + }, [activeConnection, activeTab, setConnectedIds, setDatabasesMap, setTablesMap, setColumnsMap, setLoadingDbSet, setTabs, setActiveTab, showNotification]) // 选择数据库 const handleSelectDatabase = useCallback(async (db: string, connectionId: string) => { setSelectedDatabase(db) setActiveConnection(connectionId) - setLoadingDbSet(prev => new Set(prev).add(db)) + // 使用 connectionId_db 作为 key,避免不同连接同名数据库冲突 + const dbKey = `${connectionId}_${db}` + setLoadingDbSet(prev => new Set(prev).add(dbKey)) try { await fetchTables(connectionId, db) - const tables = tablesMap.get(db) || [] + const tables = tablesMap.get(dbKey) || [] await Promise.all(tables.map(t => fetchColumns(connectionId, db, t.name))) } finally { setLoadingDbSet(prev => { const next = new Set(prev) - next.delete(db) + next.delete(dbKey) return next }) } @@ -630,7 +666,7 @@ function App() { connectedIds={connectedIds} databasesMap={databasesMap} databases={databasesMap.get(activeConnection || '') || []} - tables={tablesMap.get(selectedDatabase || '') || []} + tables={tablesMap.get(activeConnection && selectedDatabase ? `${activeConnection}_${selectedDatabase}` : '') || []} columns={columnsMap} onTabChange={handleTabChange} onCloseTab={handleCloseTab} @@ -749,8 +785,10 @@ function App() { } : undefined} onGetDatabases={async () => databasesMap.get(tableDesignerContext.connectionId) || []} onGetTables={async (db) => { + // 使用 connectionId_db 作为 key + const dbKey = `${tableDesignerContext.connectionId}_${db}` // 如果缓存中有表列表,直接返回 - const cached = tablesMap.get(db) + const cached = tablesMap.get(dbKey) if (cached && cached.length > 0) { return cached.map(t => t.name) } @@ -758,7 +796,7 @@ function App() { try { const tables = await api.getTables(tableDesignerContext.connectionId, db) // 更新缓存 - setTablesMap(prev => new Map(prev).set(db, tables)) + setTablesMap(prev => new Map(prev).set(dbKey, tables)) return tables.map((t: any) => t.name || t) } catch (err) { console.error('Failed to load tables:', err) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index ca28cc1..488599e 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -23,8 +23,9 @@ const TableGroupList = memo(function TableGroupList({ const regularTables = tables.filter(t => !t.isView) const views = tables.filter(t => t.isView) - const tablesKey = `${db}_tables` - const viewsKey = `${db}_views` + // 使用 connectionId + db 组合作为 key,避免不同连接同名数据库状态冲突 + const tablesKey = `${connectionId}_${db}_tables` + const viewsKey = `${connectionId}_${db}_views` const isTablesExpanded = expandedDbs.has(tablesKey) const isViewsExpanded = expandedDbs.has(viewsKey) @@ -225,10 +226,12 @@ export default function Sidebar({ const prevConnectedIdsRef = useRef>(new Set()) useEffect(() => { - if (selectedDatabase) { - setExpandedDbs(prev => new Set(prev).add(selectedDatabase)) + // 自动展开选中的数据库,使用 connectionId_database 作为 key + if (selectedDatabase && activeConnection) { + const dbKey = `${activeConnection}_${selectedDatabase}` + setExpandedDbs(prev => new Set(prev).add(dbKey)) } - }, [selectedDatabase]) + }, [selectedDatabase, activeConnection]) // 当连接状态变化时,只展开新建立的连接(不影响其他已连接但被折叠的连接) useEffect(() => { @@ -240,6 +243,22 @@ export default function Sidebar({ setExpandedDbs(prev => new Set(prev).add(id)) } }) + // 找出断开的连接,清理相关展开状态 + prevIds.forEach(id => { + if (!connectedIds.has(id)) { + // 清理断开连接的展开状态(连接ID及其下所有数据库) + setExpandedDbs(prev => { + const next = new Set(prev) + for (const key of prev) { + // 匹配连接ID本身或者 connectionId_xxx 格式的 key + if (key === id || key.startsWith(`${id}_`)) { + next.delete(key) + } + } + return next + }) + } + }) // 更新引用 prevConnectedIdsRef.current = new Set(connectedIds) }, [connectedIds]) @@ -263,34 +282,38 @@ export default function Sidebar({ } }, [handleSidebarKeyDown]) - const getFilteredTables = (db: string) => { - const dbTables = tablesMap.get(db) || [] + // 使用 connectionId_db 作为 key 获取表列表 + const getFilteredTables = (connectionId: string, db: string) => { + const dbKey = `${connectionId}_${db}` + const dbTables = tablesMap.get(dbKey) || [] if (!searchQuery) return dbTables return dbTables.filter(t => t.name.toLowerCase().includes(searchQuery.toLowerCase())) } - const dbHasMatchingTables = (db: string) => { + const dbHasMatchingTables = (connectionId: string, db: string) => { if (!searchQuery) return false - const dbTables = tablesMap.get(db) || [] + const dbKey = `${connectionId}_${db}` + const dbTables = tablesMap.get(dbKey) || [] return dbTables.some(t => t.name.toLowerCase().includes(searchQuery.toLowerCase())) } - const getFilteredDatabases = (connDatabases: string[]) => { + const getFilteredDatabases = (connectionId: string, connDatabases: string[]) => { return connDatabases.filter(db => { if (!searchQuery) return true const query = searchQuery.toLowerCase() if (db.toLowerCase().includes(query)) return true - if (dbHasMatchingTables(db)) return true + if (dbHasMatchingTables(connectionId, db)) return true return false }) } useEffect(() => { if (searchQuery) { - databasesMap.forEach((dbs) => { + databasesMap.forEach((dbs, connectionId) => { dbs.forEach(db => { - if (dbHasMatchingTables(db)) { - setExpandedDbs(prev => new Set(prev).add(db)) + if (dbHasMatchingTables(connectionId, db)) { + const dbKey = `${connectionId}_${db}` + setExpandedDbs(prev => new Set(prev).add(dbKey)) } }) }) @@ -454,9 +477,10 @@ export default function Sidebar({ const showDatabases = isExpanded && isConnected && connDatabases.length > 0 return ( -
+
{ @@ -525,24 +549,33 @@ export default function Sidebar({
无数据库或无权限
- ) : getFilteredDatabases(connDatabases).map(db => { - const isDbSelected = selectedDatabase === db - const isDbExpanded = expandedDbs.has(db) - const dbTables = getFilteredTables(db) - const isLoading = loadingDbSet.has(db) + ) : getFilteredDatabases(conn.id, connDatabases).map(db => { + // 选中状态需要同时匹配连接和数据库名 + const isDbSelected = selectedDatabase === db && activeConnection === conn.id + // 使用 connectionId + db 组合作为 key,避免不同连接同名数据库状态冲突 + const dbKey = `${conn.id}_${db}` + const isDbExpanded = expandedDbs.has(dbKey) + const dbTables = getFilteredTables(conn.id, db) + const isLoading = loadingDbSet.has(dbKey) return ( -
+
{ - const willExpand = !expandedDbs.has(db) - if (willExpand) onSelectDatabase(db, conn.id) + const willExpand = !expandedDbs.has(dbKey) + // 获取原始缓存数据(不经过搜索过滤) + const cachedTables = tablesMap.get(dbKey) + // 只在展开且缓存为空时才加载数据 + if (willExpand && (!cachedTables || cachedTables.length === 0)) { + onSelectDatabase(db, conn.id) + } setExpandedDbs(prev => { const next = new Set(prev) - if (next.has(db)) next.delete(db) - else next.add(db) + if (next.has(dbKey)) next.delete(dbKey) + else next.add(dbKey) return next }) }} diff --git a/src/lib/hooks.ts b/src/lib/hooks.ts index f269477..e67212c 100644 --- a/src/lib/hooks.ts +++ b/src/lib/hooks.ts @@ -328,7 +328,9 @@ export function useTableOperations(showNotification: (type: 'success' | 'error' const fetchTables = useCallback(async (connectionId: string, database: string) => { try { const tables = await api.getTables(connectionId, database) - setTablesMap(prev => new Map(prev).set(database, tables)) + // 使用 connectionId_database 作为 key,避免不同连接同名数据库冲突 + const key = `${connectionId}_${database}` + setTablesMap(prev => new Map(prev).set(key, tables)) } catch (err) { showNotification('error', '获取表列表失败') }