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",
|
||||
];
|
||||
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_INFO_URL = `${WANGYI_API_ORIGIN}/wemedia/info.do`;
|
||||
const WANGYI_CONSOLE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html`;
|
||||
const WANGYI_LOGIN_REDIRECT_URLS = [
|
||||
`${WANGYI_API_ORIGIN}/login.html`,
|
||||
`${WANGYI_CONSOLE_ORIGIN}/login.html`,
|
||||
`${WANGYI_LEGACY_CONSOLE_ORIGIN}/login.html`,
|
||||
"https://reg.163.com/",
|
||||
"https://dl.reg.163.com/",
|
||||
];
|
||||
const WANGYI_COOKIE_URLS = [
|
||||
WANGYI_API_ORIGIN,
|
||||
WANGYI_CONSOLE_ORIGIN,
|
||||
WANGYI_LEGACY_CONSOLE_ORIGIN,
|
||||
"https://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];
|
||||
if (!definition) {
|
||||
return {
|
||||
@@ -2460,6 +2467,10 @@ export async function silentRefreshPublishAccountSession(
|
||||
return false;
|
||||
}
|
||||
|
||||
if (account.platform === "wangyihao") {
|
||||
await restoreWangyihaoSessionCookies(account.id, handle.session);
|
||||
}
|
||||
|
||||
const definition = platformBindingDefinitions[account.platform];
|
||||
if (!definition) {
|
||||
return false;
|
||||
@@ -2537,13 +2548,6 @@ export async function inspectPublishAccountLocalSession(
|
||||
|
||||
const profile = await resolvePublishAccountProfileFromHandle(account, handle);
|
||||
if (!profile) {
|
||||
if (account.platform === "wangyihao") {
|
||||
return {
|
||||
availability: "expired",
|
||||
profile: null,
|
||||
};
|
||||
}
|
||||
|
||||
const consoleReady = await verifyPublishAccountConsoleAccess(account, handle).catch(() => false);
|
||||
if (consoleReady) {
|
||||
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,
|
||||
syncDesktopHealthIndicator,
|
||||
} from "./app-issue-indicator";
|
||||
import {
|
||||
getDesktopAppSettings,
|
||||
initDesktopAppSettings,
|
||||
setDesktopAppSetting,
|
||||
type DesktopAppSettingKey,
|
||||
} from "./app-settings";
|
||||
import { bindPublishAccount, openPublishAccountConsole } from "./account-binder";
|
||||
import { installObservedGlobalFetch, registerObservedRequestRendererTarget } from "./network-observer";
|
||||
import { initProcessMetricsSampler } from "./process-metrics";
|
||||
@@ -67,10 +73,13 @@ process.on("uncaughtException", (error) => {
|
||||
|
||||
let mainWindow: ElectronBrowserWindow | null = null;
|
||||
let loginWindow: ElectronBrowserWindow | null = null;
|
||||
let settingsWindow: ElectronBrowserWindow | null = null;
|
||||
let mainRendererContents: ElectronWebContents | null = null;
|
||||
let quitReleaseInFlight = false;
|
||||
const forceCloseWindows = new WeakSet<ElectronBrowserWindow>();
|
||||
|
||||
type WindowMode = "login" | "main";
|
||||
type RendererWindowMode = WindowMode | "settings";
|
||||
type WindowModeSource = "auth-state" | "login" | "logout";
|
||||
|
||||
interface WindowSizePreset {
|
||||
@@ -91,6 +100,14 @@ const WINDOW_SIZES: Record<WindowMode, WindowSizePreset> = {
|
||||
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 {
|
||||
return join(app.getPath("userData"), "window-mode.json");
|
||||
}
|
||||
@@ -155,11 +172,23 @@ function currentLoginWindow(): ElectronBrowserWindow | null {
|
||||
return loginWindow;
|
||||
}
|
||||
|
||||
function currentSettingsWindow(): ElectronBrowserWindow | null {
|
||||
if (!settingsWindow) {
|
||||
return null;
|
||||
}
|
||||
if (settingsWindow.isDestroyed()) {
|
||||
settingsWindow = null;
|
||||
return null;
|
||||
}
|
||||
return settingsWindow;
|
||||
}
|
||||
|
||||
function currentActiveWindow(): ElectronBrowserWindow | null {
|
||||
return currentMainWindow() ?? currentLoginWindow();
|
||||
}
|
||||
|
||||
function closeWindowAfterIpcReturn(window: ElectronBrowserWindow): void {
|
||||
forceCloseWindows.add(window);
|
||||
setTimeout(() => {
|
||||
if (!window.isDestroyed()) {
|
||||
window.close();
|
||||
@@ -197,6 +226,16 @@ async function ensureLoginWindow(): Promise<ElectronBrowserWindow> {
|
||||
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> {
|
||||
return mode === "main" ? ensureMainWindow() : ensureLoginWindow();
|
||||
}
|
||||
@@ -238,6 +277,16 @@ async function switchWindowMode(mode: WindowMode, _request: WindowModeRequest =
|
||||
if (previous && previous !== target && !previous.isDestroyed()) {
|
||||
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 {
|
||||
@@ -248,7 +297,7 @@ function preloadPath(): string {
|
||||
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();
|
||||
if (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;
|
||||
|
||||
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", () => {
|
||||
console.warn("[desktop-main] main window minimize event", {
|
||||
at: new Date().toISOString(),
|
||||
@@ -345,6 +406,41 @@ async function createAppWindow(mode: WindowMode): Promise<ElectronBrowserWindow>
|
||||
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 {
|
||||
if (error instanceof Error) {
|
||||
const copy = new Error(error.message || "ipc_handler_failed");
|
||||
@@ -450,6 +546,17 @@ function registerBridgeHandlers(): void {
|
||||
await shell.openExternal(url);
|
||||
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(
|
||||
"desktop:runtime-session-sync",
|
||||
(_event, session: DesktopRuntimeSessionSyncRequest | null) => {
|
||||
@@ -479,6 +586,7 @@ if (!hasSingleInstanceLock) {
|
||||
} else {
|
||||
app.whenReady().then(async () => {
|
||||
currentWindowMode = readPersistedWindowMode();
|
||||
initDesktopAppSettings();
|
||||
registerBridgeHandlers();
|
||||
await initSessionRegistry();
|
||||
initAccountHealth();
|
||||
@@ -508,6 +616,9 @@ if (!hasSingleInstanceLock) {
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (getDesktopAppSettings().keepRunningInBackground) {
|
||||
return;
|
||||
}
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ interface WindowModeRequest {
|
||||
animate?: boolean;
|
||||
}
|
||||
|
||||
interface DesktopAppSettings {
|
||||
openAtLogin: boolean;
|
||||
keepRunningInBackground: boolean;
|
||||
}
|
||||
|
||||
const rendererProxyRequestChannel = "desktop:renderer-devtools-proxy:request";
|
||||
const rendererProxyResponseChannel = "desktop:renderer-devtools-proxy:response";
|
||||
|
||||
@@ -120,6 +125,12 @@ const desktopBridge = {
|
||||
ipcRenderer.invoke("desktop:retry-publish-task", taskId) as Promise<CreatePublishJobResponse>,
|
||||
openExternalUrl: (url: string) =>
|
||||
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) =>
|
||||
ipcRenderer.invoke("desktop:runtime-session-sync", session) as Promise<null>,
|
||||
releaseRuntimeSession: (revoke?: boolean) =>
|
||||
|
||||
@@ -4,9 +4,10 @@ import { useDesktopSession } from "./composables/useDesktopSession";
|
||||
|
||||
const DesktopShell = defineAsyncComponent(() => import("./components/DesktopShell.vue"));
|
||||
const LoginView = defineAsyncComponent(() => import("./views/LoginView.vue"));
|
||||
const SettingsWindow = defineAsyncComponent(() => import("./views/SettingsWindow.vue"));
|
||||
|
||||
const { isAuthenticated, syncRuntimeSession } = useDesktopSession();
|
||||
type RendererWindowMode = "auto" | "login" | "main";
|
||||
type RendererWindowMode = "auto" | "login" | "main" | "settings";
|
||||
|
||||
interface WindowModeBridge {
|
||||
setWindowMode?: (
|
||||
@@ -17,7 +18,7 @@ interface WindowModeBridge {
|
||||
|
||||
function resolveRendererWindowMode(): RendererWindowMode {
|
||||
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 {
|
||||
@@ -31,7 +32,9 @@ function syncWindowMode(authenticated: boolean): void {
|
||||
}
|
||||
|
||||
const rendererWindowMode = resolveRendererWindowMode();
|
||||
document.documentElement.dataset.windowMode = rendererWindowMode;
|
||||
const usesAutomaticWindowMode = rendererWindowMode === "auto";
|
||||
const showsSettingsSurface = rendererWindowMode === "settings";
|
||||
const showsMainSurface = computed(() =>
|
||||
rendererWindowMode === "main" || (rendererWindowMode === "auto" && isAuthenticated.value),
|
||||
);
|
||||
@@ -63,6 +66,18 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DesktopShell v-if="showsMainSurface" />
|
||||
<SettingsWindow v-if="showsSettingsSurface" />
|
||||
<DesktopShell v-else-if="showsMainSurface" />
|
||||
<LoginView v-else />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
SendOutlined,
|
||||
LinkOutlined,
|
||||
RobotOutlined,
|
||||
ToolOutlined,
|
||||
LogoutOutlined,
|
||||
SettingOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
|
||||
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
||||
@@ -61,13 +61,6 @@ const navItems = computed(() => {
|
||||
count: accounts.filter((item) => monitoringIDs.has(item.platform)).length,
|
||||
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(
|
||||
() => liveClientLabel.value ?? session.value?.desktopClient?.device_name ?? "当前设备尚未注册 client",
|
||||
);
|
||||
|
||||
function openSettingsWindow() {
|
||||
void window.desktopBridge.app.openSettingsWindow();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -128,11 +125,18 @@ const clientLabel = computed(
|
||||
<span class="device-name-text">{{ clientLabel }}</span>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<a-tooltip title="退出登录" placement="top">
|
||||
<button type="button" class="logout-action-btn" @click="logout">
|
||||
<LogoutOutlined />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<div class="action-cluster">
|
||||
<a-tooltip title="设置" placement="top">
|
||||
<button type="button" class="footer-icon-btn" @click="openSettingsWindow">
|
||||
<SettingOutlined />
|
||||
</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>
|
||||
</section>
|
||||
|
||||
@@ -428,7 +432,13 @@ h1 {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.logout-action-btn {
|
||||
.action-cluster {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.footer-icon-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
@@ -441,9 +451,22 @@ h1 {
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
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;
|
||||
color: #ef4444;
|
||||
border-color: #fee2e2;
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ResolvedDeviceInfo {
|
||||
|
||||
const sessionStorageKey = "geo.desktop.session.v1";
|
||||
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 apiBaseURL = shallowRef(readStoredApiBaseURL());
|
||||
@@ -75,14 +76,14 @@ function persistSession(next: DesktopSession | null): void {
|
||||
|
||||
function readStoredApiBaseURL(): string {
|
||||
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 {
|
||||
const trimmed = value.trim() || "http://localhost:8080";
|
||||
const trimmed = value.trim() || DEFAULT_API_BASE_URL;
|
||||
apiBaseURL.value = trimmed;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -499,6 +500,12 @@ async function logout(): Promise<void> {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
+8
@@ -11,6 +11,11 @@ import type { DesktopRuntimeSnapshot } from "./types";
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface DesktopAppSettings {
|
||||
openAtLogin: boolean;
|
||||
keepRunningInBackground: boolean;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
desktopBridge: {
|
||||
app: {
|
||||
@@ -40,6 +45,9 @@ declare global {
|
||||
listPublishTasks(params?: ListDesktopPublishTasksParams): Promise<DesktopPublishTaskListResponse>;
|
||||
retryPublishTask(taskId: string): Promise<CreatePublishJobResponse>;
|
||||
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>;
|
||||
releaseRuntimeSession(revoke?: boolean): Promise<null>;
|
||||
setWindowMode(mode: "login" | "main", request?: {
|
||||
|
||||
@@ -23,11 +23,8 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import("./views/AiPlatformsView.vue"),
|
||||
},
|
||||
{ path: "/accounts", redirect: "/media-platforms" },
|
||||
{
|
||||
path: "/diagnostics",
|
||||
name: "diagnostics",
|
||||
component: () => import("./views/DiagnosticsView.vue"),
|
||||
},
|
||||
{ path: "/settings", redirect: "/" },
|
||||
{ path: "/diagnostics", redirect: "/" },
|
||||
];
|
||||
|
||||
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">
|
||||
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();
|
||||
|
||||
@@ -172,7 +172,7 @@ onBeforeUnmount(() => {
|
||||
<input
|
||||
v-model.trim="form.apiBaseURL"
|
||||
type="text"
|
||||
placeholder="http://localhost:8080"
|
||||
:placeholder="DEFAULT_API_BASE_URL"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</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