From e25a3c9d8fdd1a9c185b8fc7168b33871ed67083 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Mon, 17 Aug 2026 23:40:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E6=95=B4=E4=BD=93=E7=BC=A9=E6=94=BE=E4=B8=8E=E5=AD=97=E4=BD=93?= =?UTF-8?q?=E8=B6=85=E5=A4=A7=E6=A1=A3=EF=BC=8C=E4=BF=AE=E5=A4=8D=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E6=BA=A2=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 界面缩放:Electron webContents.setZoomFactor,档位 90%/100%/110%/125%/150% - 主进程新增 set-zoom-factor IPC,启动时从 window-state.json 恢复 zoomFactor - 窗口尺寸保存与退出前均持久化当前 zoom - displaySettings 新增 UiScaleOption + initUiScale,App.vue 启动恢复 - SettingsModal 新增「界面缩放」卡片,实时经 IPC 应用并持久化 - 字体大小新增「超大」档(html font-size 20px),共 5 档 - 修复设置模态框在字体/界面放大后内容超出视口:面板限制 max-h-[calc(100vh-2rem)],内容区 flex-1 overflow-y-auto 滚动 --- debian/changelog | 2 +- electron/main/index.ts | 35 +++++++++++++- src/App.vue | 4 +- src/components/SettingsModal.vue | 78 +++++++++++++++++++++++++++++-- src/global/displaySettings.ts | 79 +++++++++++++++++++++++++++++++- 5 files changed, 191 insertions(+), 7 deletions(-) diff --git a/debian/changelog b/debian/changelog index c79d5081..b57a415d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -spark-store (5.3.0.3-test) UNRELEASED; urgency=medium +spark-store (5.3.0.6-test) UNRELEASED; urgency=medium * Initial release. diff --git a/electron/main/index.ts b/electron/main/index.ts index 6b514806..941d8a1e 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -170,6 +170,18 @@ ipcMain.handle("save-window-bounds", (): boolean => { return true; }); +// 渲染端(设置页)切换界面整体缩放系数,经此实时应用到主窗口。 +// factor 限定在 0.5~3 之间,避免极端值导致界面不可用。 +ipcMain.handle("set-zoom-factor", (_event, factor: unknown): boolean => { + if (typeof factor !== "number" || !Number.isFinite(factor)) return false; + const clamped = Math.min(3, Math.max(0.5, factor)); + if (win && !win.isDestroyed()) { + win.webContents.setZoomFactor(clamped); + return true; + } + return false; +}); + ipcMain.handle("get-app-version", (): string => getAppVersion()); ipcMain.handle("get-system-info", (): { distro: string } => getSystemInfo()); @@ -307,6 +319,8 @@ interface WindowState { x?: number; y?: number; maximized?: boolean; + /** 界面整体缩放系数(Electron webContents.setZoomFactor),默认 1 */ + zoomFactor?: number; } function getWindowStatePath(): string { @@ -375,7 +389,14 @@ function flushSaveBounds(): void { } if (win && !win.isDestroyed()) { const { width, height, x, y } = win.getBounds(); - saveWindowState({ width, height, x, y, maximized: win.isMaximized() }); + saveWindowState({ + width, + height, + x, + y, + maximized: win.isMaximized(), + zoomFactor: win.webContents.getZoomFactor(), + }); } } function scheduleSaveBounds(winInstance: BrowserWindow): void { @@ -389,6 +410,7 @@ function scheduleSaveBounds(winInstance: BrowserWindow): void { x, y, maximized: winInstance.isMaximized(), + zoomFactor: winInstance.webContents.getZoomFactor(), }); }, 400); } @@ -438,6 +460,17 @@ async function createWindow() { }); win = mainWindow; + // 启动即应用持久化的界面缩放系数(默认 1 = 100%)。 + // 必须在 loadURL/loadFile 前设置,使首帧即按目标缩放渲染,避免闪烁。 + const savedZoom = + typeof saved.zoomFactor === "number" && + saved.zoomFactor >= 0.5 && + saved.zoomFactor <= 3 + ? saved.zoomFactor + : 1; + mainWindow.webContents.setZoomFactor(savedZoom); + logger.info({ zoomFactor: savedZoom }, "已应用界面缩放系数"); + // 设计意图(非缺陷,勿改为自动恢复 maximized): // 启动时不自动恢复最大化状态。原因——最大化窗口在多数屏幕上 bounds 会超过 // OVERSIZED_WINDOW_THRESHOLD(1600x900),若强行恢复最大化会导致「启动即全屏、 diff --git a/src/App.vue b/src/App.vue index e9ed680b..d3880c5f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -344,7 +344,7 @@ import ReviewUserProfileModal from "./components/ReviewUserProfileModal.vue"; import WindowTitleBar from "./components/WindowTitleBar.vue"; import SubmitterWindow from "./components/SubmitterWindow.vue"; import { initTagPriorityStrategy } from "./global/tagPriority"; -import { initFontSize } from "./global/displaySettings"; +import { initFontSize, initUiScale } from "./global/displaySettings"; import { FLARUM_BASE_URL, FLARUM_REGISTER_URL, @@ -1055,6 +1055,8 @@ onMounted(async () => { initTagPriorityStrategy(); // 恢复并应用持久化的字体大小档位(仅字号),避免渲染瞬间默认字号闪烁。 initFontSize(); + // 恢复并应用持久化的界面整体缩放(Electron setZoomFactor),与字号正交。 + void initUiScale(); initTheme(); updateCenterStore.bind(); diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index c47219bf..98e1f57a 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -13,7 +13,7 @@ @click.self="closeModal" >
- -
+ +
+ + +
+
+
+ +
+
+

