🐛 fix(query-editor): 修复异常退出后SQL草稿丢失

- 为查询页签增加 crash-recovery 草稿快照并按 160ms 落盘
- 启动恢复时补齐缺失的 query tabs 与连接上下文
- 保存或关闭查询后清理临时草稿并补充恢复回归测试
This commit is contained in:
Syngnat
2026-06-29 19:21:37 +08:00
parent dac9e1f571
commit bedd1eceb5
5 changed files with 469 additions and 31 deletions

View File

@@ -47,7 +47,12 @@ import {
ORACLE_ROWID_LOCATOR_COLUMN,
type EditRowLocator,
} from '../utils/rowLocator';
import { getQueryTabDraft, hasQueryTabDraft, setQueryTabDraft, setSQLFileTabDraft } from '../utils/sqlFileTabDrafts';
import {
clearQueryTabDraft,
getQueryTabDraft,
hasQueryTabDraft,
persistQueryTabDraftSnapshot,
} from '../utils/sqlFileTabDrafts';
import { buildEditableTriggerSql } from '../utils/triggerEditSql';
import {
getColumnDefinitionComment,
@@ -778,6 +783,15 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const savedQueries = useStore(state => state.savedQueries);
const currentConnectionIdRef = useRef(currentConnectionId);
const currentDbRef = useRef(currentDb);
const draftSnapshotTab = useMemo(() => ({
id: tab.id,
title: tab.title,
connectionId: tab.connectionId,
dbName: tab.dbName,
filePath: tab.filePath,
savedQueryId: tab.savedQueryId,
readOnly: tab.readOnly,
}), [tab.connectionId, tab.dbName, tab.filePath, tab.id, tab.readOnly, tab.savedQueryId, tab.title]);
const connectionsRef = useRef(connections);
const columnsCacheRef = useRef<Record<string, ColumnDefinition[]>>({});
const saveQuery = useStore(state => state.saveQuery);
@@ -948,12 +962,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
const syncQueryDraft = useCallback((nextQuery: string) => {
const next = String(nextQuery ?? '');
lastLocalQueryRef.current = next;
if (isExternalSQLFileTab) {
setSQLFileTabDraft(tab.id, next);
return;
}
setQueryTabDraft(tab.id, next);
}, [isExternalSQLFileTab, tab.id]);
persistQueryTabDraftSnapshot(draftSnapshotTab, next, {
connectionId: currentConnectionIdRef.current,
dbName: currentDbRef.current,
});
}, [draftSnapshotTab]);
const applyQueryState = useCallback((nextQuery: string) => {
const next = String(nextQuery ?? '');
@@ -964,8 +977,11 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
}, [isExternalSQLFileTab, syncQueryDraft]);
useEffect(() => {
setQueryTabDraft(tab.id, query);
}, [query, tab.id]);
persistQueryTabDraftSnapshot(draftSnapshotTab, query, {
connectionId: currentConnectionIdRef.current || currentConnectionId,
dbName: currentDbRef.current || currentDb,
});
}, [currentConnectionId, currentDb, draftSnapshotTab, query]);
useEffect(() => {
currentConnectionIdRef.current = currentConnectionId;
@@ -1017,13 +1033,25 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
});
}, [currentConnectionId, currentDb, isExternalSQLFileTab, tab.id, updateQueryTabDraft]);
const getCurrentQuery = useCallback(() => {
const val = editorRef.current?.getValue?.();
if (typeof val === 'string') return val;
return query || '';
}, [query]);
useEffect(() => {
if (!isExternalSQLFileTab) return;
setSQLFileTabDraft(tab.id, getCurrentQuery());
persistQueryTabDraftSnapshot(draftSnapshotTab, getCurrentQuery(), {
connectionId: currentConnectionIdRef.current,
dbName: currentDbRef.current,
});
return () => {
setSQLFileTabDraft(tab.id, getCurrentQuery());
persistQueryTabDraftSnapshot(draftSnapshotTab, getCurrentQuery(), {
connectionId: currentConnectionIdRef.current,
dbName: currentDbRef.current,
});
};
}, [isExternalSQLFileTab, tab.id]);
}, [draftSnapshotTab, getCurrentQuery, isExternalSQLFileTab]);
// 当此 Tab 成为活跃 Tab 时,将本实例的状态同步到模块级共享变量
// 确保 completion provider 始终使用当前活跃 Tab 的上下文
@@ -1198,12 +1226,6 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
refreshObjectDecorations(QUERY_EDITOR_LIVE_DECORATION_MAX_TEXT_LENGTH);
}, [currentDb, refreshObjectDecorations]);
const getCurrentQuery = () => {
const val = editorRef.current?.getValue?.();
if (typeof val === 'string') return val;
return query || '';
};
const insertTextIntoEditorAtPosition = useCallback((text: string, position?: { lineNumber: number; column: number } | null) => {
const editor = editorRef.current;
const monaco = monacoRef.current;
@@ -5187,6 +5209,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
dbName: currentDb || tab.dbName || '',
savedQueryId: persisted.id,
});
clearQueryTabDraft(tab.id);
return persisted;
};
@@ -5216,7 +5239,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
filePath,
savedQueryId: undefined,
});
setSQLFileTabDraft(tab.id, sql);
clearQueryTabDraft(tab.id);
message.success(translate('query_editor.message.sql_file_saved'));
} catch (error) {
message.error(translate('query_editor.message.save_sql_file_failed', {