fix: 修复应用长时间挂后台后恢复时表格数据永久加载的问题

三层防护:
- MySQL 连接获取时执行 ping 健康检查,死连接自动重连
- macOS Reopen 事件 + 前端 visibilitychange 触发连接刷新
- 前端 Promise.race 超时保护,确保 loading 状态不会超过 60s
This commit is contained in:
t8y2
2026-05-29 15:12:14 +08:00
parent 6c0db6f517
commit b54eda1889
8 changed files with 157 additions and 13 deletions
+2
View File
@@ -25,6 +25,7 @@ import { useDialogSources } from "@/composables/useDialogSources";
import { useNavigationTargets } from "@/composables/useNavigationTargets";
import { useDataGridActions } from "@/composables/useDataGridActions";
import { useTauriEvents } from "@/composables/useTauriEvents";
import { useVisibilityChange } from "@/composables/useVisibilityChange";
import "@/i18n";
import { translateBackendError } from "@/i18n/backend-errors";
import * as api from "@/lib/api";
@@ -211,6 +212,7 @@ const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({
openSqlFilePath,
openConnectionDeepLink,
});
useVisibilityChange();
const appVersion = ref("");
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
@@ -0,0 +1,32 @@
import { onMounted, onUnmounted } from "vue";
import { refreshConnections } from "@/lib/api";
import { useQueryStore } from "@/stores/queryStore";
let hiddenAt: number | null = null;
function handleVisibilityChange() {
if (document.hidden) {
hiddenAt = Date.now();
} else {
const wasHidden = hiddenAt;
hiddenAt = null;
if (wasHidden && Date.now() - wasHidden > 30_000) {
refreshConnections().catch(() => {});
const queryStore = useQueryStore();
const stuckTabs = queryStore.tabs.filter((t) => t.isExecuting);
if (stuckTabs.length > 0) {
queryStore.notifyConnectionMayBeLost();
}
}
}
}
export function useVisibilityChange() {
onMounted(() => {
document.addEventListener("visibilitychange", handleVisibilityChange);
});
onUnmounted(() => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
});
}
+19 -8
View File
@@ -708,14 +708,12 @@ export const useQueryStore = defineStore("query", () => {
clientSessionId: tab.id,
timeoutSecs: queryTimeoutSecs,
};
const results = await api.executeMulti(
tab.connectionId,
tab.database,
sqlToExecute,
tab.schema,
executionId,
executionOptions,
);
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
const timeoutError = new Error(`查询超时 (${frontendTimeoutSecs}s),请检查数据库连接是否正常`);
const results = await Promise.race([
api.executeMulti(tab.connectionId, tab.database, sqlToExecute, tab.schema, executionId, executionOptions),
new Promise<never>((_, reject) => setTimeout(() => reject(timeoutError), frontendTimeoutSecs * 1000)),
]);
console.info("[DBX][executeTabSql:execute-multi:done]", {
traceId,
resultCount: results.length,
@@ -916,6 +914,18 @@ export const useQueryStore = defineStore("query", () => {
tab.queryEditabilityReason = undefined;
}
function notifyConnectionMayBeLost() {
const stuck = tabs.value.filter((t) => t.isExecuting);
if (stuck.length > 0) {
stuck.forEach((t) => {
t.isExecuting = false;
t.isCancelling = false;
t.executionId = undefined;
t.result = toErrorResult(new Error("连接可能已断开,请刷新数据重试"));
});
}
}
async function trimResultCache() {
const inactive = tabs.value.filter((t) => t.id !== activeTabId.value && (t.result || t.results));
if (inactive.length > MAX_CACHED_RESULTS) {
@@ -964,5 +974,6 @@ export const useQueryStore = defineStore("query", () => {
cancelTabExecution,
cancelTabExplain,
reloadEvictedTab,
notifyConnectionMayBeLost,
};
});