From 7933b4c315fcd11bb52974ab89cb7723d06b7c62 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 19 Mar 2026 12:26:44 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(window):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=B0=BA=E5=AF=B8=E4=BD=8D=E7=BD=AE=E4=B8=8E?= =?UTF-8?q?=E4=BE=A7=E8=BE=B9=E6=A0=8F=E5=AE=BD=E5=BA=A6=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=E8=AE=B0=E5=BF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 窗口状态:新增 windowState 记录全屏/最大化/普通状态,关闭后重开自动恢复 - 窗口尺寸:普通窗口模式下每2秒自动保存宽高和坐标位置 - 侧边栏宽度:sidebarWidth 从 useState 迁移至 zustand store 持久化 - 状态恢复:启动时根据保存的状态决定全屏/最大化/恢复具体尺寸位置 - 数据校验:新增 sanitizeWindowBounds/sanitizeWindowState/sanitizeSidebarWidth 校验函数 - 兼容处理:startupFullscreen 设置优先级高于自动记忆的窗口状态 - refs #259 --- frontend/src/App.tsx | 84 ++++++++++++++++++++++++++++++++++++++++--- frontend/src/store.ts | 58 ++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 6 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9ebf4e4..41d0676 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { Layout, Button, ConfigProvider, theme, message, Modal, Spin, Slider, Progress, Switch, Input, InputNumber, Select } from 'antd'; import zhCN from 'antd/locale/zh_CN'; import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined } from '@ant-design/icons'; -import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowMaximise, WindowMinimise, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime'; +import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import ConnectionModal from './components/ConnectionModal'; @@ -89,7 +89,8 @@ function App() { const [runtimePlatform, setRuntimePlatform] = useState(''); const [isLinuxRuntime, setIsLinuxRuntime] = useState(false); const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated()); - const [sidebarWidth, setSidebarWidth] = useState(330); + const sidebarWidth = useStore(state => state.sidebarWidth); + const setSidebarWidth = useStore(state => state.setSidebarWidth); const globalProxyInvalidHintShownRef = React.useRef(false); // 同步 macOS 窗口透明度:opacity=1.0 且 blur=0 时关闭 NSVisualEffectView, @@ -285,14 +286,43 @@ function App() { }, applyRetryDelayMs); }; + const restoreWindowState = async () => { + if (cancelled) return; + const state = useStore.getState(); + // startupFullscreen 设置优先 + if (state.startupFullscreen) { + applyStartupWindowPreference(1); + return; + } + // 根据上次保存的窗口状态恢复 + const savedState = state.windowState; + if (savedState === 'fullscreen') { + applyStartupWindowPreference(1); + return; + } + if (savedState === 'maximized') { + try { await WindowMaximise(); } catch (_) {} + return; + } + // 普通窗口:恢复尺寸和位置 + const bounds = state.windowBounds; + if (!bounds || bounds.width < 400 || bounds.height < 300) return; + try { + WindowSetSize(bounds.width, bounds.height); + WindowSetPosition(bounds.x, bounds.y); + } catch (e) { + console.warn('Failed to restore window bounds', e); + } + }; + if (useStore.persist.hasHydrated()) { - applyStartupWindowPreference(1); + void restoreWindowState(); } const unsubscribeHydration = useStore.persist.onFinishHydration(() => { if (cancelled) { return; } - applyStartupWindowPreference(1); + void restoreWindowState(); }); return () => { @@ -304,6 +334,52 @@ function App() { }; }, []); + // 定时保存窗口状态、尺寸与位置 + useEffect(() => { + const SAVE_INTERVAL_MS = 2000; + let lastSaved = ''; + + const saveWindowState = async () => { + try { + const [isFs, isMax] = await Promise.all([ + WindowIsFullscreen().catch(() => false), + WindowIsMaximised().catch(() => false), + ]); + + // 保存窗口状态 + const store = useStore.getState(); + const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal'); + if (store.windowState !== newState) { + store.setWindowState(newState); + } + + // 只在普通窗口模式下保存尺寸和位置 + if (isFs || isMax) return; + + const [size, pos] = await Promise.all([ + WindowGetSize().catch(() => null), + WindowGetPosition().catch(() => null), + ]); + if (!size || !pos) return; + const w = Math.trunc(Number(size.w || 0)); + const h = Math.trunc(Number(size.h || 0)); + const x = Math.trunc(Number(pos.x || 0)); + const y = Math.trunc(Number(pos.y || 0)); + if (w < 400 || h < 300) return; + + const key = `${w},${h},${x},${y}`; + if (key === lastSaved) return; + lastSaved = key; + store.setWindowBounds({ width: w, height: h, x, y }); + } catch (e) { + // 静默忽略 + } + }; + + const timer = window.setInterval(saveWindowState, SAVE_INTERVAL_MS); + return () => window.clearInterval(timer); + }, []); + useEffect(() => { if (!isWindowsPlatform()) { return; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index 172099d..0338859 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -420,6 +420,9 @@ interface AppState { enableColumnOrderMemory: boolean; tableHiddenColumns: Record; enableHiddenColumnMemory: boolean; + windowBounds: { width: number; height: number; x: number; y: number } | null; + windowState: 'normal' | 'fullscreen' | 'maximized'; + sidebarWidth: number; addConnection: (conn: SavedConnection) => void; updateConnection: (conn: SavedConnection) => void; @@ -469,6 +472,9 @@ interface AppState { setTableHiddenColumns: (connectionId: string, dbName: string, tableName: string, hiddenColumns: string[]) => void; setEnableHiddenColumnMemory: (enabled: boolean) => void; clearTableHiddenColumns: (connectionId: string, dbName: string, tableName: string) => void; + setWindowBounds: (bounds: { width: number; height: number; x: number; y: number }) => void; + setWindowState: (state: 'normal' | 'fullscreen' | 'maximized') => void; + setSidebarWidth: (width: number) => void; } const sanitizeSavedQueries = (value: unknown): SavedQuery[] => { @@ -599,6 +605,29 @@ const sanitizeGlobalProxy = (value: unknown): GlobalProxyConfig => { }; }; +const sanitizeWindowState = (value: unknown): 'normal' | 'fullscreen' | 'maximized' => { + if (value === 'fullscreen' || value === 'maximized') return value; + return 'normal'; +}; + +const sanitizeSidebarWidth = (value: unknown): number => { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return 330; + return Math.max(200, Math.min(600, Math.trunc(parsed))); +}; + +const sanitizeWindowBounds = (value: unknown): { width: number; height: number; x: number; y: number } | null => { + if (!value || typeof value !== 'object') return null; + const raw = value as Record; + const width = Number(raw.width); + const height = Number(raw.height); + const x = Number(raw.x); + const y = Number(raw.y); + if (!Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(x) || !Number.isFinite(y)) return null; + if (width < 400 || height < 300) return null; + return { width: Math.trunc(width), height: Math.trunc(height), x: Math.trunc(x), y: Math.trunc(y) }; +}; + const unwrapPersistedAppState = (persistedState: unknown): Record => { if (!persistedState || typeof persistedState !== 'object') { return {}; @@ -635,6 +664,9 @@ export const useStore = create()( enableColumnOrderMemory: true, tableHiddenColumns: {}, enableHiddenColumnMemory: true, + windowBounds: null, + windowState: 'normal' as const, + sidebarWidth: 330, addConnection: (conn) => set((state) => ({ connections: [...state.connections, conn] })), updateConnection: (conn) => set((state) => ({ @@ -875,6 +907,19 @@ export const useStore = create()( }), setEnableHiddenColumnMemory: (enabled) => set({ enableHiddenColumnMemory: !!enabled }), + + setWindowBounds: (bounds) => set({ + windowBounds: { + width: Math.max(400, Math.trunc(bounds.width)), + height: Math.max(300, Math.trunc(bounds.height)), + x: Math.trunc(bounds.x), + y: Math.trunc(bounds.y), + } + }), + + setWindowState: (state) => set({ windowState: state }), + + setSidebarWidth: (width) => set({ sidebarWidth: Math.max(200, Math.min(600, Math.trunc(width))) }), }), { name: 'lite-db-storage', // name of the item in the storage (must be unique) @@ -906,7 +951,10 @@ export const useStore = create()( nextState.enableColumnOrderMemory = state.enableColumnOrderMemory !== false; const safeHidden = sanitizeTableHiddenColumns(state.tableHiddenColumns); nextState.tableHiddenColumns = safeHidden; - nextState.enableHiddenColumnMemory = state.enableHiddenColumnMemory !== false; + nextState.enableHiddenColumnMemory = state.enableHiddenColumnMemory !== false; + nextState.windowBounds = sanitizeWindowBounds(state.windowBounds); + nextState.windowState = sanitizeWindowState(state.windowState); + nextState.sidebarWidth = sanitizeSidebarWidth(state.sidebarWidth); return nextState as AppState; }, merge: (persistedState, currentState) => { @@ -928,6 +976,9 @@ export const useStore = create()( enableColumnOrderMemory: state.enableColumnOrderMemory !== false, tableHiddenColumns: sanitizeTableHiddenColumns(state.tableHiddenColumns), enableHiddenColumnMemory: state.enableHiddenColumnMemory !== false, + windowBounds: sanitizeWindowBounds(state.windowBounds), + windowState: sanitizeWindowState(state.windowState), + sidebarWidth: sanitizeSidebarWidth(state.sidebarWidth), sqlFormatOptions: sanitizeSqlFormatOptions(state.sqlFormatOptions), queryOptions: sanitizeQueryOptions(state.queryOptions), @@ -953,7 +1004,10 @@ export const useStore = create()( tableColumnOrders: state.tableColumnOrders, enableColumnOrderMemory: state.enableColumnOrderMemory, tableHiddenColumns: state.tableHiddenColumns, - enableHiddenColumnMemory: state.enableHiddenColumnMemory + enableHiddenColumnMemory: state.enableHiddenColumnMemory, + windowBounds: state.windowBounds, + windowState: state.windowState, + sidebarWidth: state.sidebarWidth, }), // Don't persist logs } )