🐛 fix(window): 修正最大化窗口恢复焦点后重复动画

- 收敛 Windows 最大化窗口的激活修复逻辑,避免返回前台时重复 toggle
- 标题栏按钮按窗口状态切换 maximize/restore 图标并立即同步 store
- 补充窗口状态规则测试并更新 issue backlog 记录

Fixes #368
This commit is contained in:
Syngnat
2026-04-17 14:18:38 +08:00
parent 04c4613e4d
commit 7cb46f9f69
4 changed files with 53 additions and 3 deletions

View File

@@ -39,6 +39,7 @@
| #348 | [Bug] sql查询同名字段结果集不会自动添加别名 | Fixed | Pending |
| #349 | [Bug] postgres对于表名大小写敏感且为大写时通过选中表右键新建查询时生成的sql语句没有自动带上引号"" | Fixed | Pending |
| #363 | [Bug] 日期字段无法设置值 | Fixed | Pending |
| #368 | [Bug] 窗口状态问题 | Fixed | Pending |
| #351 | 为什么没有截断和清空表的功能呀? | Fixed | Pending |
## Notes
@@ -151,6 +152,12 @@
- 处理:抽出 `dataGridTemporal` helper统一时间字段的 picker 类型、格式化和保存决策;单元格保存时优先使用 picker 回调里实时拿到的值,再回退到 Form store避免 `date/time/year` 场景把刚选中的值误判为空。
- 验证:新增 `frontend/src/components/dataGridTemporal.test.ts` 回归测试覆盖“picker 已选中日期、Form 仍为空”时仍保存 `YYYY-MM-DD`;并执行 `frontend``npm exec vitest run src/components/dataGridTemporal.test.ts``npm run build`
### #368
- 根因Windows 窗口激活修复逻辑在窗口已最大化时过于激进。应用从后台切回前台时,`activation` 路径只要判定出 viewport drift 就会直接执行两次 `WindowToggleMaximise()`,导致“重新做一遍放大动画”;与此同时,标题栏最大化按钮图标一直写死为 `BorderOutlined`,即使窗口已最大化也不会切成还原态。
- 处理:抽出 `windowStateUi` 规则 helper将“最大化窗口的 scale-fix toggle”收敛为仅在 `ratio-change` 且确实存在 drift 时才执行;激活返回时只广播 `resize`,不再重做最大化动画。并让标题栏按钮根据 `windowState` 动态切换 maximize/restore 图标,同时在标题栏切换动作后立即同步 store 中的窗口状态。
- 验证:新增 `frontend/src/utils/windowStateUi.test.ts`覆盖“activation 不应重 toggle 最大化窗口”和“maximized 状态切换为 restore 图标”两条规则,并执行 `frontend``npm exec vitest run src/utils/windowStateUi.test.ts``npm run build`
### #330
- 根因:查询结果表格已经支持拖拽调整列宽,但 resize handle 没有提供双击自适应逻辑,导致用户只能靠手工拖拽慢慢试宽度。

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { Layout, Button, ConfigProvider, theme, message, Modal, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip } 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, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined, SwitcherOutlined } from '@ant-design/icons';
import { BrowserOpenURL, Environment, EventsOn, Quit, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowToggleMaximise, WindowUnfullscreen } from '../wailsjs/runtime';
import Sidebar from './components/Sidebar';
import TabManager from './components/TabManager';
@@ -68,6 +68,7 @@ import {
isShortcutMatch,
normalizeShortcutCombo,
} from './utils/shortcuts';
import { resolveTitleBarToggleIconKey, shouldToggleMaximisedWindowForScaleFix } from './utils/windowStateUi';
import {
SIDEBAR_UTILITY_ITEM_KEYS,
resolveAIEntryPlacement,
@@ -167,6 +168,9 @@ function App() {
const effectiveUiScale = Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Number(uiScale) || DEFAULT_UI_SCALE));
const effectiveFontSize = Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, Math.round(Number(fontSize) || DEFAULT_FONT_SIZE)));
const tokenFontSize = Math.round(effectiveFontSize * effectiveUiScale);
const titleBarToggleIconKey = resolveTitleBarToggleIconKey(
windowState === 'fullscreen' ? 'fullscreen' : (windowState === 'maximized' ? 'maximized' : 'normal')
);
const tokenFontSizeSM = Math.max(10, Math.round(tokenFontSize * 0.86));
const tokenFontSizeLG = Math.max(tokenFontSize + 1, Math.round(tokenFontSize * 1.14));
const tokenControlHeight = Math.max(24, Math.round(32 * effectiveUiScale));
@@ -633,7 +637,7 @@ function App() {
});
if (isMaximised) {
if (reason !== 'ratio-change' && !hasViewportScaleDrift) {
if (!shouldToggleMaximisedWindowForScaleFix(reason, hasViewportScaleDrift)) {
window.dispatchEvent(new Event('resize'));
lastFixAt = Date.now();
return;
@@ -2148,19 +2152,34 @@ function App() {
}, [securityUpdateRepairSource]);
const handleTitleBarWindowToggle = async () => {
const syncWindowStateFromRuntime = async () => {
try {
const [isFullscreen, isMaximised] = await Promise.all([
WindowIsFullscreen().catch(() => false),
WindowIsMaximised().catch(() => false),
]);
useStore.getState().setWindowState(isFullscreen ? 'fullscreen' : (isMaximised ? 'maximized' : 'normal'));
} catch {
// ignore
}
};
try {
void emitWindowDiagnostic('action:titlebar-toggle:before');
if (await WindowIsFullscreen()) {
await WindowUnfullscreen();
await syncWindowStateFromRuntime();
void emitWindowDiagnostic('action:titlebar-toggle:after-unfullscreen');
return;
}
if (useNativeMacWindowControls && isMacRuntime) {
await WindowFullscreen();
await syncWindowStateFromRuntime();
void emitWindowDiagnostic('action:titlebar-toggle:after-fullscreen');
return;
}
await WindowToggleMaximise();
await syncWindowStateFromRuntime();
void emitWindowDiagnostic('action:titlebar-toggle:after-toggle-maximise');
} catch (_) {
// ignore
@@ -2564,7 +2583,7 @@ function App() {
/>
<Button
type="text"
icon={<BorderOutlined />}
icon={titleBarToggleIconKey === 'restore' ? <SwitcherOutlined /> : <BorderOutlined />}
style={{ height: '100%', borderRadius: 0, width: titleBarButtonWidth }}
onClick={() => { void handleTitleBarWindowToggle(); }}
/>

View File

@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { resolveTitleBarToggleIconKey, shouldToggleMaximisedWindowForScaleFix } from './windowStateUi';
describe('windowStateUi', () => {
it('does not re-toggle a maximized window on activation when focus returns', () => {
expect(shouldToggleMaximisedWindowForScaleFix('activation', true)).toBe(false);
});
it('switches the titlebar toggle icon to restore when the window is maximized', () => {
expect(resolveTitleBarToggleIconKey('maximized')).toBe('restore');
});
});

View File

@@ -0,0 +1,11 @@
export type WindowVisualState = 'normal' | 'maximized' | 'fullscreen';
export type WindowScaleFixReason = 'activation' | 'ratio-change';
export type TitleBarToggleIconKey = 'maximize' | 'restore';
export const shouldToggleMaximisedWindowForScaleFix = (
reason: WindowScaleFixReason,
hasViewportScaleDrift: boolean,
): boolean => reason === 'ratio-change' && hasViewportScaleDrift;
export const resolveTitleBarToggleIconKey = (windowState: WindowVisualState): TitleBarToggleIconKey =>
windowState === 'maximized' ? 'restore' : 'maximize';