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.

This commit is contained in:
Ethanfly 2026-01-04 11:27:04 +08:00
parent 5591081812
commit 80a70d3120
5 changed files with 133 additions and 42 deletions

View File

@ -24,24 +24,42 @@ const sshTunnels = new Map()
const configPath = path.join(app.getPath('userData'), 'connections.json') const configPath = path.join(app.getPath('userData'), 'connections.json')
// SQL.js 初始化 // SQL.js 初始化
let SQL = null let SQL = null
// 用于分配本地端口
let nextLocalPort = 33060
// ============ SSH 隧道管理 ============ // ============ 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 隧道 * 创建 SSH 隧道
* @param {Object} config - 连接配置 * @param {Object} config - 连接配置
* @returns {Promise<{ssh, server, localPort, localHost}>} * @returns {Promise<{ssh, server, localPort, localHost}>}
*/ */
async function createSSHTunnel(config) { async function createSSHTunnel(config) {
// 先找一个可用端口
const localPort = await findAvailablePort()
console.log(`[SSH] 使用本地端口: ${localPort}`)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ssh = new SSHClient() const ssh = new SSHClient()
const localPort = nextLocalPort++
// 端口范围重置
if (nextLocalPort > 65000) nextLocalPort = 33060
let server = null let server = null
let connected = false let connected = false

View File

@ -1,6 +1,6 @@
{ {
"name": "easysql", "name": "easysql",
"version": "2.0.3", "version": "2.0.12",
"description": "Modern Database Management Tool", "description": "Modern Database Management Tool",
"main": "electron/main.js", "main": "electron/main.js",
"type": "module", "type": "module",

View File

@ -109,6 +109,36 @@ function App() {
next.delete(id) next.delete(id)
return next 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 => { setTabs(prev => {
const remainingTabs = prev.filter(tab => { const remainingTabs = prev.filter(tab => {
@ -124,26 +154,32 @@ function App() {
} }
return remainingTabs return remainingTabs
}) })
if (activeConnection === id) setActiveConnection(null) // 如果断开的是当前选中数据库的连接,清空选中状态
if (activeConnection === id) {
setActiveConnection(null)
setSelectedDatabase(null)
}
showNotification('info', '连接已断开') showNotification('info', '连接已断开')
} catch (err) { } catch (err) {
showNotification('error', '断开失败:' + (err as Error).message) 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) => { const handleSelectDatabase = useCallback(async (db: string, connectionId: string) => {
setSelectedDatabase(db) setSelectedDatabase(db)
setActiveConnection(connectionId) setActiveConnection(connectionId)
setLoadingDbSet(prev => new Set(prev).add(db)) // 使用 connectionId_db 作为 key避免不同连接同名数据库冲突
const dbKey = `${connectionId}_${db}`
setLoadingDbSet(prev => new Set(prev).add(dbKey))
try { try {
await fetchTables(connectionId, db) 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))) await Promise.all(tables.map(t => fetchColumns(connectionId, db, t.name)))
} finally { } finally {
setLoadingDbSet(prev => { setLoadingDbSet(prev => {
const next = new Set(prev) const next = new Set(prev)
next.delete(db) next.delete(dbKey)
return next return next
}) })
} }
@ -630,7 +666,7 @@ function App() {
connectedIds={connectedIds} connectedIds={connectedIds}
databasesMap={databasesMap} databasesMap={databasesMap}
databases={databasesMap.get(activeConnection || '') || []} databases={databasesMap.get(activeConnection || '') || []}
tables={tablesMap.get(selectedDatabase || '') || []} tables={tablesMap.get(activeConnection && selectedDatabase ? `${activeConnection}_${selectedDatabase}` : '') || []}
columns={columnsMap} columns={columnsMap}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onCloseTab={handleCloseTab} onCloseTab={handleCloseTab}
@ -749,8 +785,10 @@ function App() {
} : undefined} } : undefined}
onGetDatabases={async () => databasesMap.get(tableDesignerContext.connectionId) || []} onGetDatabases={async () => databasesMap.get(tableDesignerContext.connectionId) || []}
onGetTables={async (db) => { 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) { if (cached && cached.length > 0) {
return cached.map(t => t.name) return cached.map(t => t.name)
} }
@ -758,7 +796,7 @@ function App() {
try { try {
const tables = await api.getTables(tableDesignerContext.connectionId, db) 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) return tables.map((t: any) => t.name || t)
} catch (err) { } catch (err) {
console.error('Failed to load tables:', err) console.error('Failed to load tables:', err)

View File

@ -23,8 +23,9 @@ const TableGroupList = memo(function TableGroupList({
const regularTables = tables.filter(t => !t.isView) const regularTables = tables.filter(t => !t.isView)
const views = tables.filter(t => t.isView) const views = tables.filter(t => t.isView)
const tablesKey = `${db}_tables` // 使用 connectionId + db 组合作为 key避免不同连接同名数据库状态冲突
const viewsKey = `${db}_views` const tablesKey = `${connectionId}_${db}_tables`
const viewsKey = `${connectionId}_${db}_views`
const isTablesExpanded = expandedDbs.has(tablesKey) const isTablesExpanded = expandedDbs.has(tablesKey)
const isViewsExpanded = expandedDbs.has(viewsKey) const isViewsExpanded = expandedDbs.has(viewsKey)
@ -225,10 +226,12 @@ export default function Sidebar({
const prevConnectedIdsRef = useRef<Set<string>>(new Set()) const prevConnectedIdsRef = useRef<Set<string>>(new Set())
useEffect(() => { useEffect(() => {
if (selectedDatabase) { // 自动展开选中的数据库,使用 connectionId_database 作为 key
setExpandedDbs(prev => new Set(prev).add(selectedDatabase)) if (selectedDatabase && activeConnection) {
const dbKey = `${activeConnection}_${selectedDatabase}`
setExpandedDbs(prev => new Set(prev).add(dbKey))
} }
}, [selectedDatabase]) }, [selectedDatabase, activeConnection])
// 当连接状态变化时,只展开新建立的连接(不影响其他已连接但被折叠的连接) // 当连接状态变化时,只展开新建立的连接(不影响其他已连接但被折叠的连接)
useEffect(() => { useEffect(() => {
@ -240,6 +243,22 @@ export default function Sidebar({
setExpandedDbs(prev => new Set(prev).add(id)) 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) prevConnectedIdsRef.current = new Set(connectedIds)
}, [connectedIds]) }, [connectedIds])
@ -263,34 +282,38 @@ export default function Sidebar({
} }
}, [handleSidebarKeyDown]) }, [handleSidebarKeyDown])
const getFilteredTables = (db: string) => { // 使用 connectionId_db 作为 key 获取表列表
const dbTables = tablesMap.get(db) || [] const getFilteredTables = (connectionId: string, db: string) => {
const dbKey = `${connectionId}_${db}`
const dbTables = tablesMap.get(dbKey) || []
if (!searchQuery) return dbTables if (!searchQuery) return dbTables
return dbTables.filter(t => t.name.toLowerCase().includes(searchQuery.toLowerCase())) return dbTables.filter(t => t.name.toLowerCase().includes(searchQuery.toLowerCase()))
} }
const dbHasMatchingTables = (db: string) => { const dbHasMatchingTables = (connectionId: string, db: string) => {
if (!searchQuery) return false 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())) return dbTables.some(t => t.name.toLowerCase().includes(searchQuery.toLowerCase()))
} }
const getFilteredDatabases = (connDatabases: string[]) => { const getFilteredDatabases = (connectionId: string, connDatabases: string[]) => {
return connDatabases.filter(db => { return connDatabases.filter(db => {
if (!searchQuery) return true if (!searchQuery) return true
const query = searchQuery.toLowerCase() const query = searchQuery.toLowerCase()
if (db.toLowerCase().includes(query)) return true if (db.toLowerCase().includes(query)) return true
if (dbHasMatchingTables(db)) return true if (dbHasMatchingTables(connectionId, db)) return true
return false return false
}) })
} }
useEffect(() => { useEffect(() => {
if (searchQuery) { if (searchQuery) {
databasesMap.forEach((dbs) => { databasesMap.forEach((dbs, connectionId) => {
dbs.forEach(db => { dbs.forEach(db => {
if (dbHasMatchingTables(db)) { if (dbHasMatchingTables(connectionId, db)) {
setExpandedDbs(prev => new Set(prev).add(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 const showDatabases = isExpanded && isConnected && connDatabases.length > 0
return ( return (
<div key={conn.id}> <div key={conn.id} className="relative">
<div <div
className={`group flex items-center gap-2 px-2.5 py-2 cursor-pointer transition-all rounded-lg className={`group flex items-center gap-2 px-2.5 py-2 cursor-pointer transition-all rounded-lg
${isExpanded && isConnected ? 'sticky top-0 z-20 !bg-[#f8fafc]' : ''}
${isSelected ? 'bg-primary-50 ring-1 ring-primary-200' : ''} ${isSelected ? 'bg-primary-50 ring-1 ring-primary-200' : ''}
${isActive && !isSelected ? 'bg-light-hover' : 'hover:bg-light-hover'}`} ${isActive && !isSelected ? 'bg-light-hover' : 'hover:bg-light-hover'}`}
onClick={() => { onClick={() => {
@ -525,24 +549,33 @@ export default function Sidebar({
<div className="px-2.5 py-2 text-sm text-text-muted"> <div className="px-2.5 py-2 text-sm text-text-muted">
</div> </div>
) : getFilteredDatabases(connDatabases).map(db => { ) : getFilteredDatabases(conn.id, connDatabases).map(db => {
const isDbSelected = selectedDatabase === db // 选中状态需要同时匹配连接和数据库名
const isDbExpanded = expandedDbs.has(db) const isDbSelected = selectedDatabase === db && activeConnection === conn.id
const dbTables = getFilteredTables(db) // 使用 connectionId + db 组合作为 key避免不同连接同名数据库状态冲突
const isLoading = loadingDbSet.has(db) const dbKey = `${conn.id}_${db}`
const isDbExpanded = expandedDbs.has(dbKey)
const dbTables = getFilteredTables(conn.id, db)
const isLoading = loadingDbSet.has(dbKey)
return ( return (
<div key={db}> <div key={db} className="relative">
<div <div
className={`flex items-center gap-2 px-2.5 py-1.5 cursor-pointer text-sm transition-all rounded-lg mx-1 className={`flex items-center gap-2 px-2.5 py-1.5 cursor-pointer text-sm transition-all rounded-lg mx-1
${isDbExpanded ? 'sticky top-[40px] z-10 !bg-[#f8fafc] -mx-1 px-3.5' : ''}
${isDbSelected ? 'bg-primary-50 text-primary-700' : 'text-text-secondary hover:bg-light-hover'}`} ${isDbSelected ? 'bg-primary-50 text-primary-700' : 'text-text-secondary hover:bg-light-hover'}`}
onClick={() => { onClick={() => {
const willExpand = !expandedDbs.has(db) const willExpand = !expandedDbs.has(dbKey)
if (willExpand) onSelectDatabase(db, conn.id) // 获取原始缓存数据(不经过搜索过滤)
const cachedTables = tablesMap.get(dbKey)
// 只在展开且缓存为空时才加载数据
if (willExpand && (!cachedTables || cachedTables.length === 0)) {
onSelectDatabase(db, conn.id)
}
setExpandedDbs(prev => { setExpandedDbs(prev => {
const next = new Set(prev) const next = new Set(prev)
if (next.has(db)) next.delete(db) if (next.has(dbKey)) next.delete(dbKey)
else next.add(db) else next.add(dbKey)
return next return next
}) })
}} }}

View File

@ -328,7 +328,9 @@ export function useTableOperations(showNotification: (type: 'success' | 'error'
const fetchTables = useCallback(async (connectionId: string, database: string) => { const fetchTables = useCallback(async (connectionId: string, database: string) => {
try { try {
const tables = await api.getTables(connectionId, database) 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) { } catch (err) {
showNotification('error', '获取表列表失败') showNotification('error', '获取表列表失败')
} }