+ 界面缩放 +

+

+ 整体放大或缩小界面(图标、间距、布局等比变化) +

+
+ +
+
+
+
@@ -240,6 +285,11 @@ import { setFontSize, FONT_SIZE_OPTIONS, type FontSizeOption, + getUiScale, + setUiScale, + uiScaleToFactor, + UI_SCALE_OPTIONS, + type UiScaleOption, } from "../global/displaySettings"; const props = defineProps<{ @@ -265,6 +315,9 @@ const strategyOptions: Array<{ value: TagPriorityStrategy; label: string }> = [ // 字体大小档位选项(小 / 标准 / 大 / 特大) const fontSizeOptions = FONT_SIZE_OPTIONS; +// 界面缩放档位选项(90% / 100% / 110% / 125% / 150%) +const uiScaleOptions = UI_SCALE_OPTIONS; + const tagPriorityStrategy = ref("auto"); // 加载标签优先显示策略(从持久化读取) @@ -291,6 +344,24 @@ const selectFontSize = (value: FontSizeOption) => { setFontSize(value); }; +// 界面整体缩放档位(Electron setZoomFactor,连图标/间距/布局等比变化) +const uiScale = ref("100"); + +const loadUiScale = () => { + uiScale.value = getUiScale(); +}; + +// 选择并保存界面缩放档位(立即经 IPC 应用到主窗口) +const selectUiScale = async (value: UiScaleOption) => { + uiScale.value = value; + setUiScale(value); + try { + await window.ipcRenderer.invoke("set-zoom-factor", uiScaleToFactor(value)); + } catch (error) { + console.error("应用界面缩放失败:", error); + } +}; + const settings = ref({ enableUpdateCheck: true, enableCreateDesktop: true, @@ -352,6 +423,7 @@ watch( loadSettings(); loadTagPriority(); loadFontSize(); + loadUiScale(); } }, ); diff --git a/src/global/displaySettings.ts b/src/global/displaySettings.ts index e34bcc71..4fd154ba 100644 --- a/src/global/displaySettings.ts +++ b/src/global/displaySettings.ts @@ -7,7 +7,7 @@ * * 持久化:localStorage key = "spark-store-font-size",存档位枚举字符串。 */ -export type FontSizeOption = "small" | "medium" | "large" | "xlarge"; +export type FontSizeOption = "small" | "medium" | "large" | "xlarge" | "xxlarge"; interface FontSizeMeta { /** 应用到 html 的 font-size(px) */ @@ -23,6 +23,7 @@ export const FONT_SIZE_OPTIONS: Array<{ { value: "medium", label: "标准" }, { value: "large", label: "大" }, { value: "xlarge", label: "特大" }, + { value: "xxlarge", label: "超大" }, ]; // 各档位对应的根字号。medium 保持现状 16px,向上放大、向下略缩。 @@ -31,6 +32,7 @@ const FONT_SIZE_MAP: Record = { medium: { px: 16, label: "标准" }, large: { px: 17, label: "大" }, xlarge: { px: 18, label: "特大" }, + xxlarge: { px: 20, label: "超大" }, }; const FONT_SIZE_STORAGE_KEY = "spark-store-font-size"; @@ -73,3 +75,78 @@ export const setFontSize = (option: FontSizeOption): void => { export const initFontSize = (): void => { applyFontSize(getFontSize()); }; + +/* ------------------------------------------------------------------ * + * 界面整体缩放(Electron webContents.setZoomFactor) + * + * 与「字体大小」正交:字号只改 html font-size(rem 字号类), + * 而界面缩放会连图标、间距、布局一起等比放大/缩小(逻辑分辨率变化)。 + * 实际缩放由主进程 setZoomFactor 执行;渲染端仅负责持久化档位偏好, + * 并在启动时把档位换算的系数通过 IPC 告知主进程。 + * ------------------------------------------------------------------ */ + +export type UiScaleOption = "90" | "100" | "110" | "125" | "150"; + +// 档位 → 缩放系数(与 Electron setZoomFactor 一致,1 = 100%) +const UI_SCALE_MAP: Record = { + "90": 0.9, + "100": 1, + "110": 1.1, + "125": 1.25, + "150": 1.5, +}; + +export const UI_SCALE_OPTIONS: Array<{ + value: UiScaleOption; + label: string; +}> = [ + { value: "90", label: "90%" }, + { value: "100", label: "100%" }, + { value: "110", label: "110%" }, + { value: "125", label: "125%" }, + { value: "150", label: "150%" }, +]; + +const UI_SCALE_STORAGE_KEY = "spark-store-ui-scale"; +const DEFAULT_UI_SCALE: UiScaleOption = "100"; + +const isValidUiScale = (v: unknown): v is UiScaleOption => + typeof v === "string" && v in UI_SCALE_MAP; + +/** 读取持久化的界面缩放档位(无/非法时回退默认「100%」) */ +export const getUiScale = (): UiScaleOption => { + try { + const raw = localStorage.getItem(UI_SCALE_STORAGE_KEY); + if (isValidUiScale(raw)) return raw; + } catch { + // localStorage 不可用时忽略,使用默认 + } + return DEFAULT_UI_SCALE; +}; + +/** 将档位换算为 Electron 缩放系数 */ +export const uiScaleToFactor = (option: UiScaleOption): number => + UI_SCALE_MAP[option] ?? UI_SCALE_MAP[DEFAULT_UI_SCALE]; + +/** 选择并持久化界面缩放档位(不在此直接调用主进程,由调用方经 IPC 应用) */ +export const setUiScale = (option: UiScaleOption): void => { + try { + localStorage.setItem(UI_SCALE_STORAGE_KEY, option); + } catch { + // 持久化失败时仍已在会话内选定,忽略写入错误 + } +}; + +/** + * 应用启动时初始化:从持久化恢复档位,并经 IPC 告知主进程应用缩放。 + * 需在 App.vue onMounted 调用(initFontSize 之后或同级均可), + * 主进程 createWindow 阶段已自有兜底(默认 1),此处确保渲染端偏好生效。 + */ +export const initUiScale = async (): Promise => { + const factor = uiScaleToFactor(getUiScale()); + try { + await window.ipcRenderer.invoke("set-zoom-factor", factor); + } catch { + // 主进程 IPC 不可用时忽略(主进程启动兜底已设为 1) + } +};