From 26c22379ae52c614a4afc733ef02fb1b61a285a9 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 9 Jul 2026 11:13:42 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(window/theme/updater/query-e?= =?UTF-8?q?ditor):=20=E4=BF=AE=E5=A4=8D=20Windows=20=E5=86=B7=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=8D=8A=E7=AA=97=E4=B8=8E=E4=B8=BB=E9=A2=98/?= =?UTF-8?q?=E6=9B=B4=E6=96=B0/=E8=A1=A5=E5=85=A8=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 窗口:最大化恢复加重试与启动宽限,无记忆尺寸时 Windows 居中大窗,冷启动主动 WebView2 settle - 主题:v2 设置中心主题外观改扁平自定义控件,legacy 保持侧栏卡片布局 - 更新:启动发现更新改开设置中心关于页,下载失败支持 assets API 回退并硬化检查链路 - 补全:suggest 列表字段名优先完整显示,元数据空间不足时再省略 - 测试:补充 window/theme/updater/补全相关回归断言 --- frontend/src/App.css | 860 +++++++++++- frontend/src/App.settings-center.test.ts | 27 + frontend/src/App.tool-center.test.ts | 13 +- frontend/src/App.tsx | 1187 ++++++++++++++++- frontend/src/App.ui-version.test.ts | 51 +- .../QueryEditor.external-sql-save.test.tsx | 9 +- .../src/hooks/useAppUpdateManager.test.tsx | 41 + frontend/src/hooks/useAppUpdateManager.ts | 49 +- frontend/src/i18n/catalog.test.ts | 5 + .../src/utils/windowStartupLayout.test.ts | 72 + frontend/src/utils/windowStartupLayout.ts | 66 + frontend/src/utils/windowStateUi.test.ts | 7 + frontend/src/utils/windowStateUi.ts | 17 +- frontend/wailsjs/go/models.ts | 7 +- internal/app/methods_update.go | 278 +++- internal/app/methods_update_test.go | 61 + shared/i18n/de-DE.json | 20 +- shared/i18n/en-US.json | 20 +- shared/i18n/ja-JP.json | 20 +- shared/i18n/ru-RU.json | 20 +- shared/i18n/zh-CN.json | 20 +- shared/i18n/zh-TW.json | 20 +- 22 files changed, 2730 insertions(+), 140 deletions(-) create mode 100644 frontend/src/utils/windowStartupLayout.test.ts create mode 100644 frontend/src/utils/windowStartupLayout.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index 31b0b55b..1e22cf69 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -252,20 +252,28 @@ body[data-theme='light'] ::-webkit-scrollbar-thumb:hover { gap: 6px; } +/* 补全主名称优先完整显示,避免被右侧表名/类型挤成 primary_ent... */ .gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left { - flex: 0 1 auto; + flex: 1 1 auto; min-width: 0; + overflow: hidden; } +/* 表名/类型等元数据让位:始终可见,但空间不足时优先压缩/省略右侧 */ .gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right { - flex: 1 1 auto; - max-width: none; + flex: 0 1 auto; + flex-shrink: 4; + max-width: 48%; min-width: 0; + overflow: hidden; } .gn-query-monaco-stage .monaco-editor .suggest-widget .monaco-list .monaco-list-row.string-label > .contents > .main > .right > .details-label { display: inline !important; margin-left: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } /* Ensure body background matches theme to avoid white flashes, but kept transparent for window composition */ @@ -803,3 +811,849 @@ body[data-theme='dark'] .gonavi-about-update-channel .ant-segmented-thumb { body[data-theme='dark'] .gonavi-about-update-channel .ant-segmented-item-selected .ant-segmented-item-label { color: #f8fafc !important; } + +/* ── Theme settings (v2 only): clean desktop settings ─── */ +body .gonavi-theme-settings { + --gn-settings-fg: #0f172a; + --gn-settings-muted: #64748b; + --gn-settings-line: #e2e8f0; + --gn-settings-track: #e2e8f0; + --gn-settings-chip-bg: #f8fafc; + --gn-settings-chip-fg: #0f172a; + --gn-settings-seg-bg: #f8fafc; + --gn-settings-seg-border: #e2e8f0; + --gn-settings-seg-item: #64748b; + --gn-settings-seg-selected-bg: #fff; + --gn-settings-seg-selected-fg: #0f172a; + --gn-settings-seg-shadow: none; + --gn-settings-accent: #16a34a; + --gn-settings-accent-soft: #dcfce7; + --gn-settings-accent-border: #86efac; + --gn-settings-card-bg: #ffffff; +} + +body[data-theme='dark'] .gonavi-theme-settings { + --gn-settings-fg: rgba(248, 250, 252, 0.94); + --gn-settings-muted: rgba(226, 232, 240, 0.58); + --gn-settings-line: rgba(255, 255, 255, 0.10); + --gn-settings-track: rgba(255, 255, 255, 0.12); + --gn-settings-chip-bg: rgba(255, 255, 255, 0.06); + --gn-settings-chip-fg: rgba(248, 250, 252, 0.88); + --gn-settings-seg-bg: rgba(255, 255, 255, 0.04); + --gn-settings-seg-border: rgba(148, 163, 184, 0.18); + --gn-settings-seg-item: rgba(226, 232, 240, 0.58); + --gn-settings-seg-selected-bg: rgba(30, 41, 59, 0.96); + --gn-settings-seg-selected-fg: #f8fafc; + --gn-settings-accent: #22c55e; + --gn-settings-accent-soft: rgba(34, 197, 94, 0.18); + --gn-settings-accent-border: rgba(74, 222, 128, 0.55); + --gn-settings-card-bg: rgba(255, 255, 255, 0.02); +} + +body .gonavi-theme-settings .gonavi-settings-section { + padding-left: 0; + border-left: none; + margin-bottom: 4px; +} + +body .gonavi-theme-settings .gonavi-settings-section + .gonavi-settings-section { + margin-top: 20px; + padding-top: 0; + border-top: none; +} + +body .gonavi-theme-settings .gonavi-settings-section-title { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 10px; + color: var(--gn-settings-fg); + font-size: 13px; + font-weight: 700; + letter-spacing: 0.01em; +} + +body .gonavi-theme-settings .gonavi-settings-section-hint { + margin: -2px 0 10px; + color: var(--gn-settings-muted); + font-size: 12px; + line-height: 1.5; +} + +body .gonavi-theme-settings .gonavi-settings-row { + display: grid; + grid-template-columns: minmax(120px, 168px) minmax(0, 1fr); + gap: 12px 16px; + align-items: center; + min-height: 40px; + padding: 8px 0; +} + +body .gonavi-theme-settings .gonavi-settings-row.is-stacked { + grid-template-columns: 1fr; + gap: 8px; + align-items: stretch; +} + +body .gonavi-theme-settings .gonavi-settings-row + .gonavi-settings-row { + border-top: 1px solid var(--gn-settings-line); +} + +body .gonavi-theme-settings .gonavi-settings-label { + color: var(--gn-settings-fg); + font-size: 13px; + font-weight: 600; + line-height: 1.35; +} + +body .gonavi-theme-settings .gonavi-settings-label-hint { + margin-top: 3px; + color: var(--gn-settings-muted); + font-size: 12px; + font-weight: 400; + line-height: 1.45; +} + +body .gonavi-theme-settings .gonavi-settings-control { + min-width: 0; +} + +body .gonavi-theme-settings .gonavi-settings-slider-row { + display: flex; + align-items: center; + gap: 12px; + min-width: 0; + padding: 2px 0; + overflow: visible; +} + +body .gonavi-theme-settings .gonavi-settings-slider-row.has-marks { + align-items: flex-start; + padding-bottom: 2px; +} + +body .gonavi-theme-settings .gonavi-settings-slider-row.has-marks .gonavi-settings-slider-value { + margin-top: 6px; +} + +body .gonavi-theme-settings .gonavi-settings-slider-main { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} + +/* + * antd Slider 手柄用 translateX(-50%),在 0%/100% 会各伸出半个圆 + 描边阴影。 + * 外层 padding 需 ≥ 半宽手柄(9) + 描边(2~3),否则仍会缺一截。 + */ +body .gonavi-theme-settings .gonavi-settings-slider-track-wrap { + flex: 0 0 auto; + width: 100%; + min-width: 0; + padding-inline: 14px; + overflow: visible; + box-sizing: border-box; +} + +body .gonavi-theme-settings .gonavi-settings-slider-track-wrap .ant-slider { + width: 100%; + margin: 0 !important; + padding-block: 14px !important; + padding-inline: 0 !important; + overflow: visible !important; +} + +/* 预设刻度:独立一行,严格在进度条下方,不叠在轨道上 */ +body .gonavi-theme-settings .gonavi-settings-slider-presets { + position: relative; + height: 18px; + margin: 0 14px; + box-sizing: border-box; +} + +body .gonavi-theme-settings .gonavi-settings-slider-preset { + position: absolute; + top: 0; + transform: translateX(-50%); + margin: 0; + padding: 0; + border: none; + background: transparent; + color: var(--gn-settings-muted); + font-size: 11px; + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 18px; + white-space: nowrap; + cursor: pointer; +} + +body .gonavi-theme-settings .gonavi-settings-slider-preset:hover:not(:disabled) { + color: var(--gn-settings-fg); +} + +body .gonavi-theme-settings .gonavi-settings-slider-preset.is-active { + color: var(--gn-settings-accent); + font-weight: 700; +} + +body .gonavi-theme-settings .gonavi-settings-slider-preset:disabled { + cursor: not-allowed; + opacity: 0.55; +} + +body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number, +body .gonavi-theme-settings .ant-input-number-group.ant-input-number-group-wrapper.gonavi-settings-slider-value-input, +body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number-group-wrapper { + flex: 0 0 auto !important; + width: 64px !important; + min-width: 56px !important; + max-width: 72px !important; +} + +body .gonavi-theme-settings .gonavi-settings-slider-value-input .ant-input-number, +body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number { + width: 100% !important; + border-radius: 999px 0 0 999px !important; + border-color: color-mix(in srgb, var(--gn-settings-accent) 28%, var(--gn-settings-line)) !important; + background: color-mix(in srgb, var(--gn-settings-accent) 8%, transparent) !important; +} + +/* 无 addon 时整颗胶囊 */ +body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number:not(.ant-input-number-group-wrapper) { + width: 52px !important; + min-width: 48px !important; + max-width: 56px !important; + border-radius: 999px !important; +} + +body .gonavi-theme-settings .gonavi-settings-slider-value-input .ant-input-number-input { + height: 28px !important; + padding: 0 2px 0 6px !important; + color: var(--gn-settings-fg) !important; + font-size: 12px !important; + font-weight: 700 !important; + font-variant-numeric: tabular-nums; + text-align: center !important; +} + +body .gonavi-theme-settings .gonavi-settings-slider-value-input .ant-input-number-group-addon { + min-width: 18px !important; + width: 18px !important; + padding: 0 6px 0 0 !important; + border: 1px solid color-mix(in srgb, var(--gn-settings-accent) 28%, var(--gn-settings-line)) !important; + border-left: none !important; + border-radius: 0 999px 999px 0 !important; + background: color-mix(in srgb, var(--gn-settings-accent) 8%, transparent) !important; + color: var(--gn-settings-muted) !important; + font-size: 11px !important; + font-weight: 700 !important; + line-height: 26px !important; +} + +/* 带 % / px 的整体圆角胶囊外观 */ +body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number-group-wrapper .ant-input-number { + border-right: none !important; +} + +body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number-disabled { + opacity: 0.6; +} + +body .gonavi-theme-settings .ant-slider-rail { + height: 8px !important; + border-radius: 999px !important; + background: var(--gn-settings-track) !important; + box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06); +} + +body .gonavi-theme-settings .ant-slider-track { + height: 8px !important; + border-radius: 999px !important; + background: linear-gradient(90deg, color-mix(in srgb, var(--gn-settings-accent) 78%, #fff) 0%, var(--gn-settings-accent) 100%) !important; +} + +body .gonavi-theme-settings .ant-slider-handle { + width: 16px !important; + height: 16px !important; + inset-block-start: 10px !important; + z-index: 2; +} + +/* 关掉 antd 默认 before 光晕,避免叠成空心环 */ +body .gonavi-theme-settings .ant-slider-handle::before { + display: none !important; +} + +body .gonavi-theme-settings .ant-slider-handle::after { + width: 16px !important; + height: 16px !important; + inset-inline-start: 0 !important; + inset-block-start: 0 !important; + border-radius: 50% !important; + /* 实心白底 + 绿描边,禁用时也不靠整控件 opacity 变空心 */ + background: #fff !important; + box-shadow: + 0 0 0 2px var(--gn-settings-accent), + 0 1px 4px rgba(15, 23, 42, 0.14) !important; + opacity: 1 !important; + transition: box-shadow 0.12s ease !important; +} + +body .gonavi-theme-settings .ant-slider-handle:hover::after, +body .gonavi-theme-settings .ant-slider-handle:focus::after { + box-shadow: + 0 0 0 2px var(--gn-settings-accent), + 0 0 0 4px color-mix(in srgb, var(--gn-settings-accent) 22%, transparent), + 0 1px 4px rgba(15, 23, 42, 0.14) !important; +} + +body .gonavi-theme-settings .ant-slider:hover .ant-slider-track, +body .gonavi-theme-settings .ant-slider:focus-within .ant-slider-track { + background: linear-gradient(90deg, color-mix(in srgb, var(--gn-settings-accent) 78%, #fff) 0%, var(--gn-settings-accent) 100%) !important; +} + +/* 禁用:不要整条 opacity,否则白心被透掉只剩绿环 */ +body .gonavi-theme-settings .ant-slider-disabled { + opacity: 1 !important; +} + +body .gonavi-theme-settings .ant-slider-disabled .ant-slider-rail { + background: color-mix(in srgb, var(--gn-settings-track) 70%, transparent) !important; +} + +body .gonavi-theme-settings .ant-slider-disabled .ant-slider-track { + background: color-mix(in srgb, var(--gn-settings-accent) 35%, var(--gn-settings-track)) !important; +} + +body .gonavi-theme-settings .ant-slider-disabled .ant-slider-handle { + cursor: not-allowed; +} + +body .gonavi-theme-settings .ant-slider-disabled .ant-slider-handle::after { + background: #fff !important; + box-shadow: + 0 0 0 2px color-mix(in srgb, var(--gn-settings-accent) 40%, #94a3b8), + 0 1px 3px rgba(15, 23, 42, 0.10) !important; + opacity: 1 !important; +} + +/* 选项 Segmented:小胶囊;is-compact 不拉满整行 */ +body .gonavi-theme-settings .gonavi-settings-segmented-choice.ant-segmented { + width: 100%; + height: auto !important; + min-height: 0 !important; + padding: 0 !important; + border: none !important; + border-radius: 0 !important; + background: transparent !important; + box-shadow: none !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice.is-compact.ant-segmented { + width: auto !important; + max-width: 100%; + display: inline-flex !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-group { + display: flex !important; + width: 100%; + gap: 6px !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice.is-compact .ant-segmented-group { + width: auto !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item { + flex: 1 1 0; + min-width: 0; + border-radius: 8px !important; + border: 1px solid var(--gn-settings-line) !important; + transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice.is-compact .ant-segmented-item { + flex: 0 0 auto !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item:not(.ant-segmented-item-selected) { + background: transparent !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item:hover:not(.ant-segmented-item-selected) { + background: var(--gn-settings-seg-bg) !important; + border-color: color-mix(in srgb, var(--gn-settings-accent) 28%, var(--gn-settings-line)) !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item-label { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + min-height: 30px !important; + padding: 0 12px !important; + font-size: 12.5px !important; + font-weight: 600 !important; + line-height: 30px !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item:not(.ant-segmented-item-selected) .ant-segmented-item-label { + color: var(--gn-settings-muted) !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-thumb { + display: none !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected, +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] { + border-color: var(--gn-settings-accent) !important; + background: var(--gn-settings-accent-soft) !important; + box-shadow: none !important; +} + +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected .ant-segmented-item-label, +body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] .ant-segmented-item-label { + color: var(--gn-settings-accent) !important; + font-weight: 700 !important; +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected, +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] { + border-color: color-mix(in srgb, var(--gn-settings-accent) 70%, rgba(255, 255, 255, 0.2)) !important; + background: color-mix(in srgb, var(--gn-settings-accent) 22%, transparent) !important; +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected .ant-segmented-item-label, +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] .ant-segmented-item-label { + color: #86efac !important; +} + +/* 界面版本预览卡(紧凑双列) */ +body .gonavi-theme-settings .gonavi-settings-ui-version-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-tile { + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; + padding: 10px; + border: 1px solid var(--gn-settings-line); + border-radius: 12px; + background: var(--gn-settings-card-bg); + color: var(--gn-settings-fg); + text-align: left; + cursor: pointer; + transition: border-color 0.12s ease, box-shadow 0.12s ease; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-tile:hover { + border-color: #cbd5e1; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-tile.is-active { + border-color: var(--gn-settings-accent); + box-shadow: 0 0 0 1px var(--gn-settings-accent-border); + background: var(--gn-settings-card-bg); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview { + position: relative; + height: 58px; + overflow: hidden; + border-radius: 8px; + border: 1px solid var(--gn-settings-line); + background: #f1f5f9; +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview { + background: #0f172a; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-legacy { + display: grid; + grid-template-columns: 34% 1fr; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 { + display: grid; + grid-template-columns: 18px 1fr; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-side { + display: flex; + flex-direction: column; + gap: 5px; + padding: 8px 6px; + border-right: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.72); +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-side { + border-right-color: rgba(255, 255, 255, 0.08); + background: rgba(30, 41, 59, 0.85); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 .uv-side { + align-items: center; + padding: 8px 3px; + background: linear-gradient(180deg, #16a34a 0%, #15803d 100%); + border-right: none; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-dot { + width: 8px; + height: 8px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.18); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 .uv-dot { + width: 7px; + height: 7px; + background: rgba(255, 255, 255, 0.88); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-main { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-bar { + height: 8px; + border-radius: 4px; + background: rgba(15, 23, 42, 0.10); +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-bar { + background: rgba(248, 250, 252, 0.12); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-bar.is-accent { + width: 42%; + background: color-mix(in srgb, var(--gn-settings-accent) 55%, rgba(15, 23, 42, 0.08)); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 .uv-bar.is-accent { + width: 48%; + background: color-mix(in srgb, var(--gn-settings-accent) 70%, #fff); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-cards { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 5px; + flex: 1; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-card { + border-radius: 5px; + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-card { + background: rgba(30, 41, 59, 0.9); + border-color: rgba(255, 255, 255, 0.08); +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-legacy .uv-card { + border-radius: 2px; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-title { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + font-size: 13px; + font-weight: 700; + line-height: 1.3; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-badge { + flex-shrink: 0; + padding: 1px 6px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.06); + color: var(--gn-settings-muted); + font-size: 10px; + font-weight: 700; +} + +body .gonavi-theme-settings .gonavi-settings-ui-version-badge.is-new { + background: rgba(2, 132, 199, 0.12); + color: #0284c7; +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-badge { + background: rgba(255, 255, 255, 0.08); +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-badge.is-new { + background: rgba(56, 189, 248, 0.18); + color: #7dd3fc; +} + +/* 顶部分段:原生 button Tab(不依赖 antd Segmented) */ +body .gonavi-theme-settings .gonavi-settings-tabs { + display: flex; + width: 100%; + border-bottom: 1px solid var(--gn-settings-line); +} + +body .gonavi-theme-settings .gonavi-settings-tab { + flex: 1 1 0; + min-width: 0; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + margin: 0 0 -1px; + padding: 10px 8px 12px; + border: 0; + border-bottom: 2px solid transparent; + background: transparent; + color: var(--gn-settings-muted); + font: inherit; + font-size: 13px; + font-weight: 600; + line-height: 1.2; + cursor: pointer; +} + +body .gonavi-theme-settings .gonavi-settings-tab:hover { + color: var(--gn-settings-fg); +} + +body .gonavi-theme-settings .gonavi-settings-tab.is-active { + color: var(--gn-settings-accent); + border-bottom-color: var(--gn-settings-accent); + font-weight: 700; +} + +body .gonavi-theme-settings .gonavi-settings-tab-icon { + display: inline-flex; + font-size: 14px; +} + +body .gonavi-theme-settings .gonavi-settings-inline-choice-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid var(--gn-settings-line); +} + +body .gonavi-theme-settings .gonavi-settings-pills { + display: inline-flex; + flex-wrap: wrap; + gap: 6px; + justify-content: flex-end; +} + +body .gonavi-theme-settings .gonavi-settings-pill { + margin: 0; + padding: 6px 12px; + border: 1px solid var(--gn-settings-line); + border-radius: 8px; + background: var(--gn-settings-card-bg); + color: var(--gn-settings-muted); + font: inherit; + font-size: 12.5px; + font-weight: 600; + line-height: 1.2; + cursor: pointer; +} + +body .gonavi-theme-settings .gonavi-settings-pill:hover { + border-color: #cbd5e1; + color: var(--gn-settings-fg); +} + +body .gonavi-theme-settings .gonavi-settings-pill.is-active { + border-color: var(--gn-settings-accent); + background: var(--gn-settings-accent-soft); + color: var(--gn-settings-accent); + font-weight: 700; +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-pill.is-active { + color: #86efac; + border-color: color-mix(in srgb, var(--gn-settings-accent) 70%, rgba(255, 255, 255, 0.2)); + background: color-mix(in srgb, var(--gn-settings-accent) 22%, transparent); +} + +body .gonavi-theme-settings .gonavi-settings-inline-meta { + margin-top: 8px; + color: var(--gn-settings-muted); + font-size: 12px; + line-height: 1.5; +} + +/* Beta:弱提示,不要大黄条 */ +body .gonavi-theme-settings .gonavi-settings-beta-chip { + display: inline-flex; + align-items: center; + padding: 0 6px; + border-radius: 999px; + background: color-mix(in srgb, var(--gn-settings-accent) 12%, transparent); + color: var(--gn-settings-accent); + font-size: 10px; + font-weight: 700; + line-height: 18px; +} + +body .gonavi-theme-settings .gonavi-settings-alert { + margin-top: 6px; + padding: 0; + border: none; + background: transparent; + color: var(--gn-settings-muted); + font-size: 11.5px; + line-height: 1.45; +} + +body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-alert { + border: none; + background: transparent; + color: var(--gn-settings-muted); +} + +/* Compact theme-mode previews */ +body .gonavi-theme-settings .gonavi-settings-mode-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +body .gonavi-theme-settings .gonavi-settings-mode-tile { + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; + padding: 10px; + border: 1px solid var(--gn-settings-line); + border-radius: 12px; + background: var(--gn-settings-card-bg); + color: var(--gn-settings-fg); + text-align: left; + cursor: pointer; + transition: border-color 0.12s ease, box-shadow 0.12s ease; +} + +body .gonavi-theme-settings .gonavi-settings-mode-tile:hover { + border-color: #cbd5e1; +} + +body .gonavi-theme-settings .gonavi-settings-mode-tile.is-active { + border-color: var(--gn-settings-accent); + box-shadow: 0 0 0 1px var(--gn-settings-accent-border); + background: var(--gn-settings-card-bg); +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview { + height: 44px; + overflow: hidden; + border-radius: 8px; + border: 1px solid var(--gn-settings-line); + display: grid; +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview.is-system { + grid-template-columns: 1fr 1fr; +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane { + display: grid; + gap: 4px; + align-content: center; + padding: 7px 8px; +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light { + background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%); +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark { + background: linear-gradient(180deg, #0f172a 0%, #111827 100%); +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview-line { + height: 5px; + border-radius: 999px; +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light .gonavi-settings-mode-preview-line:nth-child(1) { + width: 58%; + background: rgba(15, 23, 42, 0.14); +} +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light .gonavi-settings-mode-preview-line:nth-child(2) { + width: 78%; + background: rgba(22, 163, 74, 0.38); +} +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light .gonavi-settings-mode-preview-line:nth-child(3) { + width: 42%; + background: rgba(15, 23, 42, 0.10); +} + +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark .gonavi-settings-mode-preview-line:nth-child(1) { + width: 58%; + background: rgba(248, 250, 252, 0.22); +} +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark .gonavi-settings-mode-preview-line:nth-child(2) { + width: 78%; + background: rgba(74, 222, 128, 0.45); +} +body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark .gonavi-settings-mode-preview-line:nth-child(3) { + width: 42%; + background: rgba(248, 250, 252, 0.12); +} + +body .gonavi-theme-settings .gonavi-settings-mode-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + min-height: 20px; +} + +body .gonavi-theme-settings .gonavi-settings-mode-label { + font-size: 13px; + font-weight: 700; + line-height: 1.3; +} + +body .gonavi-theme-settings .gonavi-settings-mode-check { + color: var(--gn-settings-accent); + font-size: 12px; + flex-shrink: 0; +} + +/* Control-only stacked row: no duplicate label column */ +body .gonavi-theme-settings .gonavi-settings-row.is-control-only { + grid-template-columns: 1fr; + padding-top: 2px; +} + +body .gonavi-theme-settings .gonavi-settings-row.is-control-only .gonavi-settings-label { + display: none; +} diff --git a/frontend/src/App.settings-center.test.ts b/frontend/src/App.settings-center.test.ts index 320d68b8..ef4ce2bd 100644 --- a/frontend/src/App.settings-center.test.ts +++ b/frontend/src/App.settings-center.test.ts @@ -76,6 +76,24 @@ describe('settings center layout', () => { expect(appSource).toContain("t('common.back_to_previous')"); }); + it('uses instant-apply footer actions only on v2 theme settings pane', () => { + expect(appSource).toContain("isV2Ui && activeSettingsCenterPane.key === 'theme' ? ("); + expect(appSource).toContain("t('common.close')"); + expect(appSource).toContain("t('app.theme.instant_apply_hint')"); + }); + + it('gates the new theme layout to v2 and keeps legacy side nav for old UI', () => { + expect(appSource).toContain('renderThemeSettingsContentV2'); + expect(appSource).toContain('renderThemeSettingsContentLegacy'); + expect(appSource).toContain('isV2Ui ? renderThemeSettingsContentV2() : renderThemeSettingsContentLegacy()'); + expect(appSource).toContain("gridTemplateColumns: '180px minmax(0, 1fr)', gap: 16, padding: '12px 0'"); + expect(appSource).toContain('className="gonavi-theme-settings"'); + expect(appSource).toContain('ThemeSettingsSlider'); + expect(appSource).toContain("value: 'workspace'"); + expect(appSource).toContain('gonavi-settings-tabs'); + expect(appSource).toContain('setThemeModalSection(item.value)'); + }); + it('opens theme, AI, and about entries inside settings center detail panes', () => { expect(appSource).toContain("handleOpenSettingsCenterPane('preferences', 'theme')"); expect(appSource).toContain("handleOpenSettingsCenterPane('services', 'ai')"); @@ -94,6 +112,15 @@ describe('settings center layout', () => { expect(appSource).toContain('setActiveSettingsCenterPane(resolveSettingsCenterGroupInitialPane(group.key));'); }); + it('routes silent update discovery to the settings center about pane via bridge', () => { + expect(appSource).toContain('const updateCenterBridgeRef = useRef<{'); + expect(appSource).toContain('updateCenterBridgeRef,'); + expect(appSource).toContain('updateCenterBridgeRef.current = {'); + expect(appSource).toContain("handleOpenSettingsCenterPane('about', 'about-go-navi')"); + expect(appSource).toContain('prepareAboutSurface'); + expect(appSource).toContain('isSettingsAboutPaneOpen'); + }); + it('renders the settings center about page with the reference card layout', () => { expect(appSource).toContain('const renderSettingsCenterAboutPane = () => {'); expect(appSource).toContain('const renderSettingsCenterAboutProjectEntry = ({'); diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index 6ce7f4ad..10febf88 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -68,7 +68,7 @@ describe('tool center menu entries', () => { expect(appSource).toContain("title: t('app.tools.group.config.title')"); expect(appSource).toContain("title: t('app.tools.group.workflow.title')"); expect(appSource).toContain("title: t('app.tools.group.workspace.title')"); - expect(appSource).toContain("toolCenterGroups.find((group) => group.key === activeToolCenterGroupKey)"); + expect(appSource).toContain("filteredToolCenterGroups.find((group) => group.key === activeToolCenterGroupKey)"); expect(appUtilityStylesSource).toContain("const toolCenterModalSplitStyle = useMemo(() => ({"); expect(appUtilityStylesSource).toContain("gridTemplateColumns: '232px minmax(0, 1fr)'"); expect(appUtilityStylesSource).toContain("const toolCenterNavPanelStyle = useMemo(() => ({"); @@ -383,6 +383,17 @@ describe('tool center menu entries', () => { expect(appSource).toContain('该异常不一定表现为 viewport ratio drift'); }); + it('settles Windows cold-start window layout without requiring a taskbar restore', () => { + expect(appSource).toContain("const applyStartupWindowChrome = (attempt: number, mode: 'maximised' | 'fullscreen') => {"); + expect(appSource).toContain("markStartupWindowRestorePending();"); + expect(appSource).toContain("applyStartupWindowChrome(1, 'maximised');"); + expect(appSource).toContain('resolveDefaultStartupWindowBounds(readCurrentVisibleViewport())'); + expect(appSource).toContain("void fixWindowScaleIfNeeded('startup');"); + expect(appSource).toContain('const startupLayoutFixTimers = [220, 1000, 1900].map((delayMs) => ('); + expect(appSource).toContain('if (isStartupWindowRestorePending())'); + expect(appSource).toContain('clearStartupWindowRestorePending();'); + }); + it('captures window state on startup and lifecycle events instead of waiting only for the polling interval', () => { expect(appSource).toContain('const scheduleWindowStateSave = (delayMs = 120) => {'); expect(appSource).toContain('const scheduleWindowBoundsRepair = (delayMs = 80) => {'); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 55b9e8fe..802ae045 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -102,6 +102,12 @@ import { type SecurityUpdateSettingsFocusTarget, } from './utils/securityUpdateRepairFlow'; import { getWindowsScaleFixNudgedWidth, hasWindowsViewportScaleDrift } from './utils/windowsScaleFix'; +import { + clearStartupWindowRestorePending, + isStartupWindowRestorePending, + markStartupWindowRestorePending, + resolveDefaultStartupWindowBounds, +} from './utils/windowStartupLayout'; import { SHORTCUT_ACTION_META, SHORTCUT_ACTION_ORDER, @@ -152,10 +158,200 @@ const MIN_UI_SCALE = 0.8; const MAX_UI_SCALE = 1.25; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 20; +/** 设置页 Slider 底部预设刻度 */ +const UI_SCALE_SLIDER_MARKS: Record = { + 0.8: '80%', + 0.9: '90%', + 1: '100%', + 1.1: '110%', + 1.25: '125%', +}; +const FONT_SIZE_SLIDER_MARKS: Record = { + 12: '12', + 14: '14', + 16: '16', + 18: '18', + 20: '20', +}; +const SIDEBAR_RAIL_SCALE_SLIDER_MARKS: Record = { + 1: '100%', + 1.25: '125%', + 1.5: '150%', + 1.8: '180%', +}; +const OPACITY_SLIDER_MARKS: Record = { + 0.1: '10%', + 0.5: '50%', + 1: '100%', +}; +const BLUR_SLIDER_MARKS: Record = { + 0: '0', + 6: '6', + 12: '12', + 20: '20', +}; +const DATA_TABLE_FONT_SLIDER_MARKS: Record = { + 10: '10', + 12: '12', + 14: '14', + 16: '16', + 18: '18', +}; const DEFAULT_UI_SCALE = 1.0; const DEFAULT_FONT_SIZE = 14; const EMPTY_INSTALLED_FONT_FAMILIES: InstalledFontFamily[] = []; +type ThemeSettingsSliderUnit = 'percent' | 'px' | 'none'; + +type ThemeSettingsSliderProps = { + min: number; + max: number; + step?: number; + value: number; + onChange: (value: number) => void; + disabled?: boolean; + marks?: Record; + /** percent:右侧按百分比输入(内部仍用 0~1 / 0.8~1.25 比例) */ + unit?: ThemeSettingsSliderUnit; +}; + +const clampThemeSliderValue = (value: number, min: number, max: number, step?: number): number => { + let next = Math.min(max, Math.max(min, value)); + if (step && step > 0) { + const steps = Math.round((next - min) / step); + next = min + steps * step; + // 消除浮点误差 + const decimals = String(step).includes('.') ? (String(step).split('.')[1]?.length ?? 0) : 0; + if (decimals > 0) { + next = Number(next.toFixed(decimals)); + } + next = Math.min(max, Math.max(min, next)); + } + return next; +}; + +/** 主题设置页:滑条 + 底部预设 + 可编辑数值 */ +const ThemeSettingsSlider: React.FC = ({ + min, + max, + step, + value, + onChange, + disabled, + marks, + unit = 'none', +}) => { + const isPercent = unit === 'percent'; + const minN = Number(min); + const maxN = Number(max); + const span = maxN - minN || 1; + const stepN = step === undefined || step === null ? undefined : Number(step); + const current = Number(value); + const displayMin = isPercent ? minN * 100 : minN; + const displayMax = isPercent ? maxN * 100 : maxN; + const displayStep = isPercent + ? (stepN !== undefined ? stepN * 100 : 1) + : (stepN !== undefined ? stepN : 1); + const displayValue = Number.isFinite(current) + ? (isPercent ? Number((current * 100).toFixed(4)) : current) + : displayMin; + + const commitDisplayValue = (raw: number | string | null) => { + if (raw === null || raw === undefined || raw === '') { + return; + } + const parsed = typeof raw === 'number' ? raw : Number(String(raw).trim().replace(/%/g, '')); + if (!Number.isFinite(parsed)) { + return; + } + const modelValue = isPercent ? parsed / 100 : parsed; + onChange(clampThemeSliderValue(modelValue, minN, maxN, stepN)); + }; + + const markEntries = marks + ? Object.entries(marks).map(([raw, label]) => ({ + value: Number(raw), + label, + })) + : []; + + return ( +
+
+
+ { + const n = Array.isArray(next) ? next[0] : next; + if (typeof n === 'number' && Number.isFinite(n)) { + onChange(clampThemeSliderValue(n, minN, maxN, stepN)); + } + }} + disabled={disabled} + tooltip={{ open: false }} + /> +
+ {markEntries.length > 0 ? ( +
+ {markEntries.map((mark) => { + const pct = ((mark.value - minN) / span) * 100; + const active = Number.isFinite(current) + ? (stepN && stepN > 0 + ? Math.abs(current - mark.value) <= stepN / 2 + 1e-9 + : Math.abs(current - mark.value) < 1e-6) + : false; + return ( + + ); + })} +
+ ) : null} +
+ { + if (typeof next === 'number') { + commitDisplayValue(next); + } + }} + onBlur={(event) => { + commitDisplayValue(event.target.value); + }} + onPressEnter={(event) => { + commitDisplayValue((event.target as HTMLInputElement).value); + (event.target as HTMLInputElement).blur(); + }} + /> +
+ ); +}; + const createEmptySecurityUpdateStatus = (): SecurityUpdateStatus => ({ overallStatus: 'not_detected', summary: { @@ -864,7 +1060,10 @@ function App() { return false; }; - const applyStartupWindowPreference = (attempt: number) => { + // mode: + // - maximised: 始终最大化(Windows 启动偏好 / 记忆的 maximized / Windows 上的 fullscreen 记忆) + // - fullscreen: 非 Windows 优先真全屏,失败再最大化 + const applyStartupWindowChrome = (attempt: number, mode: 'maximised' | 'fullscreen') => { if (startupWindowTimer !== null) { window.clearTimeout(startupWindowTimer); } @@ -872,24 +1071,21 @@ function App() { if (cancelled) { return; } - if (!useStore.getState().startupFullscreen) { - return; - } void Promise.resolve() .then(async () => { if (await checkStartupPreferenceApplied()) { + clearStartupWindowRestorePending(); return; } - // Windows 使用最大化,避免进入真正全屏后无法通过标题栏交互退出。 - // 其他平台保持全屏优先、最大化兜底。 try { - if (useMaximiseForStartup) { + if (mode === 'maximised') { await WindowMaximise(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); } else { await WindowFullscreen(); await new Promise((resolve) => window.setTimeout(resolve, settleDelayMs)); if (await checkStartupPreferenceApplied()) { + clearStartupWindowRestorePending(); return; } await WindowMaximise(); @@ -900,10 +1096,11 @@ function App() { } if (await checkStartupPreferenceApplied()) { + clearStartupWindowRestorePending(); return; } if (attempt < maxApplyAttempts) { - applyStartupWindowPreference(attempt + 1); + applyStartupWindowChrome(attempt + 1, mode); } }); }, applyRetryDelayMs); @@ -914,22 +1111,44 @@ function App() { const state = useStore.getState(); // startupFullscreen 设置优先 if (state.startupFullscreen) { - applyStartupWindowPreference(1); + markStartupWindowRestorePending(); + applyStartupWindowChrome(1, useMaximiseForStartup ? 'maximised' : 'fullscreen'); return; } // 根据上次保存的窗口状态恢复 const savedState = state.windowState; if (savedState === 'fullscreen') { - applyStartupWindowPreference(1); + // Windows 上记忆的 fullscreen 也走最大化,避免真全屏后标题栏交互困难 + markStartupWindowRestorePending(); + applyStartupWindowChrome(1, useMaximiseForStartup ? 'maximised' : 'fullscreen'); return; } if (savedState === 'maximized') { - try { await WindowMaximise(); } catch (_) {} + // 必须重试:Windows 冷启动 HWND/WebView2 未就绪时单次 Maximise 经常失败, + // 会残留 main.go 默认 1024x768 贴左上角;任务栏恢复后才“突然正常”。 + markStartupWindowRestorePending(); + applyStartupWindowChrome(1, 'maximised'); return; } // 普通窗口:恢复尺寸和位置 const bounds = state.windowBounds; - if (!bounds || bounds.width < 400 || bounds.height < 300) return; + if (!bounds || bounds.width < 400 || bounds.height < 300) { + // 无记忆尺寸时,Windows 默认左上角小窗看起来像“只开了一半”,改为居中可用尺寸 + if (isWindowsPlatform()) { + try { + const nextBounds = resolveDefaultStartupWindowBounds(readCurrentVisibleViewport()); + WindowSetSize(nextBounds.width, nextBounds.height); + WindowSetPosition(nextBounds.x, nextBounds.y); + state.setWindowBounds(nextBounds); + void emitWindowDiagnostic('adjust:startup-default-window-bounds', { + to: nextBounds, + }); + } catch (e) { + console.warn('Failed to apply default Windows startup bounds', e); + } + } + return; + } try { const nextBounds = resolveVisibleStartupWindowBounds(bounds, readCurrentVisibleViewport()); if ( @@ -989,6 +1208,15 @@ function App() { safeWindowRuntimeCall(() => WindowIsMaximised(), false), ]); + // 启动最大化/全屏尚未 settle 时,禁止把状态写回 normal, + // 否则下次冷启动会落到默认 1024x768 左上角(Windows 首次打开“只显示一半”)。 + if (isStartupWindowRestorePending()) { + if (!isFs && !isMax) { + return; + } + clearStartupWindowRestorePending(); + } + // 保存窗口状态 const store = useStore.getState(); const newState = isFs ? 'fullscreen' : (isMax ? 'maximized' : 'normal'); @@ -1043,6 +1271,10 @@ function App() { if (cancelled || !hydrated) { return; } + // 启动最大化 settle 期间不要抢跑普通 bounds 校正 + if (isStartupWindowRestorePending()) { + return; + } try { const [isFs, isMax] = await Promise.all([ safeWindowRuntimeCall(() => WindowIsFullscreen(), false), @@ -1386,6 +1618,15 @@ function App() { void rememberMinimisedState(); checkDevicePixelRatio(); }, 900); + // Windows 冷启动:WebView2 首次布局常只铺满左上角一部分,任务栏恢复才会走 restore 修复。 + // 这里在启动后主动按 startup 原因做几次轻量 settle,避免用户必须双击任务栏。 + // 间隔需大于 fixWindowScaleIfNeeded 的 700ms 节流,确保多次都能真正执行。 + const startupLayoutFixTimers = [220, 1000, 1900].map((delayMs) => ( + window.setTimeout(() => { + if (cancelled) return; + void fixWindowScaleIfNeeded('startup'); + }, delayMs) + )); window.addEventListener('resize', handleWindowResize); window.addEventListener('focus', handleWindowFocus); window.addEventListener('blur', handleWindowBlur); @@ -1400,6 +1641,9 @@ function App() { if (resizeTimer !== null) { window.clearTimeout(resizeTimer); } + for (const timer of startupLayoutFixTimers) { + window.clearTimeout(timer); + } window.clearInterval(pollTimer); window.removeEventListener('resize', handleWindowResize); window.removeEventListener('focus', handleWindowFocus); @@ -1685,6 +1929,12 @@ function App() { useEffect(() => { return installGlobalImeCompositionTracking(window, document); }, []); + // 启动发现更新时打开设置中心「关于」页(由 useAppUpdateManager 通过 bridge 调用) + const updateCenterBridgeRef = useRef<{ + open: () => void; + close: () => void; + isOpen: () => boolean; + } | null>(null); const { aboutDisplayVersion, aboutInfo, @@ -1706,6 +1956,7 @@ function App() { markUpdateProgressDismissed, muteLatestUpdate, openDownloadedUpdateDirectory, + prepareAboutSurface, setIsAboutOpen, showUpdateDownloadProgress, updateChannel, @@ -1713,6 +1964,7 @@ function App() { } = useAppUpdateManager({ runtimeBuildType, t, + updateCenterBridgeRef, }); const [aboutLastCheckedAt, setAboutLastCheckedAt] = useState(''); useEffect(() => { @@ -2262,7 +2514,29 @@ function App() { const [activeSettingsCenterGroupKey, setActiveSettingsCenterGroupKey] = useState('preferences'); const [activeSettingsCenterPane, setActiveSettingsCenterPane] = useState(null); const [isThemeModalOpen, setIsThemeModalOpen] = useState(false); - const [themeModalSection, setThemeModalSection] = useState<'theme' | 'appearance'>('theme'); + type ThemeSettingsSection = 'theme' | 'appearance' | 'workspace'; + const THEME_SETTINGS_SECTION_STORAGE_KEY = 'gonavi.themeSettingsSection'; + const sanitizeThemeSettingsSection = useCallback((value: unknown): ThemeSettingsSection => { + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === 'appearance' || normalized === 'workspace') { + return normalized; + } + return 'theme'; + }, []); + const [themeModalSection, setThemeModalSection] = useState(() => { + try { + return sanitizeThemeSettingsSection(window.localStorage.getItem(THEME_SETTINGS_SECTION_STORAGE_KEY)); + } catch { + return 'theme'; + } + }); + useEffect(() => { + try { + window.localStorage.setItem(THEME_SETTINGS_SECTION_STORAGE_KEY, themeModalSection); + } catch { + // ignore persistence failures + } + }, [themeModalSection]); const [isLinuxCJKFontBannerDismissed, setIsLinuxCJKFontBannerDismissed] = useState(false); const [isAppearanceModalOpen, setIsAppearanceModalOpen] = useState(false); const [isShortcutModalOpen, setIsShortcutModalOpen] = useState(false); @@ -2325,7 +2599,7 @@ function App() { }, [isThemeModalOpen, isThemeSettingsPaneOpen, runtimePlatform, t, themeModalSection]); useEffect(() => { - if ((!isThemeModalOpen && !isThemeSettingsPaneOpen) || themeModalSection !== 'appearance' || tabDisplaySettingsFocusRequest === 0) { + if ((!isThemeModalOpen && !isThemeSettingsPaneOpen) || themeModalSection !== 'workspace' || tabDisplaySettingsFocusRequest === 0) { return; } const timer = window.setTimeout(() => { @@ -2627,6 +2901,32 @@ function App() { setActiveSettingsCenterPane(null); setIsSettingsModalOpen(false); }, []); + const isSettingsAboutPaneOpen = isSettingsModalOpen && activeSettingsCenterPane?.key === 'about-go-navi'; + const isSettingsAboutPaneOpenRef = useRef(false); + useEffect(() => { + isSettingsAboutPaneOpenRef.current = isSettingsAboutPaneOpen; + }, [isSettingsAboutPaneOpen]); + useEffect(() => { + updateCenterBridgeRef.current = { + open: () => { + handleOpenSettingsCenterPane('about', 'about-go-navi'); + }, + close: () => { + setActiveSettingsCenterPane(null); + setIsSettingsModalOpen(false); + }, + isOpen: () => isSettingsAboutPaneOpenRef.current, + }; + return () => { + updateCenterBridgeRef.current = null; + }; + }, [handleOpenSettingsCenterPane]); + useEffect(() => { + if (!isSettingsAboutPaneOpen) { + return; + } + prepareAboutSurface(); + }, [isSettingsAboutPaneOpen, prepareAboutSurface]); const handleOpenToolCenterPane = useCallback((group: ToolCenterGroupKey, key: ToolCenterPaneKey) => { setToolCenterBackGroupKey(group); setActiveToolCenterGroupKey(group); @@ -3163,7 +3463,7 @@ function App() { useEffect(() => { const handleOpenTabDisplaySettingsEvent = () => { setIsSettingsModalOpen(false); - setThemeModalSection('appearance'); + setThemeModalSection('workspace'); setIsThemeModalOpen(true); setTabDisplaySettingsFocusRequest((current) => current + 1); }; @@ -4179,7 +4479,822 @@ function App() { ); - const renderThemeSettingsContent = () => ( + const renderThemeSettingsSection = (title: React.ReactNode, children: React.ReactNode, hint?: React.ReactNode) => ( +
+
{title}
+ {hint ?
{hint}
: null} +
{children}
+
+ ); + + const renderThemeSettingsRow = ({ + label, + hint, + control, + stacked = false, + controlOnly = false, + }: { + label?: React.ReactNode; + hint?: React.ReactNode; + control: React.ReactNode; + stacked?: boolean; + /** 分区标题已说明用途时,只渲染控件,避免标题重复 */ + controlOnly?: boolean; + }) => ( +
+ {!controlOnly ? ( +
+
{label}
+ {hint ?
{hint}
: null} +
+ ) : null} +
{control}
+
+ ); + + const renderThemeModePreview = (preview: 'light' | 'dark' | 'system') => ( +
+ {(preview === 'light' || preview === 'system') ? ( +
+ + + +
+ ) : null} + {(preview === 'dark' || preview === 'system') ? ( +
+ + + +
+ ) : null} +
+ ); + + const renderUiVersionPreview = (version: 'legacy' | 'v2') => ( +
+
+ + + + {version === 'legacy' ? : null} +
+
+
+
+
+
+
+
+
+ ); + + const renderThemeSettingsContentV2 = () => ( +
+
+ {/* 自绘 Tab:避开 antd Segmented CSS-in-JS 压掉选中态 */} +
+ {([ + { value: 'theme' as const, label: t('app.theme.nav.theme.title'), icon: }, + { value: 'appearance' as const, label: t('app.theme.nav.appearance.title'), icon: }, + { value: 'workspace' as const, label: t('app.theme.nav.workspace.title'), icon: }, + ]).map((item) => { + const active = themeModalSection === item.value; + return ( + + ); + })} +
+
+ {t('app.theme.instant_apply_hint')} +
+
+
+ {themeModalSection === 'theme' ? ( +
+ {renderThemeSettingsSection( + t('app.theme.mode_title'), +
+ {([ + { key: 'light' as const, label: t('app.theme.mode.light.label'), preview: 'light' as const }, + { key: 'dark' as const, label: t('app.theme.mode.dark.label'), preview: 'dark' as const }, + { key: 'system' as const, label: t('app.theme.mode.system.label'), preview: 'system' as const }, + ]).map((item) => { + const active = themePreference === item.key; + return ( + + ); + })} +
, + )} + {renderThemeSettingsSection( + + {t('app.theme.ui_version.title')} + + {t('app.theme.ui_version.badge.new')} + + {appearance.uiVersion === 'v2' ? ( + {t('app.theme.ui_version.v2.badge')} + ) : null} + , + <> +
+ {([ + { + key: 'legacy' as const, + label: t('app.theme.ui_version.legacy.label'), + badge: t('app.theme.ui_version.legacy.badge'), + badgeClass: '', + }, + { + key: 'v2' as const, + label: t('app.theme.ui_version.v2.label'), + badge: t('app.theme.ui_version.v2.badge'), + badgeClass: ' is-new', + }, + ]).map((item) => { + const active = (appearance.uiVersion ?? 'legacy') === item.key; + return ( + + ); + })} +
+
+ {t('app.theme.ui_version.platform_hint')} +
+ {appearance.uiVersion === 'v2' ? ( +
+ + + {t('app.theme.ui_version.sidebar_search.title')} + + +
+ {([ + { value: 'command' as const, label: t('app.theme.ui_version.sidebar_search.command') }, + { value: 'filter' as const, label: t('app.theme.ui_version.sidebar_search.filter') }, + ]).map((item) => { + const active = (appearance.v2SidebarSearchMode ?? 'command') === item.value; + return ( + + ); + })} +
+
+ ) : null} + , + )} +
+ ) : themeModalSection === 'appearance' ? ( +
+ {renderThemeSettingsSection( + t('app.theme.nav.appearance.title'), + <> + {renderThemeSettingsRow({ + label: t('app.theme.appearance.ui_scale_title'), + hint: t('app.theme.appearance.ui_scale_hint'), + stacked: true, + control: ( + setUiScale(v)} + /> + ), + })} + {renderThemeSettingsRow({ + label: t('app.theme.appearance.font_size_title'), + stacked: true, + control: ( + setFontSize(v)} + /> + ), + })} + {appearance.uiVersion === 'v2' ? renderThemeSettingsRow({ + label: t('app.theme.appearance.sidebar_rail_scale_title'), + hint: t('app.theme.appearance.sidebar_rail_scale_hint'), + stacked: true, + control: ( + setAppearance({ + v2SidebarRailScale: sanitizeV2SidebarRailScale(value), + })} + /> + ), + }) : null} + , + )} + {renderThemeSettingsSection( + t('app.theme.font_family.title'), + <> +
+
{t('app.theme.font_family.ui_title')}
+ setAppearance({ + customMonoFontFamily: sanitizeFontFamilyInput(value), + })} + onClear={() => setAppearance({ customMonoFontFamily: null })} + options={monoFontOptions.map((option) => ({ + value: option.value, + label: option.label, + }))} + filterOption={filterFontOption} + popupMatchSelectWidth + style={{ width: '100%' }} + optionRender={(option) => renderFontOptionLabel({ + value: String(option.data.value), + label: String(option.data.label), + })} + /> +
+ {fontFamiliesLoadError + ? t('app.theme.font_family.mono_fallback_hint') + : t('app.theme.font_family.mono_hint')} +
+
+ , + )} + {renderThemeSettingsSection( + t('app.theme.appearance.transparency_blur_title'), + <> + {renderThemeSettingsRow({ + label: t('app.theme.appearance.enable_transparency_blur'), + hint: t('app.theme.appearance.enable_transparency_blur_hint'), + control: ( + setAppearance({ enabled: checked })} + /> + ), + })} +
+ {renderThemeSettingsRow({ + label: t('app.theme.appearance.opacity_title'), + stacked: true, + control: ( + setAppearance({ opacity: v })} + /> + ), + })} + {isWindowsPlatform() ? ( +
{t('app.theme.appearance.windows_acrylic_hint')}
+ ) : ( + renderThemeSettingsRow({ + label: t('app.theme.appearance.blur_title'), + hint: t('app.theme.appearance.blur_hint'), + stacked: true, + control: ( + setAppearance({ blur: v })} + /> + ), + }) + )} +
+ , + )} +
+ ) : ( +
+ {renderThemeSettingsSection( + t('app.theme.query_template.title'), + <> +
{t('app.theme.query_template.description')}
+ setAppearance({ newQuerySqlTemplate: event.target.value })} + style={{ fontFamily: 'var(--gn-font-mono)' }} + /> +
+
{t('app.theme.query_template.hint')}
+ +
+ , + )} +
+
{t('app.theme.tab_display.title')}
+
{t('app.theme.tab_display.description')}
+ {renderThemeSettingsRow({ + label: t('app.theme.tab_display.title'), + stacked: true, + control: ( + setTabDisplayLayout(value as TabDisplayLayout)} + /> + ), + })} +
+{tabDisplayElementOrder.map((key) => { + const checked = visibleTabDisplayElementKeys.has(key); + const row = tabDisplaySettings.secondaryElements.includes(key) ? 'secondary' : 'primary'; + const currentRowElements = row === 'secondary' + ? tabDisplaySettings.secondaryElements + : tabDisplaySettings.primaryElements; + const indexInRow = currentRowElements.indexOf(key); + const canMoveUp = checked && indexInRow > 0; + const canMoveDown = checked && indexInRow >= 0 && indexInRow < currentRowElements.length - 1; + const isFocused = focusedTabDisplayElementKey === key; + return ( +
setFocusedTabDisplayElementKey(key)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + setFocusedTabDisplayElementKey(key); + } + }} + style={{ + display: 'grid', + gridTemplateColumns: 'minmax(0, 1fr) auto', + gap: 10, + alignItems: 'center', + padding: '9px 10px', + borderRadius: 10, + border: `1px solid ${isFocused + ? (isV2Ui + ? v2AntPrimaryBorderHoverColor + : (darkMode ? 'rgba(255,214,102,0.54)' : 'rgba(24,144,255,0.54)')) + : (darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(16,24,40,0.08)')}`, + boxShadow: isFocused + ? (isV2Ui + ? `0 0 0 2px ${v2AntControlActiveBg}` + : (darkMode ? '0 0 0 2px rgba(255,214,102,0.14)' : '0 0 0 2px rgba(24,144,255,0.12)')) + : 'none', + background: isFocused + ? (isV2Ui + ? `linear-gradient(90deg, ${v2AntPrimaryBgHoverColor} 0%, rgba(255,255,255,0.045) 100%)` + : (darkMode ? 'linear-gradient(90deg, rgba(255,214,102,0.12) 0%, rgba(255,255,255,0.045) 100%)' : 'linear-gradient(90deg, rgba(24,144,255,0.10) 0%, rgba(255,255,255,0.78) 100%)')) + : checked + ? (darkMode ? 'rgba(255,255,255,0.045)' : 'rgba(255,255,255,0.62)') + : (darkMode ? 'rgba(255,255,255,0.02)' : 'rgba(16,24,40,0.025)'), + cursor: 'pointer', + transition: 'border-color 140ms ease, box-shadow 140ms ease, background 140ms ease', + }} + > +
+ + {checked && indexInRow >= 0 ? indexInRow + 1 : '-'} + + event.stopPropagation()} + onChange={(nextChecked) => updateTabDisplayElementVisibility(key, nextChecked)} + /> +
+
+ {getTabDisplayElementLabel(key)} + {isFocused ? ( + + {t('app.theme.tab_display.badge.current')} + + ) : null} + {checked && tabDisplaySettings.layout === 'double' ? ( + + {row === 'secondary' + ? t('app.theme.tab_display.row.secondary') + : t('app.theme.tab_display.row.primary')} + + ) : null} +
+
{getTabDisplayElementDescription(key)}
+
+
+
+ {tabDisplaySettings.layout === 'double' && checked ? ( + setTabDisplayElementRow(key, value as 'primary' | 'secondary')} + onClick={(event) => event.stopPropagation()} + /> + ) : null} + + +
+
+ ); + })} +
+
+ {t('app.theme.tab_display.preview.prefix')} + {tabDisplaySettings.layout === 'double' ? `${t('app.theme.tab_display.row.primary')} ` : ''} + {tabDisplaySettings.primaryElements.map(getTabDisplayElementLabel).join(' / ') || t('app.theme.tab_display.preview.default_label')} + {tabDisplaySettings.layout === 'double' && tabDisplaySettings.secondaryElements.length > 0 + ? t('app.theme.tab_display.preview.secondary', { + labels: tabDisplaySettings.secondaryElements.map(getTabDisplayElementLabel).join(' / '), + }) + : ''} + {focusedTabDisplayElementKey + ? t('app.theme.tab_display.preview.focused', { + label: getTabDisplayElementLabel(focusedTabDisplayElementKey), + }) + : ''} +
+
+ {renderThemeSettingsSection( + t('app.theme.data_table.title'), + <> + {renderThemeSettingsRow({ + label: t('app.theme.data_table.vertical_borders'), + hint: t('app.theme.data_table.vertical_borders_hint'), + control: ( + setAppearance({ showDataTableVerticalBorders: checked })} + /> + ), + })} + {renderThemeSettingsRow({ + label: t('app.theme.data_table.table_double_click_action'), + hint: t('app.theme.data_table.table_double_click_action_hint'), + stacked: true, + control: ( + setAppearance({ tableDoubleClickAction: value as 'open-data' | 'open-design' })} + /> + ), + })} + {renderThemeSettingsRow({ + label: t('app.theme.data_table.density'), + hint: t('app.theme.data_table.density_hint'), + stacked: true, + control: ( + ({ + ...option, + label: t(`app.theme.data_table.density.${option.value}`), + }))} + value={appearance.dataTableDensity} + onChange={(value) => setAppearance({ dataTableDensity: sanitizeDataTableDensity(value) })} + /> + ), + })} + {renderThemeSettingsRow({ + label: ( + + {t('app.theme.data_table.font_size')} + + + ), + stacked: true, + control: ( + setAppearance({ + dataTableFontSize: sanitizeDataTableFontSize(value), + dataTableFontSizeFollowGlobal: false, + })} + /> + ), + })} + {renderThemeSettingsRow({ + label: ( + + {t('app.theme.data_table.sidebar_tree_font_size')} + + + ), + stacked: true, + control: ( + setAppearance({ + sidebarTreeFontSize: sanitizeSidebarTreeFontSize(value), + sidebarTreeFontSizeFollowGlobal: false, + })} + /> + ), + })} + , + )} + {isMacRuntime ? renderThemeSettingsSection( + t('app.theme.mac_window.title'), + renderThemeSettingsRow({ + label: t('app.theme.mac_window.use_native_controls'), + hint: t('app.theme.mac_window.restart_hint'), + control: ( + setAppearance({ useNativeMacWindowControls: checked })} + /> + ), + }), + t('app.theme.mac_window.use_native_controls_hint'), + ) : null} + {renderThemeSettingsSection( + t('app.theme.startup_window.title'), + renderThemeSettingsRow({ + label: isWindowsRuntime + ? t('app.theme.startup_window.fullscreen_windows') + : t('app.theme.startup_window.fullscreen'), + hint: isWindowsRuntime + ? t('app.theme.startup_window.windows_hint') + : t('app.theme.startup_window.hint'), + control: ( + setStartupFullscreen(checked)} /> + ), + }), + )} +
+ +
+
+ )} +
+
+ ); + + + const renderThemeSettingsContentLegacy = () => (
{t('app.theme.navigation_title')}
@@ -4942,6 +6057,12 @@ function App() {
); + + + const renderThemeSettingsContent = () => ( + isV2Ui ? renderThemeSettingsContentV2() : renderThemeSettingsContentLegacy() + ); + const settingsCenterGroups = [ { key: 'preferences' as const, @@ -5032,12 +6153,15 @@ function App() { : null; const isSettingsCenterContainedScrollPane = activeSettingsCenterPane?.key === 'theme' || activeSettingsCenterPane?.key === 'ai'; + const isV2ThemeSettingsPane = isV2Ui && activeSettingsCenterPane?.key === 'theme'; const settingsCenterDetailBodyStyle: React.CSSProperties = isSettingsCenterContainedScrollPane ? { ...toolCenterDetailBodyStyle, overflowY: 'hidden', - overflowX: 'hidden', - paddingRight: 0, + // v2 主题设置页含 Slider 手柄横向伸出,hidden 会裁切贴边圆点 + overflowX: isV2ThemeSettingsPane ? 'visible' : 'hidden', + paddingRight: isV2ThemeSettingsPane ? 8 : 0, + paddingLeft: isV2ThemeSettingsPane ? 4 : undefined, } : toolCenterDetailBodyStyle; const renderSettingsCenterPane = () => { @@ -6301,6 +7425,15 @@ function App() { > {activeSettingsCenterPane.key === 'about-go-navi' ? ( renderSettingsCenterAboutFooter() + ) : isV2Ui && activeSettingsCenterPane.key === 'theme' ? ( + <> + + + ) : ( <>