feat(desktop-client): add settings window and switch default API to production
- Open a dedicated settings BrowserWindow with general / login / diagnostics / about tabs - Persist openAtLogin and keepRunningInBackground via desktop-settings.json; hide main window instead of quitting when background mode is on - Default API base URL now points to api.shengxintui.com; LoginView and SettingsView share a single constant - Restore wangyihao session cookies on probe and silent refresh, and accept the legacy http://mp.163.com console origin - Drop the standalone diagnostics view in favor of the settings tab Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -277,19 +277,22 @@ const ZOL_LOGIN_REDIRECT_URLS = [
|
|||||||
"https://service.zol.com.cn/user/login",
|
"https://service.zol.com.cn/user/login",
|
||||||
];
|
];
|
||||||
const WANGYI_API_ORIGIN = "https://mp.163.com";
|
const WANGYI_API_ORIGIN = "https://mp.163.com";
|
||||||
const WANGYI_CONSOLE_ORIGIN = "http://mp.163.com";
|
const WANGYI_CONSOLE_ORIGIN = "https://mp.163.com";
|
||||||
|
const WANGYI_LEGACY_CONSOLE_ORIGIN = "http://mp.163.com";
|
||||||
const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html`;
|
const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html`;
|
||||||
const WANGYI_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`;
|
const WANGYI_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`;
|
||||||
const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`;
|
const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`;
|
||||||
const WANGYI_LOGIN_REDIRECT_URLS = [
|
const WANGYI_LOGIN_REDIRECT_URLS = [
|
||||||
`${WANGYI_API_ORIGIN}/login.html`,
|
`${WANGYI_API_ORIGIN}/login.html`,
|
||||||
`${WANGYI_CONSOLE_ORIGIN}/login.html`,
|
`${WANGYI_CONSOLE_ORIGIN}/login.html`,
|
||||||
|
`${WANGYI_LEGACY_CONSOLE_ORIGIN}/login.html`,
|
||||||
"https://reg.163.com/",
|
"https://reg.163.com/",
|
||||||
"https://dl.reg.163.com/",
|
"https://dl.reg.163.com/",
|
||||||
];
|
];
|
||||||
const WANGYI_COOKIE_URLS = [
|
const WANGYI_COOKIE_URLS = [
|
||||||
WANGYI_API_ORIGIN,
|
WANGYI_API_ORIGIN,
|
||||||
WANGYI_CONSOLE_ORIGIN,
|
WANGYI_CONSOLE_ORIGIN,
|
||||||
|
WANGYI_LEGACY_CONSOLE_ORIGIN,
|
||||||
"https://reg.163.com/",
|
"https://reg.163.com/",
|
||||||
"https://dl.reg.163.com/",
|
"https://dl.reg.163.com/",
|
||||||
];
|
];
|
||||||
@@ -2092,6 +2095,10 @@ export async function probePublishAccountSession(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.platform === "wangyihao") {
|
||||||
|
await restoreWangyihaoSessionCookies(account.id, handle.session);
|
||||||
|
}
|
||||||
|
|
||||||
const definition = platformBindingDefinitions[account.platform];
|
const definition = platformBindingDefinitions[account.platform];
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return {
|
return {
|
||||||
@@ -2460,6 +2467,10 @@ export async function silentRefreshPublishAccountSession(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.platform === "wangyihao") {
|
||||||
|
await restoreWangyihaoSessionCookies(account.id, handle.session);
|
||||||
|
}
|
||||||
|
|
||||||
const definition = platformBindingDefinitions[account.platform];
|
const definition = platformBindingDefinitions[account.platform];
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
return false;
|
return false;
|
||||||
@@ -2537,13 +2548,6 @@ export async function inspectPublishAccountLocalSession(
|
|||||||
|
|
||||||
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
|
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
if (account.platform === "wangyihao") {
|
|
||||||
return {
|
|
||||||
availability: "expired",
|
|
||||||
profile: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
|
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
|
||||||
if (consoleReady) {
|
if (consoleReady) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
import { app } from "electron/main";
|
||||||
|
|
||||||
|
export interface DesktopAppSettings {
|
||||||
|
openAtLogin: boolean;
|
||||||
|
keepRunningInBackground: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DesktopAppSettingKey = keyof DesktopAppSettings;
|
||||||
|
|
||||||
|
const DEFAULT_DESKTOP_APP_SETTINGS: DesktopAppSettings = {
|
||||||
|
openAtLogin: false,
|
||||||
|
keepRunningInBackground: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedSettings: DesktopAppSettings = { ...DEFAULT_DESKTOP_APP_SETTINGS };
|
||||||
|
|
||||||
|
function settingsPath(): string {
|
||||||
|
return join(app.getPath("userData"), "desktop-settings.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSettings(input: unknown): DesktopAppSettings {
|
||||||
|
if (!input || typeof input !== "object") {
|
||||||
|
return { ...DEFAULT_DESKTOP_APP_SETTINGS };
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = input as Partial<Record<DesktopAppSettingKey, unknown>>;
|
||||||
|
return {
|
||||||
|
openAtLogin:
|
||||||
|
typeof candidate.openAtLogin === "boolean"
|
||||||
|
? candidate.openAtLogin
|
||||||
|
: DEFAULT_DESKTOP_APP_SETTINGS.openAtLogin,
|
||||||
|
keepRunningInBackground:
|
||||||
|
typeof candidate.keepRunningInBackground === "boolean"
|
||||||
|
? candidate.keepRunningInBackground
|
||||||
|
: DEFAULT_DESKTOP_APP_SETTINGS.keepRunningInBackground,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPersistedSettings(): DesktopAppSettings {
|
||||||
|
try {
|
||||||
|
return normalizeSettings(JSON.parse(readFileSync(settingsPath(), "utf8")));
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_DESKTOP_APP_SETTINGS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistSettings(settings: DesktopAppSettings): void {
|
||||||
|
try {
|
||||||
|
writeFileSync(settingsPath(), JSON.stringify(settings), "utf8");
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[desktop-settings] persist failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSystemOpenAtLogin(fallback: boolean): boolean {
|
||||||
|
try {
|
||||||
|
return app.getLoginItemSettings().openAtLogin;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[desktop-settings] read login item failed", error);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOpenAtLogin(openAtLogin: boolean): boolean {
|
||||||
|
try {
|
||||||
|
app.setLoginItemSettings({
|
||||||
|
openAtLogin,
|
||||||
|
openAsHidden: false,
|
||||||
|
});
|
||||||
|
return readSystemOpenAtLogin(openAtLogin);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[desktop-settings] apply login item failed", error);
|
||||||
|
return openAtLogin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initDesktopAppSettings(): DesktopAppSettings {
|
||||||
|
cachedSettings = readPersistedSettings();
|
||||||
|
cachedSettings = {
|
||||||
|
...cachedSettings,
|
||||||
|
openAtLogin: applyOpenAtLogin(cachedSettings.openAtLogin),
|
||||||
|
};
|
||||||
|
persistSettings(cachedSettings);
|
||||||
|
return getDesktopAppSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDesktopAppSettings(): DesktopAppSettings {
|
||||||
|
cachedSettings = {
|
||||||
|
...cachedSettings,
|
||||||
|
openAtLogin: readSystemOpenAtLogin(cachedSettings.openAtLogin),
|
||||||
|
};
|
||||||
|
return { ...cachedSettings };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDesktopAppSetting(
|
||||||
|
key: DesktopAppSettingKey,
|
||||||
|
value: boolean,
|
||||||
|
): DesktopAppSettings {
|
||||||
|
const next: DesktopAppSettings = {
|
||||||
|
...cachedSettings,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key === "openAtLogin") {
|
||||||
|
next.openAtLogin = applyOpenAtLogin(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedSettings = next;
|
||||||
|
persistSettings(cachedSettings);
|
||||||
|
return getDesktopAppSettings();
|
||||||
|
}
|
||||||
@@ -16,6 +16,12 @@ import {
|
|||||||
startDesktopHealthIndicator,
|
startDesktopHealthIndicator,
|
||||||
syncDesktopHealthIndicator,
|
syncDesktopHealthIndicator,
|
||||||
} from "./app-issue-indicator";
|
} from "./app-issue-indicator";
|
||||||
|
import {
|
||||||
|
getDesktopAppSettings,
|
||||||
|
initDesktopAppSettings,
|
||||||
|
setDesktopAppSetting,
|
||||||
|
type DesktopAppSettingKey,
|
||||||
|
} from "./app-settings";
|
||||||
import { bindPublishAccount, openPublishAccountConsole } from "./account-binder";
|
import { bindPublishAccount, openPublishAccountConsole } from "./account-binder";
|
||||||
import { installObservedGlobalFetch, registerObservedRequestRendererTarget } from "./network-observer";
|
import { installObservedGlobalFetch, registerObservedRequestRendererTarget } from "./network-observer";
|
||||||
import { initProcessMetricsSampler } from "./process-metrics";
|
import { initProcessMetricsSampler } from "./process-metrics";
|
||||||
@@ -67,10 +73,13 @@ process.on("uncaughtException", (error) => {
|
|||||||
|
|
||||||
let mainWindow: ElectronBrowserWindow | null = null;
|
let mainWindow: ElectronBrowserWindow | null = null;
|
||||||
let loginWindow: ElectronBrowserWindow | null = null;
|
let loginWindow: ElectronBrowserWindow | null = null;
|
||||||
|
let settingsWindow: ElectronBrowserWindow | null = null;
|
||||||
let mainRendererContents: ElectronWebContents | null = null;
|
let mainRendererContents: ElectronWebContents | null = null;
|
||||||
let quitReleaseInFlight = false;
|
let quitReleaseInFlight = false;
|
||||||
|
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>();
|
||||||
|
|
||||||
type WindowMode = "login" | "main";
|
type WindowMode = "login" | "main";
|
||||||
|
type RendererWindowMode = WindowMode | "settings";
|
||||||
type WindowModeSource = "auth-state" | "login" | "logout";
|
type WindowModeSource = "auth-state" | "login" | "logout";
|
||||||
|
|
||||||
interface WindowSizePreset {
|
interface WindowSizePreset {
|
||||||
@@ -91,6 +100,14 @@ const WINDOW_SIZES: Record<WindowMode, WindowSizePreset> = {
|
|||||||
main: { width: 1320, height: 860, minWidth: 900, minHeight: 600, resizable: true },
|
main: { width: 1320, height: 860, minWidth: 900, minHeight: 600, resizable: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SETTINGS_WINDOW_SIZE: WindowSizePreset = {
|
||||||
|
width: 920,
|
||||||
|
height: 640,
|
||||||
|
minWidth: 760,
|
||||||
|
minHeight: 520,
|
||||||
|
resizable: true,
|
||||||
|
};
|
||||||
|
|
||||||
function windowModePath(): string {
|
function windowModePath(): string {
|
||||||
return join(app.getPath("userData"), "window-mode.json");
|
return join(app.getPath("userData"), "window-mode.json");
|
||||||
}
|
}
|
||||||
@@ -155,11 +172,23 @@ function currentLoginWindow(): ElectronBrowserWindow | null {
|
|||||||
return loginWindow;
|
return loginWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentSettingsWindow(): ElectronBrowserWindow | null {
|
||||||
|
if (!settingsWindow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (settingsWindow.isDestroyed()) {
|
||||||
|
settingsWindow = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return settingsWindow;
|
||||||
|
}
|
||||||
|
|
||||||
function currentActiveWindow(): ElectronBrowserWindow | null {
|
function currentActiveWindow(): ElectronBrowserWindow | null {
|
||||||
return currentMainWindow() ?? currentLoginWindow();
|
return currentMainWindow() ?? currentLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeWindowAfterIpcReturn(window: ElectronBrowserWindow): void {
|
function closeWindowAfterIpcReturn(window: ElectronBrowserWindow): void {
|
||||||
|
forceCloseWindows.add(window);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!window.isDestroyed()) {
|
if (!window.isDestroyed()) {
|
||||||
window.close();
|
window.close();
|
||||||
@@ -197,6 +226,16 @@ async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
|
|||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureSettingsWindow(): Promise<ElectronBrowserWindow> {
|
||||||
|
const existing = currentSettingsWindow();
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
const window = await createSettingsWindow();
|
||||||
|
settingsWindow = window;
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
|
async function ensureWindow(mode: WindowMode): Promise<ElectronBrowserWindow> {
|
||||||
return mode === "main" ? ensureMainWindow() : ensureLoginWindow();
|
return mode === "main" ? ensureMainWindow() : ensureLoginWindow();
|
||||||
}
|
}
|
||||||
@@ -238,6 +277,16 @@ async function switchWindowMode(mode: WindowMode, _request: WindowModeRequest =
|
|||||||
if (previous && previous !== target && !previous.isDestroyed()) {
|
if (previous && previous !== target && !previous.isDestroyed()) {
|
||||||
closeWindowAfterIpcReturn(previous);
|
closeWindowAfterIpcReturn(previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = currentSettingsWindow();
|
||||||
|
if (mode === "login" && settings) {
|
||||||
|
closeWindowAfterIpcReturn(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSettingsWindow(): Promise<void> {
|
||||||
|
const window = await ensureSettingsWindow();
|
||||||
|
await revealWindow(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rendererURL(): string | null {
|
function rendererURL(): string | null {
|
||||||
@@ -248,7 +297,7 @@ function preloadPath(): string {
|
|||||||
return join(__dirname, "../preload/bridge.cjs");
|
return join(__dirname, "../preload/bridge.cjs");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRenderer(window: ElectronBrowserWindow, mode: WindowMode): Promise<void> {
|
async function loadRenderer(window: ElectronBrowserWindow, mode: RendererWindowMode): Promise<void> {
|
||||||
const devServerURL = rendererURL();
|
const devServerURL = rendererURL();
|
||||||
if (devServerURL) {
|
if (devServerURL) {
|
||||||
const url = new URL(devServerURL);
|
const url = new URL(devServerURL);
|
||||||
@@ -265,7 +314,7 @@ async function loadRenderer(window: ElectronBrowserWindow, mode: WindowMode): Pr
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mountRendererWebContents(window: ElectronBrowserWindow, mode: WindowMode): Promise<void> {
|
async function mountRendererWebContents(window: ElectronBrowserWindow, mode: RendererWindowMode): Promise<void> {
|
||||||
const contents = window.webContents;
|
const contents = window.webContents;
|
||||||
|
|
||||||
contents.setWindowOpenHandler(() => ({ action: "deny" }));
|
contents.setWindowOpenHandler(() => ({ action: "deny" }));
|
||||||
@@ -321,6 +370,18 @@ async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.on("close", (event) => {
|
||||||
|
if (
|
||||||
|
mode === "main"
|
||||||
|
&& !quitReleaseInFlight
|
||||||
|
&& !forceCloseWindows.has(window)
|
||||||
|
&& getDesktopAppSettings().keepRunningInBackground
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.on("minimize", () => {
|
window.on("minimize", () => {
|
||||||
console.warn("[desktop-main] main window minimize event", {
|
console.warn("[desktop-main] main window minimize event", {
|
||||||
at: new Date().toISOString(),
|
at: new Date().toISOString(),
|
||||||
@@ -345,6 +406,41 @@ async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow>
|
|||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createSettingsWindow(): Promise<ElectronBrowserWindow> {
|
||||||
|
const initial = SETTINGS_WINDOW_SIZE;
|
||||||
|
const window = new BrowserWindow({
|
||||||
|
width: initial.width,
|
||||||
|
height: initial.height,
|
||||||
|
minWidth: initial.minWidth,
|
||||||
|
minHeight: initial.minHeight,
|
||||||
|
show: false,
|
||||||
|
title: "GEO Rankly Desktop 设置",
|
||||||
|
icon: createDesktopHealthIcon("normal"),
|
||||||
|
titleBarStyle: "hiddenInset",
|
||||||
|
trafficLightPosition: { x: 14, y: 15 },
|
||||||
|
resizable: initial.resizable,
|
||||||
|
maximizable: initial.resizable,
|
||||||
|
fullscreenable: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f2f4f7",
|
||||||
|
webPreferences: {
|
||||||
|
preload: preloadPath(),
|
||||||
|
sandbox: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
nodeIntegration: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
window.once("closed", () => {
|
||||||
|
if (settingsWindow === window) {
|
||||||
|
settingsWindow = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await mountRendererWebContents(window, "settings");
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
|
||||||
function flattenError(error: unknown): Error {
|
function flattenError(error: unknown): Error {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
const copy = new Error(error.message || "ipc_handler_failed");
|
const copy = new Error(error.message || "ipc_handler_failed");
|
||||||
@@ -450,6 +546,17 @@ function registerBridgeHandlers(): void {
|
|||||||
await shell.openExternal(url);
|
await shell.openExternal(url);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
safeHandle("desktop:open-settings-window", async () => {
|
||||||
|
await openSettingsWindow();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
ipcMain.handle("desktop:get-app-settings", () => getDesktopAppSettings());
|
||||||
|
safeHandle("desktop:set-app-setting", async (_event, key: DesktopAppSettingKey, value: boolean) => {
|
||||||
|
if (key !== "openAtLogin" && key !== "keepRunningInBackground") {
|
||||||
|
throw new Error("unsupported_app_setting");
|
||||||
|
}
|
||||||
|
return setDesktopAppSetting(key, value);
|
||||||
|
});
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"desktop:runtime-session-sync",
|
"desktop:runtime-session-sync",
|
||||||
(_event, session: DesktopRuntimeSessionSyncRequest | null) => {
|
(_event, session: DesktopRuntimeSessionSyncRequest | null) => {
|
||||||
@@ -479,6 +586,7 @@ if (!hasSingleInstanceLock) {
|
|||||||
} else {
|
} else {
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
currentWindowMode = readPersistedWindowMode();
|
currentWindowMode = readPersistedWindowMode();
|
||||||
|
initDesktopAppSettings();
|
||||||
registerBridgeHandlers();
|
registerBridgeHandlers();
|
||||||
await initSessionRegistry();
|
await initSessionRegistry();
|
||||||
initAccountHealth();
|
initAccountHealth();
|
||||||
@@ -508,6 +616,9 @@ if (!hasSingleInstanceLock) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
app.on("window-all-closed", () => {
|
||||||
|
if (getDesktopAppSettings().keepRunningInBackground) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== "darwin") {
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ interface WindowModeRequest {
|
|||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DesktopAppSettings {
|
||||||
|
openAtLogin: boolean;
|
||||||
|
keepRunningInBackground: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request";
|
const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request";
|
||||||
const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response";
|
const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response";
|
||||||
|
|
||||||
@@ -120,6 +125,12 @@ const desktopBridge = {
|
|||||||
ipcRenderer.invoke("desktop:retry-publish-task", taskId) as Promise<CreatePublishJobResponse>,
|
ipcRenderer.invoke("desktop:retry-publish-task", taskId) as Promise<CreatePublishJobResponse>,
|
||||||
openExternalUrl: (url: string) =>
|
openExternalUrl: (url: string) =>
|
||||||
ipcRenderer.invoke("desktop:open-external-url", url) as Promise<null>,
|
ipcRenderer.invoke("desktop:open-external-url", url) as Promise<null>,
|
||||||
|
openSettingsWindow: () =>
|
||||||
|
ipcRenderer.invoke("desktop:open-settings-window") as Promise<null>,
|
||||||
|
getAppSettings: () =>
|
||||||
|
ipcRenderer.invoke("desktop:get-app-settings") as Promise<DesktopAppSettings>,
|
||||||
|
setAppSetting: (key: keyof DesktopAppSettings, value: boolean) =>
|
||||||
|
ipcRenderer.invoke("desktop:set-app-setting", key, value) as Promise<DesktopAppSettings>,
|
||||||
syncRuntimeSession: (session: DesktopRuntimeSessionSyncRequest | null) =>
|
syncRuntimeSession: (session: DesktopRuntimeSessionSyncRequest | null) =>
|
||||||
ipcRenderer.invoke("desktop:runtime-session-sync", session) as Promise<null>,
|
ipcRenderer.invoke("desktop:runtime-session-sync", session) as Promise<null>,
|
||||||
releaseRuntimeSession: (revoke?: boolean) =>
|
releaseRuntimeSession: (revoke?: boolean) =>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { useDesktopSession } from "./composables/useDesktopSession";
|
|||||||
|
|
||||||
const DesktopShell = defineAsyncComponent(() => import("./components/DesktopShell.vue"));
|
const DesktopShell = defineAsyncComponent(() => import("./components/DesktopShell.vue"));
|
||||||
const LoginView = defineAsyncComponent(() => import("./views/LoginView.vue"));
|
const LoginView = defineAsyncComponent(() => import("./views/LoginView.vue"));
|
||||||
|
const SettingsWindow = defineAsyncComponent(() => import("./views/SettingsWindow.vue"));
|
||||||
|
|
||||||
const { isAuthenticated, syncRuntimeSession } = useDesktopSession();
|
const { isAuthenticated, syncRuntimeSession } = useDesktopSession();
|
||||||
type RendererWindowMode = "auto" | "login" | "main";
|
type RendererWindowMode = "auto" | "login" | "main" | "settings";
|
||||||
|
|
||||||
interface WindowModeBridge {
|
interface WindowModeBridge {
|
||||||
setWindowMode?: (
|
setWindowMode?: (
|
||||||
@@ -17,7 +18,7 @@ interface WindowModeBridge {
|
|||||||
|
|
||||||
function resolveRendererWindowMode(): RendererWindowMode {
|
function resolveRendererWindowMode(): RendererWindowMode {
|
||||||
const mode = new URLSearchParams(window.location.search).get("window");
|
const mode = new URLSearchParams(window.location.search).get("window");
|
||||||
return mode === "login" || mode === "main" ? mode : "auto";
|
return mode === "login" || mode === "main" || mode === "settings" ? mode : "auto";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModeBridge(): WindowModeBridge | undefined {
|
function getModeBridge(): WindowModeBridge | undefined {
|
||||||
@@ -31,7 +32,9 @@ function syncWindowMode(authenticated: boolean): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rendererWindowMode = resolveRendererWindowMode();
|
const rendererWindowMode = resolveRendererWindowMode();
|
||||||
|
document.documentElement.dataset.windowMode = rendererWindowMode;
|
||||||
const usesAutomaticWindowMode = rendererWindowMode === "auto";
|
const usesAutomaticWindowMode = rendererWindowMode === "auto";
|
||||||
|
const showsSettingsSurface = rendererWindowMode === "settings";
|
||||||
const showsMainSurface = computed(() =>
|
const showsMainSurface = computed(() =>
|
||||||
rendererWindowMode === "main" || (rendererWindowMode === "auto" && isAuthenticated.value),
|
rendererWindowMode === "main" || (rendererWindowMode === "auto" && isAuthenticated.value),
|
||||||
);
|
);
|
||||||
@@ -63,6 +66,18 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DesktopShell v-if="showsMainSurface" />
|
<SettingsWindow v-if="showsSettingsSurface" />
|
||||||
|
<DesktopShell v-else-if="showsMainSurface" />
|
||||||
<LoginView v-else />
|
<LoginView v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
SendOutlined,
|
SendOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
ToolOutlined,
|
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
|
SettingOutlined,
|
||||||
} from "@ant-design/icons-vue";
|
} from "@ant-design/icons-vue";
|
||||||
|
|
||||||
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
||||||
@@ -61,13 +61,6 @@ const navItems = computed(() => {
|
|||||||
count: accounts.filter((item) => monitoringIDs.has(item.platform)).length,
|
count: accounts.filter((item) => monitoringIDs.has(item.platform)).length,
|
||||||
icon: RobotOutlined,
|
icon: RobotOutlined,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
to: "/diagnostics",
|
|
||||||
title: "诊断",
|
|
||||||
description: "vault、scheduler、transport 与原始快照",
|
|
||||||
count: data?.clients.length ?? 0,
|
|
||||||
icon: ToolOutlined,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,6 +71,10 @@ const liveClientLabel = computed(() => snapshot.value?.clients[0]?.deviceName ??
|
|||||||
const clientLabel = computed(
|
const clientLabel = computed(
|
||||||
() => liveClientLabel.value ?? session.value?.desktopClient?.device_name ?? "当前设备尚未注册 client",
|
() => liveClientLabel.value ?? session.value?.desktopClient?.device_name ?? "当前设备尚未注册 client",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function openSettingsWindow() {
|
||||||
|
void window.desktopBridge.app.openSettingsWindow();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -128,11 +125,18 @@ const clientLabel = computed(
|
|||||||
<span class="device-name-text">{{ clientLabel }}</span>
|
<span class="device-name-text">{{ clientLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip title="退出登录" placement="top">
|
<div class="action-cluster">
|
||||||
<button type="button" class="logout-action-btn" @click="logout">
|
<a-tooltip title="设置" placement="top">
|
||||||
<LogoutOutlined />
|
<button type="button" class="footer-icon-btn" @click="openSettingsWindow">
|
||||||
</button>
|
<SettingOutlined />
|
||||||
</a-tooltip>
|
</button>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip title="退出登录" placement="top">
|
||||||
|
<button type="button" class="footer-icon-btn footer-icon-btn--danger" @click="logout">
|
||||||
|
<LogoutOutlined />
|
||||||
|
</button>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -428,7 +432,13 @@ h1 {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-action-btn {
|
.action-cluster {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icon-btn {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -441,9 +451,22 @@ h1 {
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logout-action-btn:hover {
|
.footer-icon-btn:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icon-btn.router-link-active {
|
||||||
|
background: #e6f4ff;
|
||||||
|
color: #1677ff;
|
||||||
|
border-color: #e6f4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-icon-btn--danger:hover {
|
||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
border-color: #fee2e2;
|
border-color: #fee2e2;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface ResolvedDeviceInfo {
|
|||||||
|
|
||||||
const sessionStorageKey = "geo.desktop.session.v1";
|
const sessionStorageKey = "geo.desktop.session.v1";
|
||||||
const apiBaseURLStorageKey = "geo.desktop.api-base-url";
|
const apiBaseURLStorageKey = "geo.desktop.api-base-url";
|
||||||
|
export const DEFAULT_API_BASE_URL = import.meta.env.VITE_DESKTOP_API_BASE_URL ?? "https://api.shengxintui.com";
|
||||||
|
|
||||||
const session = shallowRef<DesktopSession | null>(readStoredSession());
|
const session = shallowRef<DesktopSession | null>(readStoredSession());
|
||||||
const apiBaseURL = shallowRef(readStoredApiBaseURL());
|
const apiBaseURL = shallowRef(readStoredApiBaseURL());
|
||||||
@@ -75,14 +76,14 @@ function persistSession(next: DesktopSession | null): void {
|
|||||||
|
|
||||||
function readStoredApiBaseURL(): string {
|
function readStoredApiBaseURL(): string {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return "http://localhost:8080";
|
return DEFAULT_API_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.localStorage.getItem(apiBaseURLStorageKey) ?? "http://localhost:8080";
|
return window.localStorage.getItem(apiBaseURLStorageKey) ?? DEFAULT_API_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
function persistApiBaseURL(value: string): void {
|
function persistApiBaseURL(value: string): void {
|
||||||
const trimmed = value.trim() || "http://localhost:8080";
|
const trimmed = value.trim() || DEFAULT_API_BASE_URL;
|
||||||
apiBaseURL.value = trimmed;
|
apiBaseURL.value = trimmed;
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -499,6 +500,12 @@ async function logout(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
|
window.addEventListener("storage", (event) => {
|
||||||
|
if (event.key === apiBaseURLStorageKey) {
|
||||||
|
apiBaseURL.value = event.newValue?.trim() || DEFAULT_API_BASE_URL;
|
||||||
|
void syncRuntimeSessionToMain();
|
||||||
|
}
|
||||||
|
});
|
||||||
void syncStoredDesktopClientDeviceInfo();
|
void syncStoredDesktopClientDeviceInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
@@ -11,6 +11,11 @@ import type { DesktopRuntimeSnapshot } from "./types";
|
|||||||
export {};
|
export {};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
interface DesktopAppSettings {
|
||||||
|
openAtLogin: boolean;
|
||||||
|
keepRunningInBackground: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
desktopBridge: {
|
desktopBridge: {
|
||||||
app: {
|
app: {
|
||||||
@@ -40,6 +45,9 @@ declare global {
|
|||||||
listPublishTasks(params?: ListDesktopPublishTasksParams): Promise<DesktopPublishTaskListResponse>;
|
listPublishTasks(params?: ListDesktopPublishTasksParams): Promise<DesktopPublishTaskListResponse>;
|
||||||
retryPublishTask(taskId: string): Promise<CreatePublishJobResponse>;
|
retryPublishTask(taskId: string): Promise<CreatePublishJobResponse>;
|
||||||
openExternalUrl(url: string): Promise<null>;
|
openExternalUrl(url: string): Promise<null>;
|
||||||
|
openSettingsWindow(): Promise<null>;
|
||||||
|
getAppSettings(): Promise<DesktopAppSettings>;
|
||||||
|
setAppSetting(key: keyof DesktopAppSettings, value: boolean): Promise<DesktopAppSettings>;
|
||||||
syncRuntimeSession(session: DesktopRuntimeSessionSyncRequest | null): Promise<null>;
|
syncRuntimeSession(session: DesktopRuntimeSessionSyncRequest | null): Promise<null>;
|
||||||
releaseRuntimeSession(revoke?: boolean): Promise<null>;
|
releaseRuntimeSession(revoke?: boolean): Promise<null>;
|
||||||
setWindowMode(mode: "login" | "main", request?: {
|
setWindowMode(mode: "login" | "main", request?: {
|
||||||
|
|||||||
@@ -23,11 +23,8 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import("./views/AiPlatformsView.vue"),
|
component: () => import("./views/AiPlatformsView.vue"),
|
||||||
},
|
},
|
||||||
{ path: "/accounts", redirect: "/media-platforms" },
|
{ path: "/accounts", redirect: "/media-platforms" },
|
||||||
{
|
{ path: "/settings", redirect: "/" },
|
||||||
path: "/diagnostics",
|
{ path: "/diagnostics", redirect: "/" },
|
||||||
name: "diagnostics",
|
|
||||||
component: () => import("./views/DiagnosticsView.vue"),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
|
||||||
|
|
||||||
const { snapshot } = useDesktopRuntime();
|
|
||||||
|
|
||||||
const subsystemEntries = computed(() =>
|
|
||||||
Object.entries(snapshot.value?.subsystems ?? {}).map(([key, value]) => ({
|
|
||||||
key,
|
|
||||||
value: JSON.stringify(value, null, 2),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const copied = ref(false);
|
|
||||||
function copyLogs() {
|
|
||||||
if (!snapshot.value) return;
|
|
||||||
const payload = JSON.stringify(snapshot.value, null, 2);
|
|
||||||
navigator.clipboard.writeText(payload).then(() => {
|
|
||||||
copied.value = true;
|
|
||||||
setTimeout(() => {
|
|
||||||
copied.value = false;
|
|
||||||
}, 2500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="page">
|
|
||||||
<header class="hero">
|
|
||||||
<div class="hero-copy">
|
|
||||||
<p class="eyebrow">SYSTEM DIAGNOSTICS</p>
|
|
||||||
<h2>系统诊断</h2>
|
|
||||||
<p class="hero-summary">
|
|
||||||
桌面客户端底层运行环境与依赖项巡检。此页面记录的任务补偿日志、本地路由锁与异常状态,主要用于开发侧进行问题上卷与 Trace 回顾。
|
|
||||||
</p>
|
|
||||||
<div class="actions">
|
|
||||||
<a-button type="primary" :ghost="copied" @click="copyLogs">
|
|
||||||
{{ copied ? "已复制到剪贴板!" : "一键包复制发给研发排查" }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<h3>运行时底层快照日志</h3>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<a-collapse :bordered="false" class="custom-collapse">
|
|
||||||
<a-collapse-panel
|
|
||||||
v-for="entry in subsystemEntries"
|
|
||||||
:key="entry.key"
|
|
||||||
:header="`Subsystem: ${entry.key}`"
|
|
||||||
class="collapse-item"
|
|
||||||
>
|
|
||||||
<pre class="json-code">{{ entry.value }}</pre>
|
|
||||||
</a-collapse-panel>
|
|
||||||
|
|
||||||
<a-collapse-panel
|
|
||||||
key="raw"
|
|
||||||
header="完整 Runtime 原始快照 (Full Engine Snapshot)"
|
|
||||||
class="collapse-item"
|
|
||||||
>
|
|
||||||
<pre class="json-code">{{ JSON.stringify(snapshot, null, 2) }}</pre>
|
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page {
|
|
||||||
display: grid;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-copy {
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #e6edf5;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow,
|
|
||||||
.hero-summary,
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
color: #8c8c8c;
|
|
||||||
font-size: 11px;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a1a;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-summary {
|
|
||||||
margin-top: 12px;
|
|
||||||
max-width: 800px;
|
|
||||||
color: #8c8c8c;
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid #e6edf5;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
padding: 20px 24px;
|
|
||||||
border-bottom: 1px solid #e6edf5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1a1a1a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-body {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-collapse {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-item {
|
|
||||||
border-radius: 8px !important;
|
|
||||||
border: 1px solid #e6edf5 !important;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
background: #fafafb;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-collapse-item:last-child) {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-collapse-header) {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1a1a1a !important;
|
|
||||||
padding: 12px 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.ant-collapse-content) {
|
|
||||||
border-top: 1px dashed #e6edf5 !important;
|
|
||||||
background: #ffffff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-code {
|
|
||||||
margin: 0;
|
|
||||||
padding: 16px;
|
|
||||||
background: #f8fafc;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
color: #334155;
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.5;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
max-height: 500px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||||
import { useDesktopSession } from "../composables/useDesktopSession";
|
import { DEFAULT_API_BASE_URL, useDesktopSession } from "../composables/useDesktopSession";
|
||||||
|
|
||||||
const { apiBaseURL, error, pending, setApiBaseURL, login } = useDesktopSession();
|
const { apiBaseURL, error, pending, setApiBaseURL, login } = useDesktopSession();
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ onBeforeUnmount(() => {
|
|||||||
<input
|
<input
|
||||||
v-model.trim="form.apiBaseURL"
|
v-model.trim="form.apiBaseURL"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="http://localhost:8080"
|
:placeholder="DEFAULT_API_BASE_URL"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -0,0 +1,775 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
|
import {
|
||||||
|
InfoCircleOutlined,
|
||||||
|
LoginOutlined,
|
||||||
|
SlidersOutlined,
|
||||||
|
ToolOutlined,
|
||||||
|
} from "@ant-design/icons-vue";
|
||||||
|
|
||||||
|
import { DEFAULT_API_BASE_URL, useDesktopSession } from "../composables/useDesktopSession";
|
||||||
|
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
||||||
|
|
||||||
|
const { snapshot } = useDesktopRuntime();
|
||||||
|
const { apiBaseURL, setApiBaseURL } = useDesktopSession();
|
||||||
|
|
||||||
|
type SectionKey =
|
||||||
|
| "general"
|
||||||
|
| "notifications"
|
||||||
|
| "storage"
|
||||||
|
| "shortcuts"
|
||||||
|
| "permissions"
|
||||||
|
| "login"
|
||||||
|
| "theme"
|
||||||
|
| "diagnostics"
|
||||||
|
| "about";
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{ key: "general" as const, title: "通用", icon: SlidersOutlined },
|
||||||
|
{ key: "login" as const, title: "登录设置", icon: LoginOutlined },
|
||||||
|
{ key: "diagnostics" as const, title: "诊断", icon: ToolOutlined },
|
||||||
|
{ key: "about" as const, title: "关于GEO", icon: InfoCircleOutlined },
|
||||||
|
];
|
||||||
|
|
||||||
|
const activeSection = ref<SectionKey>("general");
|
||||||
|
const appSettings = ref<DesktopAppSettings>({
|
||||||
|
openAtLogin: false,
|
||||||
|
keepRunningInBackground: true,
|
||||||
|
});
|
||||||
|
const serverBaseURL = ref(apiBaseURL.value);
|
||||||
|
const settingsLoaded = ref(false);
|
||||||
|
const settingsError = ref<string | null>(null);
|
||||||
|
const serverSettingsMessage = ref<string | null>(null);
|
||||||
|
const serverSettingsError = ref<string | null>(null);
|
||||||
|
const savingSetting = ref<keyof DesktopAppSettings | null>(null);
|
||||||
|
|
||||||
|
const activeTitle = computed(() => sections.find((item) => item.key === activeSection.value)?.title ?? "设置");
|
||||||
|
const subsystemEntries = computed(() =>
|
||||||
|
Object.entries(snapshot.value?.subsystems ?? {}).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value: JSON.stringify(value, null, 2),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const clientCount = computed(() => snapshot.value?.clients.length ?? 0);
|
||||||
|
const appVersion = computed(() => snapshot.value?.app.version ?? "0.1.0");
|
||||||
|
const hasServerSettingChanged = computed(() => normalizeServerURLInput(serverBaseURL.value) !== apiBaseURL.value);
|
||||||
|
const generalRows = computed(() => [
|
||||||
|
{
|
||||||
|
key: "openAtLogin" as const,
|
||||||
|
label: "开机自动启动",
|
||||||
|
enabled: appSettings.value.openAtLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "keepRunningInBackground" as const,
|
||||||
|
label: "保持后台运行",
|
||||||
|
enabled: appSettings.value.keepRunningInBackground,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const copied = ref(false);
|
||||||
|
function copyLogs() {
|
||||||
|
if (!snapshot.value) return;
|
||||||
|
const payload = JSON.stringify(snapshot.value, null, 2);
|
||||||
|
navigator.clipboard.writeText(payload).then(() => {
|
||||||
|
copied.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
copied.value = false;
|
||||||
|
}, 2500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openServiceTerms() {
|
||||||
|
void window.desktopBridge.app.openExternalUrl("https://shengxintui.com/terms");
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeServerURLInput(value: string): string {
|
||||||
|
return value.trim() || DEFAULT_API_BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateServerURL(value: string): string | null {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(value);
|
||||||
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||||
|
return "服务器地址必须以 http:// 或 https:// 开头";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return "请输入有效的服务器地址";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveServerSettings() {
|
||||||
|
const next = normalizeServerURLInput(serverBaseURL.value);
|
||||||
|
const validationError = validateServerURL(next);
|
||||||
|
serverSettingsMessage.value = null;
|
||||||
|
serverSettingsError.value = validationError;
|
||||||
|
if (validationError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setApiBaseURL(next);
|
||||||
|
serverBaseURL.value = apiBaseURL.value;
|
||||||
|
serverSettingsMessage.value = "已保存,后续登录和桌面运行会使用此地址";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetServerSettings() {
|
||||||
|
serverBaseURL.value = DEFAULT_API_BASE_URL;
|
||||||
|
saveServerSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAppSettings() {
|
||||||
|
settingsError.value = null;
|
||||||
|
try {
|
||||||
|
appSettings.value = await window.desktopBridge.app.getAppSettings();
|
||||||
|
} catch (error) {
|
||||||
|
settingsError.value = error instanceof Error ? error.message : "设置读取失败";
|
||||||
|
} finally {
|
||||||
|
settingsLoaded.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleAppSetting(key: keyof DesktopAppSettings) {
|
||||||
|
if (!settingsLoaded.value || savingSetting.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = appSettings.value;
|
||||||
|
const value = !previous[key];
|
||||||
|
appSettings.value = { ...previous, [key]: value };
|
||||||
|
savingSetting.value = key;
|
||||||
|
settingsError.value = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
appSettings.value = await window.desktopBridge.app.setAppSetting(key, value);
|
||||||
|
} catch (error) {
|
||||||
|
appSettings.value = previous;
|
||||||
|
settingsError.value = error instanceof Error ? error.message : "设置保存失败";
|
||||||
|
} finally {
|
||||||
|
savingSetting.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void loadAppSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(apiBaseURL, (next) => {
|
||||||
|
serverBaseURL.value = next;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="settings-shell">
|
||||||
|
<aside class="settings-nav">
|
||||||
|
<nav class="settings-nav__list" aria-label="设置分类">
|
||||||
|
<button
|
||||||
|
v-for="item in sections"
|
||||||
|
:key="item.key"
|
||||||
|
type="button"
|
||||||
|
class="settings-nav__item"
|
||||||
|
:class="{ 'is-active': activeSection === item.key }"
|
||||||
|
@click="activeSection = item.key"
|
||||||
|
>
|
||||||
|
<component :is="item.icon" class="settings-nav__icon" />
|
||||||
|
<span>{{ item.title }}</span>
|
||||||
|
<span v-if="item.key === 'diagnostics' && clientCount > 0" class="settings-nav__badge">
|
||||||
|
{{ clientCount }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="settings-main">
|
||||||
|
<header class="settings-heading">
|
||||||
|
<h1>{{ activeTitle }}</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="settings-body">
|
||||||
|
<section v-if="activeSection === 'about'" class="about-page">
|
||||||
|
<div class="about-version-card">
|
||||||
|
<div class="about-logo" aria-hidden="true">
|
||||||
|
<span class="brand-ring"></span>
|
||||||
|
<span class="brand-core"></span>
|
||||||
|
</div>
|
||||||
|
<strong>版本: {{ appVersion }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="about-footer">
|
||||||
|
<p>
|
||||||
|
Copyright © 2026 省心推. All Rights Reserved.
|
||||||
|
<a href="https://shengxintui.com/terms" @click.prevent="openServiceTerms">服务协议</a>
|
||||||
|
</p>
|
||||||
|
<p class="tech-line">
|
||||||
|
基于 <span class="tech-mark">◇</span><strong>省心推</strong> Desktop 技术架构
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="activeSection === 'diagnostics'" class="diagnostics-page">
|
||||||
|
<div class="diagnostics-card">
|
||||||
|
<div class="card-title-row">
|
||||||
|
<div>
|
||||||
|
<h2>运行时快照</h2>
|
||||||
|
<p>客户端、调度器与发布通道状态</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="copy-button" @click="copyLogs">
|
||||||
|
{{ copied ? "已复制" : "复制日志" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-collapse :bordered="false" class="diagnostics-collapse">
|
||||||
|
<a-collapse-panel
|
||||||
|
v-for="entry in subsystemEntries"
|
||||||
|
:key="entry.key"
|
||||||
|
:header="entry.key"
|
||||||
|
class="collapse-item"
|
||||||
|
>
|
||||||
|
<pre class="json-code">{{ entry.value }}</pre>
|
||||||
|
</a-collapse-panel>
|
||||||
|
|
||||||
|
<a-collapse-panel key="raw" header="完整 Runtime 快照" class="collapse-item">
|
||||||
|
<pre class="json-code">{{ JSON.stringify(snapshot, null, 2) }}</pre>
|
||||||
|
</a-collapse-panel>
|
||||||
|
</a-collapse>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="activeSection === 'login'" class="settings-form-page">
|
||||||
|
<div class="server-settings-card">
|
||||||
|
<div class="server-settings-row">
|
||||||
|
<div class="server-settings-copy">
|
||||||
|
<strong>服务器地址</strong>
|
||||||
|
<span>默认连接省心推官方服务;私有化部署时可改为客户自己的网关 API 地址。</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model.trim="serverBaseURL"
|
||||||
|
class="server-input"
|
||||||
|
type="url"
|
||||||
|
:placeholder="DEFAULT_API_BASE_URL"
|
||||||
|
spellcheck="false"
|
||||||
|
@keydown.enter.prevent="saveServerSettings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="server-actions">
|
||||||
|
<span class="server-current">当前: {{ apiBaseURL }}</span>
|
||||||
|
<button type="button" class="server-button" @click="resetServerSettings">
|
||||||
|
恢复默认
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="server-button server-button--primary"
|
||||||
|
:disabled="!hasServerSettingChanged"
|
||||||
|
@click="saveServerSettings"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="serverSettingsError" class="settings-error">{{ serverSettingsError }}</p>
|
||||||
|
<p v-else-if="serverSettingsMessage" class="settings-success">{{ serverSettingsMessage }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else-if="activeSection === 'general'" class="settings-form-page">
|
||||||
|
<div class="settings-list-card">
|
||||||
|
<div
|
||||||
|
v-for="row in generalRows"
|
||||||
|
:key="row.key"
|
||||||
|
class="settings-row"
|
||||||
|
:class="{ 'is-disabled': !settingsLoaded || (savingSetting !== null && savingSetting !== row.key) }"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="row.enabled"
|
||||||
|
tabindex="0"
|
||||||
|
@click="toggleAppSetting(row.key)"
|
||||||
|
@keydown.enter.prevent="toggleAppSetting(row.key)"
|
||||||
|
@keydown.space.prevent="toggleAppSetting(row.key)"
|
||||||
|
>
|
||||||
|
<span>{{ row.label }}</span>
|
||||||
|
<a-switch
|
||||||
|
class="row-switch"
|
||||||
|
:checked="row.enabled"
|
||||||
|
:loading="savingSetting === row.key"
|
||||||
|
:disabled="!settingsLoaded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="settingsError" class="settings-error">{{ settingsError }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else class="settings-form-page">
|
||||||
|
<div class="settings-list-card">
|
||||||
|
<div class="settings-row settings-row--static">
|
||||||
|
<span>暂无可配置项</span>
|
||||||
|
<span class="row-value">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.settings-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 220px minmax(0, 1fr);
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f4f4f5;
|
||||||
|
color: #0a0a0a;
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav {
|
||||||
|
height: 100vh;
|
||||||
|
padding: 50px 10px 18px;
|
||||||
|
background: #dedee4;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav__list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: #030303;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: left;
|
||||||
|
transition: background-color 0.16s ease;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav__item:hover {
|
||||||
|
background: rgba(197, 197, 203, 0.64);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav__item.is-active {
|
||||||
|
background: #c9c9cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav__icon {
|
||||||
|
width: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #080808;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav__badge {
|
||||||
|
margin-left: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: #c4c4ca;
|
||||||
|
color: #111111;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-main {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100vh;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f6f6f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 76px;
|
||||||
|
padding: 0 42px;
|
||||||
|
border-bottom: 1px solid #e7e7e8;
|
||||||
|
background: #f7f7f8;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-heading h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-body {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-page {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
|
padding: 28px 44px 108px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-version-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 78px;
|
||||||
|
gap: 22px;
|
||||||
|
padding: 0 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-version-card strong {
|
||||||
|
color: #050505;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-logo {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-ring,
|
||||||
|
.brand-core {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-ring {
|
||||||
|
inset: 0;
|
||||||
|
border: 1px solid #8ec8ff;
|
||||||
|
background: #e5f3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-core {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
background: #1677ff;
|
||||||
|
box-shadow: 0 3px 8px rgba(22, 119, 255, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-footer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 34px;
|
||||||
|
color: #777777;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-footer p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-footer a {
|
||||||
|
color: #1677ff;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-line {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 14px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-mark {
|
||||||
|
color: #d59a4a;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-page,
|
||||||
|
.settings-form-page {
|
||||||
|
padding: 28px 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-card,
|
||||||
|
.settings-list-card {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-settings-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 22px 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-settings-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 0.72fr) minmax(260px, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-settings-copy {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-settings-copy strong {
|
||||||
|
color: #090909;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-settings-copy span {
|
||||||
|
color: #777777;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #d8d8dc;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #f7f7f8;
|
||||||
|
color: #111111;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-input:focus {
|
||||||
|
border-color: #b7c6db;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border-top: 1px solid #eeeeef;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-current {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #777777;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-button {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eeeeef;
|
||||||
|
color: #111111;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-button:hover:not(:disabled) {
|
||||||
|
background: #e3e3e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-button--primary {
|
||||||
|
background: #1677ff;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-button--primary:hover:not(:disabled) {
|
||||||
|
background: #0f6bea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 22px 28px;
|
||||||
|
border-bottom: 1px solid #eeeeef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-row h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #050505;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-row p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: #808080;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eeeeef;
|
||||||
|
color: #111111;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostics-collapse {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-item {
|
||||||
|
border-bottom: 1px solid #eeeeef !important;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-collapse-header) {
|
||||||
|
padding: 14px 28px !important;
|
||||||
|
color: #111111 !important;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-collapse-content) {
|
||||||
|
border-top: 1px solid #eeeeef !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-code {
|
||||||
|
max-height: 330px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5f5f6;
|
||||||
|
color: #333333;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 0 28px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #eeeeef;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #090909;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row:hover:not(.is-disabled):not(.settings-row--static) {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row:focus-visible {
|
||||||
|
background: #f3f6fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row.is-disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-row--static,
|
||||||
|
.settings-row--static:hover {
|
||||||
|
cursor: default;
|
||||||
|
background: #ffffff;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-switch {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-value {
|
||||||
|
color: #7d7d7d;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-error {
|
||||||
|
margin: 12px 4px 0;
|
||||||
|
color: #d93025;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-success {
|
||||||
|
margin: 12px 4px 0;
|
||||||
|
color: #27824a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.settings-shell {
|
||||||
|
grid-template-columns: 190px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-nav__item {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-heading,
|
||||||
|
.about-page,
|
||||||
|
.diagnostics-page,
|
||||||
|
.settings-form-page {
|
||||||
|
padding-left: 28px;
|
||||||
|
padding-right: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-settings-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import SettingsView from "./SettingsView.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SettingsView />
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user