mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-06 20:03:05 +08:00
🐛 fix(sidebar): 修复断开连接后无法重连及状态残留问题
- 递归清除断开连接/关闭数据库时所有子节点的 loadedKeys 和 connectionStates - 解决 Ant Design Tree 因状态残留导致不再触发 loadData 的问题 - DataGrid: 优化 ResizeObserver 逻辑,引入 requestAnimationFrame 解决标签页切换高度塌陷 - DataGrid: 为每个表格实例生成唯一 ID 以隔离 CSS 样式冲突 - CSS: 强制禁止侧边栏文字选中,优化右键菜单触发区域
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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()}
|
||||
|
||||
Reference in New Issue
Block a user