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)
+ }
+};