mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-04 21:07:45 +08:00
🐛 fix(ui): 修复新版数据视图布局与 AI 面板加载容错
- 修复新版数据视图底部分页、列快速定位与当前页查找的对齐和压缩问题 - 优化窄屏下 AI 面板布局,避免挤压工作区并增加懒加载失败重试兜底 - 补充窗口运行时、AI 面板布局与 UI 回归测试,更新相关样式快照
This commit is contained in:
63
frontend/src/utils/aiPanelLayout.test.ts
Normal file
63
frontend/src/utils/aiPanelLayout.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
DEFAULT_AI_PANEL_WIDTH,
|
||||
resolveOverlayAIPanelWidth,
|
||||
shouldOverlayAIPanel,
|
||||
} from './aiPanelLayout';
|
||||
|
||||
describe('aiPanelLayout', () => {
|
||||
it('keeps the v2 AI panel docked while enough workbench width remains', () => {
|
||||
expect(shouldOverlayAIPanel({
|
||||
isV2Ui: true,
|
||||
viewportWidth: 1440,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minWorkbenchWidth: 320,
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('switches the v2 AI panel to overlay mode when docking would crush the workbench', () => {
|
||||
expect(shouldOverlayAIPanel({
|
||||
isV2Ui: true,
|
||||
viewportWidth: 825,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minWorkbenchWidth: 320,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('also protects the legacy UI from being crushed by the AI panel', () => {
|
||||
expect(shouldOverlayAIPanel({
|
||||
isV2Ui: false,
|
||||
viewportWidth: 825,
|
||||
sidebarWidth: 330,
|
||||
})).toBe(true);
|
||||
});
|
||||
|
||||
it('clamps overlay width to the available workspace instead of overflowing', () => {
|
||||
expect(resolveOverlayAIPanelWidth({
|
||||
viewportWidth: 825,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth: 260,
|
||||
overlayGap: 12,
|
||||
})).toBe(380);
|
||||
|
||||
expect(resolveOverlayAIPanelWidth({
|
||||
viewportWidth: 620,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth: 260,
|
||||
overlayGap: 12,
|
||||
})).toBe(278);
|
||||
|
||||
expect(resolveOverlayAIPanelWidth({
|
||||
viewportWidth: 540,
|
||||
sidebarWidth: 330,
|
||||
panelWidth: DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth: 260,
|
||||
overlayGap: 12,
|
||||
})).toBe(210);
|
||||
});
|
||||
});
|
||||
61
frontend/src/utils/aiPanelLayout.ts
Normal file
61
frontend/src/utils/aiPanelLayout.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export const DEFAULT_AI_PANEL_WIDTH = 380;
|
||||
export const MIN_WORKBENCH_WIDTH_WHEN_AI_DOCKED = 320;
|
||||
export const MIN_AI_PANEL_OVERLAY_WIDTH = 260;
|
||||
export const AI_PANEL_OVERLAY_GAP = 12;
|
||||
|
||||
interface AIPanelLayoutOptions {
|
||||
isV2Ui: boolean;
|
||||
viewportWidth: number;
|
||||
sidebarWidth: number;
|
||||
panelWidth?: number;
|
||||
minWorkbenchWidth?: number;
|
||||
}
|
||||
|
||||
interface AIPanelOverlayWidthOptions {
|
||||
viewportWidth: number;
|
||||
sidebarWidth: number;
|
||||
panelWidth?: number;
|
||||
minOverlayWidth?: number;
|
||||
overlayGap?: number;
|
||||
}
|
||||
|
||||
const normalizePositiveNumber = (value: number, fallback: number) => {
|
||||
const normalized = Number(value);
|
||||
return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
|
||||
};
|
||||
|
||||
export const shouldOverlayAIPanel = ({
|
||||
isV2Ui,
|
||||
viewportWidth,
|
||||
sidebarWidth,
|
||||
panelWidth = DEFAULT_AI_PANEL_WIDTH,
|
||||
minWorkbenchWidth = MIN_WORKBENCH_WIDTH_WHEN_AI_DOCKED,
|
||||
}: AIPanelLayoutOptions): boolean => {
|
||||
const safeViewportWidth = normalizePositiveNumber(viewportWidth, 0);
|
||||
const safeSidebarWidth = Math.max(0, normalizePositiveNumber(sidebarWidth, 0));
|
||||
const safePanelWidth = Math.max(0, normalizePositiveNumber(panelWidth, DEFAULT_AI_PANEL_WIDTH));
|
||||
const safeMinWorkbenchWidth = Math.max(0, normalizePositiveNumber(minWorkbenchWidth, MIN_WORKBENCH_WIDTH_WHEN_AI_DOCKED));
|
||||
const workspaceWidth = Math.max(0, safeViewportWidth - safeSidebarWidth);
|
||||
|
||||
void isV2Ui;
|
||||
return workspaceWidth - safePanelWidth < safeMinWorkbenchWidth;
|
||||
};
|
||||
|
||||
export const resolveOverlayAIPanelWidth = ({
|
||||
viewportWidth,
|
||||
sidebarWidth,
|
||||
panelWidth = DEFAULT_AI_PANEL_WIDTH,
|
||||
minOverlayWidth = MIN_AI_PANEL_OVERLAY_WIDTH,
|
||||
overlayGap = AI_PANEL_OVERLAY_GAP,
|
||||
}: AIPanelOverlayWidthOptions): number => {
|
||||
const safeViewportWidth = normalizePositiveNumber(viewportWidth, panelWidth);
|
||||
const safeSidebarWidth = Math.max(0, normalizePositiveNumber(sidebarWidth, 0));
|
||||
const safePanelWidth = Math.max(0, normalizePositiveNumber(panelWidth, DEFAULT_AI_PANEL_WIDTH));
|
||||
const safeMinOverlayWidth = Math.max(0, normalizePositiveNumber(minOverlayWidth, MIN_AI_PANEL_OVERLAY_WIDTH));
|
||||
const safeOverlayGap = Math.max(0, normalizePositiveNumber(overlayGap, AI_PANEL_OVERLAY_GAP));
|
||||
const workspaceWidth = Math.max(0, safeViewportWidth - safeSidebarWidth);
|
||||
const preferredWidth = Math.min(safePanelWidth, Math.max(0, workspaceWidth - safeOverlayGap));
|
||||
const lowerBound = Math.min(safeMinOverlayWidth, workspaceWidth);
|
||||
|
||||
return Math.max(lowerBound, preferredWidth);
|
||||
};
|
||||
21
frontend/src/utils/wailsRuntime.test.ts
Normal file
21
frontend/src/utils/wailsRuntime.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { safeWindowRuntimeCall } from './wailsRuntime';
|
||||
|
||||
describe('safeWindowRuntimeCall', () => {
|
||||
it('accepts synchronous runtime return values', async () => {
|
||||
await expect(safeWindowRuntimeCall(() => true, false)).resolves.toBe(true);
|
||||
await expect(safeWindowRuntimeCall(() => ({ w: 1280, h: 720 }), null)).resolves.toEqual({ w: 1280, h: 720 });
|
||||
});
|
||||
|
||||
it('keeps supporting Promise based runtime return values', async () => {
|
||||
await expect(safeWindowRuntimeCall(async () => false, true)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('falls back when the runtime call throws or rejects', async () => {
|
||||
await expect(safeWindowRuntimeCall(() => {
|
||||
throw new Error('sync failure');
|
||||
}, false)).resolves.toBe(false);
|
||||
await expect(safeWindowRuntimeCall(async () => Promise.reject(new Error('async failure')), true)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
10
frontend/src/utils/wailsRuntime.ts
Normal file
10
frontend/src/utils/wailsRuntime.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const safeWindowRuntimeCall = async <T>(
|
||||
invoke: () => T | Promise<T>,
|
||||
fallback: T,
|
||||
): Promise<T> => {
|
||||
try {
|
||||
return await Promise.resolve().then(invoke);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user