🐛 fix(sidebar): 修复断开连接后无法重连及状态残留问题

- 递归清除断开连接/关闭数据库时所有子节点的 loadedKeys 和 connectionStates
- 解决 Ant Design Tree 因状态残留导致不再触发 loadData 的问题
- DataGrid: 优化 ResizeObserver 逻辑,引入 requestAnimationFrame 解决标签页切换高度塌陷
- DataGrid: 为每个表格实例生成唯一 ID 以隔离 CSS 样式冲突
- CSS: 强制禁止侧边栏文字选中,优化右键菜单触发区域
This commit is contained in:
杨国锋
2026-02-02 17:53:54 +08:00
parent 9dbea2f93a
commit 4ac8522dab
4 changed files with 40 additions and 41 deletions

View File

@@ -5,6 +5,7 @@ import { ReloadOutlined, ImportOutlined, ExportOutlined, DownOutlined, PlusOutli
import { Resizable } from 'react-resizable';
import { ImportData, ExportTable, ExportData, ApplyChanges } from '../../wailsjs/go/app/App';
import { useStore } from '../store';
import { v4 as uuidv4 } from 'uuid';
import 'react-resizable/css/styles.css';
// --- Helper: Format Value ---
@@ -214,6 +215,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const addSqlLog = useStore(state => state.addSqlLog);
const [form] = Form.useForm();
const [modal, contextHolder] = Modal.useModal();
const gridId = useMemo(() => `grid-${uuidv4()}`, []);
// Helper to export specific data
const exportData = async (rows: any[], format: string) => {
@@ -234,16 +236,26 @@ const DataGrid: React.FC<DataGridProps> = ({
useEffect(() => {
if (!containerRef.current) return;
let rafId: number;
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
// Subtract header height (~40px)
// Ensure minimum height to prevent collapse loop
const h = Math.max(100, entry.contentRect.height - 42);
setTableHeight(h);
}
rafId = requestAnimationFrame(() => {
for (let entry of entries) {
// Use boundingClientRect for more accurate render size (including padding if any)
const height = entry.contentRect.height;
if (height < 50) return;
// Subtract header (~42px) and a buffer
const h = Math.max(100, height - 42);
setTableHeight(h);
}
});
});
resizeObserver.observe(containerRef.current);
return () => resizeObserver.disconnect();
return () => {
resizeObserver.disconnect();
cancelAnimationFrame(rafId);
};
}, []);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
@@ -685,7 +697,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const totalWidth = columns.reduce((sum, col) => sum + (col.width as number || 200), 0);
return (
<div style={{ height: '100%', overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column' }}>
<div className={gridId} style={{ height: '100%', overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
{/* Toolbar */}
<div style={{ padding: '8px', borderBottom: '1px solid #eee', display: 'flex', gap: 8, alignItems: 'center' }}>
{onReload && <Button icon={<ReloadOutlined />} onClick={() => {
@@ -747,7 +759,7 @@ const DataGrid: React.FC<DataGridProps> = ({
</div>
)}
<div ref={containerRef} style={{ flex: 1, overflow: 'hidden', position: 'relative' }}>
<div ref={containerRef} style={{ flex: 1, overflow: 'hidden', position: 'relative', minHeight: 0 }}>
{contextHolder}
<Form component={false} form={form}>
<DataContext.Provider value={{ selectedRowKeysRef, displayDataRef, handleCopyInsert, handleCopyJson, handleCopyCsv, handleExportSelected, copyToClipboard, tableName }}>
@@ -794,9 +806,9 @@ const DataGrid: React.FC<DataGridProps> = ({
)}
<style>{`
.row-added td { background-color: #f6ffed !important; }
.row-modified td { background-color: #e6f7ff !important; }
.ant-table-body {
.${gridId} .row-added td { background-color: #f6ffed !important; }
.${gridId} .row-modified td { background-color: #e6f7ff !important; }
.${gridId} .ant-table-body {
height: ${tableHeight}px !important;
max-height: ${tableHeight}px !important;
}

View File

@@ -162,6 +162,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
}, [tab, sortInfo, filterConditions]); // Initial load and re-load on sort/filter
return (
<div style={{ height: '100%', width: '100%', overflow: 'hidden' }}>
<DataGrid
data={data}
columnNames={columnNames}
@@ -178,6 +179,7 @@ const DataViewer: React.FC<{ tab: TabData }> = ({ tab }) => {
onToggleFilter={handleToggleFilter}
onApplyFilter={handleApplyFilter}
/>
</div>
);
};

View File

@@ -503,13 +503,21 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
label: '断开连接',
icon: <DisconnectOutlined />,
onClick: () => {
// Reset status recursively
setConnectionStates(prev => {
const next = { ...prev };
delete next[node.key];
Object.keys(next).forEach(k => {
if (k === node.key || k.startsWith(`${node.key}-`)) {
delete next[k];
}
});
return next;
});
setExpandedKeys(prev => prev.filter(k => k !== node.key));
setLoadedKeys(prev => prev.filter(k => k !== node.key));
// Collapse node and children
setExpandedKeys(prev => prev.filter(k => k !== node.key && !k.toString().startsWith(`${node.key}-`)));
// Reset loaded state recursively
setLoadedKeys(prev => prev.filter(k => k !== node.key && !k.toString().startsWith(`${node.key}-`)));
// Clear children (undefined to trigger reload)
setTreeData(origin => updateTreeData(origin, node.key, undefined));
message.success("已断开连接");
}
@@ -553,8 +561,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
delete next[node.key];
return next;
});
setExpandedKeys(prev => prev.filter(k => k !== node.key));
setLoadedKeys(prev => prev.filter(k => k !== node.key));
setExpandedKeys(prev => prev.filter(k => k !== node.key && !k.toString().startsWith(`${node.key}-`)));
setLoadedKeys(prev => prev.filter(k => k !== node.key && !k.toString().startsWith(`${node.key}-`)));
setTreeData(origin => updateTreeData(origin, node.key, undefined));
}
},

View File

@@ -10,19 +10,7 @@ import (
// Generic DB Methods
func (a *App) DBConnect(config connection.ConnectionConfig) connection.QueryResult {
key := getCacheKey(config)
// Use an anonymous function to scope the lock
func() {
a.mu.Lock()
defer a.mu.Unlock()
if oldDB, ok := a.dbCache[key]; ok {
oldDB.Close()
delete(a.dbCache, key)
}
}()
// getDatabase acquires the lock internally, so we must be unlocked here
// getDatabase checks cache and Pings. If valid, reuses. If not, connects.
_, err := a.getDatabase(config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
@@ -32,17 +20,6 @@ func (a *App) DBConnect(config connection.ConnectionConfig) connection.QueryResu
}
func (a *App) TestConnection(config connection.ConnectionConfig) connection.QueryResult {
// Force close existing cached connection if any to ensure fresh test
key := getCacheKey(config)
func() {
a.mu.Lock()
defer a.mu.Unlock()
if oldDB, ok := a.dbCache[key]; ok {
oldDB.Close()
delete(a.dbCache, key)
}
}()
_, err := a.getDatabase(config)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}