feat(native-window): 支持标签页拖出为原生独立窗口

- 支持 SQL、数据和结果标签拖出到跨显示器原生窗口
- 通过受认证 loopback bridge 复用主进程后端与状态
- 支持子窗口继续拆窗、聚焦、关闭、还原与异常退出恢复
- 补充多窗口协议、状态同步和跨平台回归测试
This commit is contained in:
Syngnat
2026-07-16 20:25:08 +08:00
parent d3dee295d6
commit fa9d21be8b
33 changed files with 5036 additions and 33 deletions

View File

@@ -64,6 +64,8 @@ import {
takeQueryEditorResultSession,
} from '../utils/queryEditorResultSessionCache';
import { buildEditableTriggerSql } from '../utils/triggerEditSql';
import { openNativeQueryResultWindow } from '../utils/nativeDetachedWindowHost';
import { isNativeDetachedWindow } from '../utils/nativeDetachedWindowClient';
import {
getColumnDefinitionComment,
getColumnDefinitionKey,
@@ -1427,6 +1429,23 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
);
const isResultPanelVisibleRef = useRef(isResultPanelVisible);
isResultPanelVisibleRef.current = isResultPanelVisible;
const publishesDetachedResultSession = useMemo(() => isNativeDetachedWindow(), []);
useEffect(() => {
const captureSession = (event: Event) => {
const requestedTabId = String((event as CustomEvent).detail?.tabId || '').trim();
if (requestedTabId !== tab.id) return;
saveQueryEditorResultSession(tab.id, {
resultSets: resultSetsRef.current,
activeResultKey: activeResultKeyRef.current,
isResultPanelVisible: isResultPanelVisibleRef.current,
});
};
window.addEventListener('gonavi:capture-query-result-session', captureSession);
return () => {
window.removeEventListener('gonavi:capture-query-result-session', captureSession);
};
}, [tab.id]);
useEffect(() => {
// Keep result panel state across detach/attach remounts of the same tab.
@@ -1438,6 +1457,15 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
});
};
}, [tab.id]);
useEffect(() => {
if (!publishesDetachedResultSession) return;
saveQueryEditorResultSession(tab.id, {
resultSets,
activeResultKey,
isResultPanelVisible,
});
}, [activeResultKey, isResultPanelVisible, publishesDetachedResultSession, resultSets, tab.id]);
const shortcutOptions = useStore(state => state.shortcutOptions);
const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform());
const runQueryShortcutBinding = useMemo(
@@ -8470,7 +8498,7 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
? translate('query_editor.results_panel.tab.message', { index: index + 1 })
: translate('query_editor.results_panel.detached.title', { index: index + 1 });
const windowId = `query-result:${tab.id}:${target.key}`;
useStore.getState().detachQueryResultWindow({
const detachedWindow = {
id: windowId,
sourceQueryTabId: tab.id,
connectionId: currentConnectionId || tab.connectionId || '',
@@ -8500,8 +8528,14 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
showRowNumberColumn: target.showRowNumberColumn,
truncated: target.truncated,
},
});
handleCloseResult(key);
};
void openNativeQueryResultWindow(detachedWindow)
.then((opened) => {
if (opened) handleCloseResult(key);
})
.catch((error) => {
message.error(error instanceof Error ? error.message : String(error));
});
};
React.useEffect(() => {