feat(desktop): close-to-tray setting

This commit is contained in:
2977094657
2026-01-18 14:43:43 +08:00
parent 78ace41b0e
commit d4828b1a0a
4 changed files with 270 additions and 0 deletions

BIN
desktop/src/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

View File

@@ -2,6 +2,7 @@ const {
app, app,
BrowserWindow, BrowserWindow,
Menu, Menu,
Tray,
ipcMain, ipcMain,
globalShortcut, globalShortcut,
dialog, dialog,
@@ -19,6 +20,10 @@ const BACKEND_HEALTH_URL = `http://${BACKEND_HOST}:${BACKEND_PORT}/api/health`;
let backendProc = null; let backendProc = null;
let backendStdioStream = null; let backendStdioStream = null;
let resolvedDataDir = null; let resolvedDataDir = null;
let mainWindow = null;
let tray = null;
let isQuitting = false;
let desktopSettings = null;
function nowIso() { function nowIso() {
return new Date().toISOString(); return new Date().toISOString();
@@ -109,6 +114,163 @@ function logMain(line) {
} catch {} } catch {}
} }
function getDesktopSettingsPath() {
const dir = getUserDataDir();
if (!dir) return null;
return path.join(dir, "desktop-settings.json");
}
function loadDesktopSettings() {
if (desktopSettings) return desktopSettings;
const defaults = {
// 'tray' (default): closing the window hides it to the system tray.
// 'exit': closing the window quits the app.
closeBehavior: "tray",
};
const p = getDesktopSettingsPath();
if (!p) {
desktopSettings = { ...defaults };
return desktopSettings;
}
try {
if (!fs.existsSync(p)) {
desktopSettings = { ...defaults };
return desktopSettings;
}
const raw = fs.readFileSync(p, { encoding: "utf8" });
const parsed = JSON.parse(raw || "{}");
desktopSettings = { ...defaults, ...(parsed && typeof parsed === "object" ? parsed : {}) };
} catch (err) {
desktopSettings = { ...defaults };
logMain(`[main] failed to load settings: ${err?.message || err}`);
}
return desktopSettings;
}
function persistDesktopSettings() {
const p = getDesktopSettingsPath();
if (!p) return;
if (!desktopSettings) return;
try {
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(desktopSettings, null, 2), { encoding: "utf8" });
} catch (err) {
logMain(`[main] failed to persist settings: ${err?.message || err}`);
}
}
function getCloseBehavior() {
const v = String(loadDesktopSettings()?.closeBehavior || "").trim().toLowerCase();
return v === "exit" ? "exit" : "tray";
}
function setCloseBehavior(next) {
const v = String(next || "").trim().toLowerCase();
loadDesktopSettings();
desktopSettings.closeBehavior = v === "exit" ? "exit" : "tray";
persistDesktopSettings();
return desktopSettings.closeBehavior;
}
function getTrayIconPath() {
// Prefer an icon shipped in `src/` so it works both in dev and packaged (asar) builds.
const shipped = path.join(__dirname, "icon.ico");
try {
if (fs.existsSync(shipped)) return shipped;
} catch {}
// Dev fallback (not available in packaged builds).
const dev = path.resolve(__dirname, "..", "build", "icon.ico");
try {
if (fs.existsSync(dev)) return dev;
} catch {}
return null;
}
function showMainWindow() {
if (!mainWindow) return;
try {
mainWindow.setSkipTaskbar(false);
} catch {}
try {
if (mainWindow.isMinimized()) mainWindow.restore();
} catch {}
try {
mainWindow.show();
} catch {}
try {
mainWindow.focus();
} catch {}
}
function createTray() {
if (tray) return tray;
if (!app.isPackaged) return null;
const iconPath = getTrayIconPath();
if (!iconPath) {
logMain("[main] tray icon not found; disabling tray behavior");
return null;
}
try {
tray = new Tray(iconPath);
} catch (err) {
tray = null;
logMain(`[main] failed to create tray: ${err?.message || err}`);
return null;
}
try {
tray.setToolTip("WeChatDataAnalysis");
} catch {}
try {
tray.setContextMenu(
Menu.buildFromTemplate([
{
label: "显示",
click: () => showMainWindow(),
},
{
label: "退出",
click: () => {
isQuitting = true;
app.quit();
},
},
])
);
} catch {}
try {
tray.on("click", () => showMainWindow());
tray.on("double-click", () => showMainWindow());
} catch {}
return tray;
}
function destroyTray() {
if (!tray) return;
try {
tray.destroy();
} catch {}
tray = null;
}
function ensureTrayForCloseBehavior() {
const behavior = getCloseBehavior();
if (behavior === "tray") createTray();
else destroyTray();
}
function getBackendStdioLogPath(dataDir) { function getBackendStdioLogPath(dataDir) {
return path.join(dataDir, "backend-stdio.log"); return path.join(dataDir, "backend-stdio.log");
} }
@@ -335,6 +497,26 @@ function createMainWindow() {
}, },
}); });
win.on("close", (event) => {
// In packaged builds, we default to "close -> minimize to tray" unless the user opts out.
if (!app.isPackaged) return;
if (isQuitting) return;
if (getCloseBehavior() !== "tray") return;
if (!tray) return;
try {
event.preventDefault();
win.setSkipTaskbar(true);
win.hide();
try {
tray.displayBalloon({
title: "WeChatDataAnalysis",
content: "已最小化到托盘,可从托盘图标再次打开。",
});
} catch {}
} catch {}
});
win.on("closed", () => { win.on("closed", () => {
stopBackend(); stopBackend();
}); });
@@ -409,6 +591,26 @@ function registerWindowIpc() {
return on; return on;
} }
}); });
ipcMain.handle("app:getCloseBehavior", () => {
try {
return getCloseBehavior();
} catch (err) {
logMain(`[main] getCloseBehavior failed: ${err?.message || err}`);
return "tray";
}
});
ipcMain.handle("app:setCloseBehavior", (_event, behavior) => {
try {
const next = setCloseBehavior(behavior);
ensureTrayForCloseBehavior();
return next;
} catch (err) {
logMain(`[main] setCloseBehavior failed: ${err?.message || err}`);
return getCloseBehavior();
}
});
} }
async function main() { async function main() {
@@ -428,6 +630,8 @@ async function main() {
await waitForBackend({ timeoutMs: 30_000 }); await waitForBackend({ timeoutMs: 30_000 });
const win = createMainWindow(); const win = createMainWindow();
mainWindow = win;
ensureTrayForCloseBehavior();
const startUrl = const startUrl =
process.env.ELECTRON_START_URL || process.env.ELECTRON_START_URL ||
@@ -455,6 +659,8 @@ app.on("will-quit", () => {
}); });
app.on("before-quit", () => { app.on("before-quit", () => {
isQuitting = true;
destroyTray();
stopBackend(); stopBackend();
}); });

View File

@@ -8,4 +8,7 @@ contextBridge.exposeInMainWorld("wechatDesktop", {
getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"), getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"),
setAutoLaunch: (enabled) => ipcRenderer.invoke("app:setAutoLaunch", !!enabled), setAutoLaunch: (enabled) => ipcRenderer.invoke("app:setAutoLaunch", !!enabled),
getCloseBehavior: () => ipcRenderer.invoke("app:getCloseBehavior"),
setCloseBehavior: (behavior) => ipcRenderer.invoke("app:setCloseBehavior", String(behavior || "")),
}); });

