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', '获取表列表失败') }