Compare commits

...

2 Commits

Author SHA1 Message Date
Xu Liang 5cd5d6685b feat(desktop-client): adapt title bar and drag region for Windows/Linux
Frontend CI / Frontend (push) Has been cancelled
- Add appTitleBarStyle() and appTitleBarOverlay() helpers for cross-platform title bar
- Use titleBarOverlay on Windows/Linux so system caption buttons render correctly
- Disable minimize button on login window (non-resizable, no need to minimize)
- Detect platform at renderer startup and set data-platform on <html> for CSS targeting
- DesktopShell: flip drag bar layout — full-width minus caption area on Win/Linux, traffic-light gap on macOS
- LoginView: move top-bar to Win/Linux caption area (top: 34px), mirror to left on macOS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:02:52 +08:00
Xu Liang b7e096ae37 feat(desktop-client): suppress native menu bar on Windows/Linux and support phone number login
- Add suppressNativeWindowMenu() to hide native menu bar on all windows (Win/Linux only)
- Set autoHideMenuBar: true on BrowserWindow creation as belt-and-suspenders
- Import Menu from electron/main to support menu suppression
- Login form: rename field from email to identifier, support phone number input
- Migrate saved credential key from geo_rankly_saved_email to geo_rankly_saved_identifier with fallback
- Update login error message: "邮箱或密码错误" → "账号或密码错误"
- Remove unset ELECTRON_RUN_AS_NODE from dev script (not needed on Windows)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:02:00 +08:00
6 changed files with 71 additions and 18 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
"type": "module",
"main": "out/main/bootstrap.cjs",
"scripts": {
"dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev --inspect=9229",
"dev": "electron-vite dev --inspect=9229",
"build": "electron-vite build",
"package:mac": "pnpm run build && electron-builder --mac --arm64,universal",
"package:win": "pnpm run build && electron-builder --win --x64",
+40 -5
View File
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { shell } from "electron";
import { BrowserWindow, app, ipcMain, nativeTheme } from "electron/main";
import { BrowserWindow, Menu, app, ipcMain, nativeTheme } from "electron/main";
import type {
BrowserWindow as ElectronBrowserWindow,
IpcMainInvokeEvent,
@@ -132,6 +132,32 @@ const SETTINGS_WINDOW_SIZE: WindowSizePreset = {
resizable: true,
};
const TITLE_BAR_OVERLAY_HEIGHT = 24;
function appTitleBarStyle(): "hidden" | "hiddenInset" {
return process.platform === "darwin" ? "hiddenInset" : "hidden";
}
function appTitleBarOverlay(color: string): { color: string; symbolColor: string; height: number } | undefined {
if (process.platform === "darwin") {
return undefined;
}
return {
color,
symbolColor: nativeTheme.shouldUseDarkColors ? "#f8fafc" : "#111827",
height: TITLE_BAR_OVERLAY_HEIGHT,
};
}
function suppressNativeWindowMenu(window?: ElectronBrowserWindow): void {
if (process.platform === "darwin") {
return;
}
Menu.setApplicationMenu(null);
window?.setMenu(null);
window?.setMenuBarVisibility(false);
}
function windowModePath(): string {
return join(app.getPath("userData"), "window-mode.json");
}
@@ -450,6 +476,7 @@ async function mountRendererWebContents(window: ElectronBrowserWindow, mode: Ren
async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
const initial = WINDOW_SIZES[mode];
const backgroundColor = mode === "login" ? "#eaf2ff" : nativeTheme.shouldUseDarkColors ? "#15191a" : "#f0f2f5";
const window = new BrowserWindow({
width: initial.width,
height: initial.height,
@@ -458,12 +485,15 @@ async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow>
show: false,
title: "省心推",
icon: createDesktopHealthIcon("normal"),
titleBarStyle: "hiddenInset",
titleBarStyle: appTitleBarStyle(),
titleBarOverlay: appTitleBarOverlay(mode === "login" ? "#00000000" : backgroundColor),
trafficLightPosition: { x: 12, y: 14 },
resizable: initial.resizable,
minimizable: mode !== "login",
maximizable: initial.resizable,
fullscreenable: initial.resizable,
backgroundColor: mode === "login" ? "#eaf2ff" : nativeTheme.shouldUseDarkColors ? "#15191a" : "#f0f2f5",
autoHideMenuBar: true,
backgroundColor,
webPreferences: {
preload: preloadPath(),
sandbox: true,
@@ -472,6 +502,7 @@ async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow>
},
});
appWindowModes.set(window, mode);
suppressNativeWindowMenu(window);
if (!initial.resizable) {
window.setMinimumSize(initial.minWidth, initial.minHeight);
@@ -525,6 +556,7 @@ async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow>
async function createSettingsWindow(): Promise<ElectronBrowserWindow> {
const initial = SETTINGS_WINDOW_SIZE;
const backgroundColor = nativeTheme.shouldUseDarkColors ? "#15191a" : "#f2f4f7";
const window = new BrowserWindow({
width: initial.width,
height: initial.height,
@@ -533,13 +565,14 @@ async function createSettingsWindow(): Promise<ElectronBrowserWindow> {
show: false,
title: "省心推设置",
icon: createDesktopHealthIcon("normal"),
titleBarStyle: "hiddenInset",
titleBarStyle: appTitleBarStyle(),
titleBarOverlay: appTitleBarOverlay(backgroundColor),
trafficLightPosition: { x: 14, y: 15 },
resizable: initial.resizable,
maximizable: initial.resizable,
fullscreenable: false,
autoHideMenuBar: true,
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f2f4f7",
backgroundColor,
webPreferences: {
preload: preloadPath(),
sandbox: true,
@@ -548,6 +581,7 @@ async function createSettingsWindow(): Promise<ElectronBrowserWindow> {
},
});
appWindowModes.set(window, "settings");
suppressNativeWindowMenu(window);
window.once("closed", () => {
if (settingsWindow === window) {
@@ -703,6 +737,7 @@ if (!hasSingleInstanceLock) {
app.exit(0);
} else {
app.whenReady().then(async () => {
suppressNativeWindowMenu();
currentWindowMode = readPersistedWindowMode();
initDesktopAppSettings();
registerBridgeHandlers();
+2
View File
@@ -33,6 +33,8 @@ function syncWindowMode(authenticated: boolean): void {
const rendererWindowMode = resolveRendererWindowMode();
document.documentElement.dataset.windowMode = rendererWindowMode;
document.documentElement.dataset.platform =
navigator.platform.toLowerCase().includes("mac") ? "darwin" : "win32";
const usesAutomaticWindowMode = rendererWindowMode === "auto";
const showsSettingsSurface = rendererWindowMode === "settings";
const showsMainSurface = computed(() =>
@@ -170,11 +170,17 @@ function openSettingsWindow() {
.window-drag-bar {
position: fixed;
top: 0;
left: 0;
right: 144px;
height: 24px;
z-index: 1000;
-webkit-app-region: drag;
}
:global(html[data-platform="darwin"]) .window-drag-bar {
left: 80px;
right: 0;
height: 28px;
z-index: 1000;
-webkit-app-region: drag;
}
.app-shell :where(button, a, input, select, textarea, [role="button"], [role="link"]) {
@@ -375,7 +375,7 @@ async function syncRuntimeSessionToMain(next: DesktopSession | null = session.va
function formatLoginError(err: unknown): string {
if (err instanceof ApiClientError) {
const messages: Record<string, string> = {
invalid_credentials: "邮箱或密码错误",
invalid_credentials: "账号或密码错误",
login_rate_limited: "登录请求过于频繁",
login_locked: "登录已被临时保护",
login_in_progress: "登录请求正在处理中",
@@ -7,10 +7,14 @@ const { apiBaseURL, error, pending, setApiBaseURL, login } = useDesktopSession()
const menuOpen = ref(false);
const showServerDialog = ref(false);
const rememberPassword = ref(!!localStorage.getItem("geo_rankly_saved_password"));
const savedIdentifier =
localStorage.getItem("geo_rankly_saved_identifier")
?? localStorage.getItem("geo_rankly_saved_email")
?? "";
const form = reactive({
apiBaseURL: apiBaseURL.value,
email: localStorage.getItem("geo_rankly_saved_email") || "",
identifier: savedIdentifier,
password: localStorage.getItem("geo_rankly_saved_password") || "",
});
@@ -49,16 +53,17 @@ function saveServerSettings() {
async function submitLogin() {
if (rememberPassword.value) {
localStorage.setItem("geo_rankly_saved_email", form.email);
localStorage.setItem("geo_rankly_saved_identifier", form.identifier);
localStorage.removeItem("geo_rankly_saved_email");
localStorage.setItem("geo_rankly_saved_password", form.password);
} else {
localStorage.removeItem("geo_rankly_saved_identifier");
localStorage.removeItem("geo_rankly_saved_email");
localStorage.removeItem("geo_rankly_saved_password");
}
await login({
identifier: form.email,
email: form.email,
identifier: form.identifier,
password: form.password,
});
await window.desktopBridge?.app?.setWindowMode?.("main", { source: "login" });
@@ -111,10 +116,10 @@ onBeforeUnmount(() => {
<form class="login-form" @submit.prevent="submitLogin">
<div class="field">
<input
v-model.trim="form.email"
type="email"
v-model.trim="form.identifier"
type="text"
autocomplete="username"
placeholder="邮箱 / 号"
placeholder="邮箱 / 手机号"
required
/>
</div>
@@ -255,12 +260,17 @@ onBeforeUnmount(() => {
.top-bar {
position: absolute;
top: 10px;
right: 10px;
top: 34px;
right: 14px;
z-index: 10;
-webkit-app-region: no-drag;
}
:global(html[data-platform="darwin"]) .top-bar {
left: 52px;
right: auto;
}
.menu-trigger {
width: 28px;
height: 28px;