View File

@@ -1600,6 +1600,25 @@
{{ desktopAutoLaunchError }} {{ desktopAutoLaunchError }}
</div> </div>
<div class="flex items-center justify-between gap-4">
<div class="min-w-0">
<div class="text-sm font-medium text-gray-900">关闭窗口行为</div>
<div class="text-xs text-gray-500">点击关闭按钮时默认最小化到托盘</div>
</div>
<select
class="text-sm px-2 py-1 rounded-md border border-gray-200"
:disabled="desktopCloseBehaviorLoading"
:value="desktopCloseBehavior"
@change="onDesktopCloseBehaviorChange"
>
<option value="tray">最小化到托盘</option>
<option value="exit">直接退出</option>
</select>
</div>
<div v-if="desktopCloseBehaviorError" class="text-xs text-red-600 whitespace-pre-wrap">
{{ desktopCloseBehaviorError }}
</div>
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div class="min-w-0"> <div class="min-w-0">
<div class="text-sm font-medium text-gray-900">启动后自动开启实时获取</div> <div class="text-sm font-medium text-gray-900">启动后自动开启实时获取</div>
@@ -1717,6 +1736,10 @@ const desktopAutoLaunch = ref(false)
const desktopAutoLaunchLoading = ref(false) const desktopAutoLaunchLoading = ref(false)
const desktopAutoLaunchError = ref('') const desktopAutoLaunchError = ref('')
const desktopCloseBehavior = ref('tray') // tray | exit
const desktopCloseBehaviorLoading = ref(false)
const desktopCloseBehaviorError = ref('')
const readLocalBool = (key) => { const readLocalBool = (key) => {
if (!process.client) return false if (!process.client) return false
try { try {
@@ -1768,9 +1791,42 @@ const setDesktopAutoLaunch = async (enabled) => {
} }
} }
const refreshDesktopCloseBehavior = async () => {
if (!process.client || typeof window === 'undefined') return
if (!window.wechatDesktop?.getCloseBehavior) return
desktopCloseBehaviorLoading.value = true
desktopCloseBehaviorError.value = ''
try {
const v = await window.wechatDesktop.getCloseBehavior()
desktopCloseBehavior.value = (String(v || '').toLowerCase() === 'exit') ? 'exit' : 'tray'
} catch (e) {
desktopCloseBehaviorError.value = e?.message || '读取关闭窗口行为失败'
} finally {
desktopCloseBehaviorLoading.value = false
}
}
const setDesktopCloseBehavior = async (behavior) => {
if (!process.client || typeof window === 'undefined') return
if (!window.wechatDesktop?.setCloseBehavior) return
const desired = (String(behavior || '').toLowerCase() === 'exit') ? 'exit' : 'tray'
desktopCloseBehaviorLoading.value = true
desktopCloseBehaviorError.value = ''
try {
const v = await window.wechatDesktop.setCloseBehavior(desired)
desktopCloseBehavior.value = (String(v || '').toLowerCase() === 'exit') ? 'exit' : 'tray'
} catch (e) {
desktopCloseBehaviorError.value = e?.message || '设置关闭窗口行为失败'
await refreshDesktopCloseBehavior()
} finally {
desktopCloseBehaviorLoading.value = false
}
}
const openDesktopSettings = async () => { const openDesktopSettings = async () => {
desktopSettingsOpen.value = true desktopSettingsOpen.value = true
await refreshDesktopAutoLaunch() await refreshDesktopAutoLaunch()
await refreshDesktopCloseBehavior()
} }
const closeDesktopSettings = () => { const closeDesktopSettings = () => {
@@ -1782,6 +1838,11 @@ const onDesktopAutoLaunchToggle = async (ev) => {
await setDesktopAutoLaunch(checked) await setDesktopAutoLaunch(checked)
} }
const onDesktopCloseBehaviorChange = async (ev) => {
const v = String(ev?.target?.value || '').trim()
await setDesktopCloseBehavior(v)
}
const onDesktopAutoRealtimeToggle = async (ev) => { const onDesktopAutoRealtimeToggle = async (ev) => {
const checked = !!ev?.target?.checked const checked = !!ev?.target?.checked
desktopAutoRealtime.value = checked desktopAutoRealtime.value = checked