From 39fc21b7dffdb25b4cfedad6104763f28eee0b38 Mon Sep 17 00:00:00 2001 From: imsyy Date: Mon, 6 Jul 2026 16:03:09 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/apis/netease/core/ncbl.ts | 28 +++++++------ electron/main/apis/netease/core/request.ts | 8 +++- electron/main/apis/netease/core/xeapi.ts | 3 +- electron/main/core/index.ts | 3 ++ electron/main/ipc/config.ts | 6 +++ electron/main/ipc/system.ts | 4 ++ electron/main/utils/proxy.ts | 44 ++++++++++++++++++++ electron/preload/index.d.ts | 1 + electron/preload/index.ts | 2 + shared/defaults/settings.ts | 5 +++ shared/types/settings.ts | 15 +++++++ src/components/settings/SettingsItem.vue | 9 +++++ src/i18n/locales/en-US.json | 22 ++++++++++ src/i18n/locales/zh-CN.json | 22 ++++++++++ src/settings/categories/services.ts | 47 ++++++++++++++++++++++ src/types/settings-schema.ts | 1 + 16 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 electron/main/utils/proxy.ts diff --git a/electron/main/apis/netease/core/ncbl.ts b/electron/main/apis/netease/core/ncbl.ts index 703533ff..e5539c27 100644 --- a/electron/main/apis/netease/core/ncbl.ts +++ b/electron/main/apis/netease/core/ncbl.ts @@ -1,5 +1,6 @@ import { randomBytes, randomUUID as cryptoRandomUUID } from "node:crypto"; import * as zlib from "node:zlib"; +import { net } from "electron"; import { CLIENT_LOG3_DOMAIN } from "./config"; interface NeteaseLogRecord { @@ -437,19 +438,22 @@ export const doUpload = async ( ): Promise => { const payload = encryptNCBL(metaJson, body); const multipart = buildMultipart(payload); - const resp = await fetch(`${CLIENT_LOG3_DOMAIN}/api/clientlog/encrypt/upload?multiupload=true`, { - method: "POST", - headers: { - "Content-Type": `multipart/form-data; boundary=${multipart.boundary}`, - Referer: "https://music.163.com/di", - "User-Agent": `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/${ctx.app.version}`, - "Accept-Encoding": "gzip,deflate", - "Accept-Language": "zh-CN,zh;q=0.8", - Cookie: cookieStr, + const resp = await net.fetch( + `${CLIENT_LOG3_DOMAIN}/api/clientlog/encrypt/upload?multiupload=true`, + { + method: "POST", + headers: { + "Content-Type": `multipart/form-data; boundary=${multipart.boundary}`, + Referer: "https://music.163.com/di", + "User-Agent": `Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36 Chrome/91.0.4472.164 NeteaseMusicDesktop/${ctx.app.version}`, + "Accept-Encoding": "gzip,deflate", + "Accept-Language": "zh-CN,zh;q=0.8", + Cookie: cookieStr, + }, + body: new Uint8Array(multipart.body), + signal: AbortSignal.timeout(15000), }, - body: new Uint8Array(multipart.body), - signal: AbortSignal.timeout(15000), - }); + ); const text = await resp.text(); let respBody: Record; diff --git a/electron/main/apis/netease/core/request.ts b/electron/main/apis/netease/core/request.ts index e23a1e75..38f4526c 100644 --- a/electron/main/apis/netease/core/request.ts +++ b/electron/main/apis/netease/core/request.ts @@ -6,6 +6,7 @@ */ import { randomBytes } from "node:crypto"; +import { net } from "electron"; import { API_DOMAIN, DOMAIN, @@ -266,7 +267,12 @@ export const createRequest = async ( let res: Response; try { - res = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(8000) }); + res = await net.fetch(url, { + method: "POST", + headers, + body, + signal: AbortSignal.timeout(8000), + }); } catch (err) { answer.status = 502; answer.body = { code: 502, msg: err instanceof Error ? err.message : String(err) }; diff --git a/electron/main/apis/netease/core/xeapi.ts b/electron/main/apis/netease/core/xeapi.ts index cd1f8400..461a3190 100644 --- a/electron/main/apis/netease/core/xeapi.ts +++ b/electron/main/apis/netease/core/xeapi.ts @@ -5,6 +5,7 @@ * 拉取 X25519 公钥包(缓存于进程),首次请求后服务端经响应头下发会话密钥,后续请求复用。 */ +import { net } from "electron"; import { API_DOMAIN, UA_MAP } from "./config"; import { xeapiSign, xeapiDecryptPublicKey, type XeapiPublicKey } from "./crypto"; @@ -40,7 +41,7 @@ const fetchPublicKey = async ( uid: "", }; - const res = await fetch(`${API_DOMAIN}/api/gorilla/anti/crawler/security/key/get`, { + const res = await net.fetch(`${API_DOMAIN}/api/gorilla/anti/crawler/security/key/get`, { method: "POST", headers: { "User-Agent": UA_MAP.api.android, diff --git a/electron/main/core/index.ts b/electron/main/core/index.ts index 735abdf5..43c2ebe7 100644 --- a/electron/main/core/index.ts +++ b/electron/main/core/index.ts @@ -23,6 +23,7 @@ import { dispose as disposePlaybackBridge, } from "@main/plugins/playbackBridge"; import { registerCacheScheme, handleCacheProtocol } from "@main/utils/protocol"; +import { applyNetworkProxy } from "@main/utils/proxy"; import { startServer, stopServer } from "@main/server"; import { initUpdater, disposeUpdater } from "@main/services/updater"; import { coreLog, initLogger } from "@main/utils/logger"; @@ -101,6 +102,8 @@ export const initApp = (): void => { electronApp.setAppUserModelId("top.imsyy.splayer-next"); // 注册 cache:// 协议处理 handleCacheProtocol(); + // 应用持久化网络代理 + void applyNetworkProxy(); app.on("browser-window-created", (_, window) => { optimizer.watchWindowShortcuts(window); }); diff --git a/electron/main/ipc/config.ts b/electron/main/ipc/config.ts index de07ce7e..0f9d3d15 100644 --- a/electron/main/ipc/config.ts +++ b/electron/main/ipc/config.ts @@ -31,6 +31,7 @@ import { isWin } from "@main/utils/config"; import { startServer, stopServer } from "@main/server"; import { setOrpheusProtocolRegistered } from "@main/services/orpheus"; import { setTaskbarThumbnailEnabled } from "@main/services/thumbnail"; +import { applyNetworkProxy } from "@main/utils/proxy"; /** 配置写入后的副作用 */ const applyConfigChange = (keyPath: string, value: unknown): void => { @@ -70,6 +71,11 @@ const applyConfigChange = (keyPath: string, value: unknown): void => { case "system.registerOrpheusProtocol": setOrpheusProtocolRegistered(value as boolean); break; + case "system.networkProxy.protocol": + case "system.networkProxy.host": + case "system.networkProxy.port": + void applyNetworkProxy(); + break; case "externalApi.enabled": void (value ? startServer() : stopServer()); break; diff --git a/electron/main/ipc/system.ts b/electron/main/ipc/system.ts index dc59877f..b14afa27 100644 --- a/electron/main/ipc/system.ts +++ b/electron/main/ipc/system.ts @@ -12,6 +12,7 @@ import { getMainWindow, focusMainWindow } from "@main/window"; import { fetchBytes } from "@main/utils/fetchBytes"; import { logsDir } from "@main/utils/paths"; import { consumePendingOrpheusUrl } from "@main/services/orpheus"; +import { testNetworkProxy } from "@main/utils/proxy"; /** * 注册系统相关的 IPC 事件 @@ -75,6 +76,9 @@ export const registerSystemIpc = (): void => { app.exit(0); }); + // 测试当前网络代理 + ipcMain.handle("system:testNetworkProxy", () => testNetworkProxy()); + // 把封面图 URL 拉成字节回渲染层 // 用于 canvas 取色等需要绕过跨域 tainted 的场景;限定 image/* 响应 ipcMain.handle("system:fetchRemoteBytes", async (_event, url: string) => { diff --git a/electron/main/utils/proxy.ts b/electron/main/utils/proxy.ts new file mode 100644 index 00000000..559f0572 --- /dev/null +++ b/electron/main/utils/proxy.ts @@ -0,0 +1,44 @@ +import { net, session } from "electron"; +import { store } from "@main/store"; +import { systemLog } from "@main/utils/logger"; + +const MAIN_PARTITION = "persist:main"; +const PROXY_TEST_URL = "https://www.baidu.com"; + +/** 当前网络代理规则 */ +export const currentProxyRules = (): string => { + const config = store.get("system.networkProxy"); + if (config.protocol === "off") return ""; + const host = config.host.trim(); + const port = Number(config.port); + if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return ""; + return `${config.protocol}://${host}:${port}`; +}; + +/** 应用网络代理配置到 Electron 会话 */ +export const applyNetworkProxy = async (): Promise => { + const proxyRules = currentProxyRules(); + try { + await Promise.all([ + session.defaultSession.setProxy({ proxyRules }), + session.fromPartition(MAIN_PARTITION).setProxy({ proxyRules }), + ]); + systemLog.info(proxyRules ? `[proxy] enabled ${proxyRules}` : "[proxy] disabled"); + } catch (err) { + systemLog.warn("[proxy] apply failed", err); + } +}; + +/** 测试当前代理是否可用 */ +export const testNetworkProxy = async (): Promise => { + const proxyRules = currentProxyRules(); + if (!proxyRules) return false; + try { + await applyNetworkProxy(); + const res = await net.fetch(PROXY_TEST_URL, { signal: AbortSignal.timeout(8000) }); + return res.ok; + } catch (err) { + systemLog.warn("[proxy] test failed", err); + return false; + } +}; diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 4bb40822..c1ef31bf 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -45,6 +45,7 @@ declare global { fileName: string, ) => Promise<{ success: boolean; path?: string; error?: string }>; relaunch: () => Promise; + testNetworkProxy: () => Promise; onProtocolUrl: (callback: (url: string) => void) => () => void; consumePendingProtocolUrl: () => Promise; }; diff --git a/electron/preload/index.ts b/electron/preload/index.ts index c780c75a..1fcf4b2d 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -134,6 +134,8 @@ const api = { ipcRenderer.invoke("system:saveFile", data, defaultName), // 重启应用 relaunch: () => ipcRenderer.invoke("system:relaunch"), + // 测试当前网络代理 + testNetworkProxy: () => ipcRenderer.invoke("system:testNetworkProxy"), // 订阅主进程下发的 orpheus 唤起 URL onProtocolUrl: (callback: (url: string) => void) => subscribe("protocol:orpheus", callback), diff --git a/shared/defaults/settings.ts b/shared/defaults/settings.ts index cdb5f85a..3099d3d3 100644 --- a/shared/defaults/settings.ts +++ b/shared/defaults/settings.ts @@ -145,6 +145,11 @@ export const defaultSystemConfig: SystemConfig = { uiZoom: 100, onboardingCompleted: false, neteaseRealIp: false, + networkProxy: { + protocol: "off", + host: "127.0.0.1", + port: 80, + }, neteaseScrobbleEnabled: false, neteaseScrobbleMode: "ncbl", registerOrpheusProtocol: false, diff --git a/shared/types/settings.ts b/shared/types/settings.ts index 351e3d5b..fa370885 100644 --- a/shared/types/settings.ts +++ b/shared/types/settings.ts @@ -226,6 +226,19 @@ export interface ExternalApiSettings { port: number; } +/** 网络代理协议 */ +export type NetworkProxyProtocol = "off" | "http" | "https"; + +/** 网络代理配置 */ +export interface NetworkProxySettings { + /** 代理协议;off 表示关闭 */ + protocol: NetworkProxyProtocol; + /** 代理服务器地址 */ + host: string; + /** 代理服务器端口 */ + port: number; +} + /** 外部 API 服务运行时状态 */ export interface ExternalApiStatus { /** 是否正在监听 */ @@ -397,6 +410,8 @@ export interface SystemConfig { onboardingCompleted: boolean; /** NCM请求注入国内 IP(X-Real-IP/X-Forwarded-For) */ neteaseRealIp: boolean; + /** 网络代理配置 */ + networkProxy: NetworkProxySettings; /** 听歌打卡开关 */ neteaseScrobbleEnabled: boolean; /** 听歌打卡上报方式 */ diff --git a/src/components/settings/SettingsItem.vue b/src/components/settings/SettingsItem.vue index e7bb6b7c..c1dfb2c0 100644 --- a/src/components/settings/SettingsItem.vue +++ b/src/components/settings/SettingsItem.vue @@ -132,6 +132,15 @@ const descriptionText = computed(() => :disabled="isDisabled" class="w-full" /> + => { + const ok = await window.api.system.testNetworkProxy(); + const { t } = i18n.global; + if (ok) toast.success(t("settings.networkProxyTest.success")); + else toast.error(t("settings.networkProxyTest.failed")); +}; + const servicesCategory: SettingCategory = { id: "services", icon: IconLucideGlobe, @@ -10,6 +20,43 @@ const servicesCategory: SettingCategory = { { id: "network", items: [ + { + key: "networkProxyProtocol", + type: "select", + binding: { store: "settings", path: "system.system.networkProxy.protocol" }, + options: [ + { value: "off", labelKey: "settings.networkProxyProtocol.off" }, + { value: "http", labelKey: "settings.networkProxyProtocol.http" }, + { value: "https", labelKey: "settings.networkProxyProtocol.https" }, + ], + defaultValue: "off", + childrenCondition: () => + useSettingsStore().system.system.networkProxy.protocol !== "off", + children: [ + { + key: "networkProxyHost", + type: "text", + binding: { store: "settings", path: "system.system.networkProxy.host" }, + defaultValue: "127.0.0.1", + placeholderKey: "settings.networkProxyHost.placeholder", + disabled: () => useSettingsStore().system.system.networkProxy.protocol === "off", + }, + { + key: "networkProxyPort", + type: "number", + binding: { store: "settings", path: "system.system.networkProxy.port" }, + min: 1, + max: 65535, + defaultValue: 80, + disabled: () => useSettingsStore().system.system.networkProxy.protocol === "off", + }, + { + key: "networkProxyTest", + type: "button", + action: testNetworkProxy, + }, + ], + }, { key: "neteaseRealIp", type: "switch", diff --git a/src/types/settings-schema.ts b/src/types/settings-schema.ts index d28561e9..d67b7712 100644 --- a/src/types/settings-schema.ts +++ b/src/types/settings-schema.ts @@ -8,6 +8,7 @@ export type SettingWidgetType = | "color" | "button" | "custom" + | "text" | "number"; /** 选择项 */ From 8484c628a4b6002478893b5e4dc79934764d90fd Mon Sep 17 00:00:00 2001 From: imsyy Date: Mon, 6 Jul 2026 17:04:49 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E4=BB=A3=E7=90=86=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/utils/proxy.ts | 28 +++++++++++++++++----------- shared/defaults/settings.ts | 2 +- shared/types/settings.ts | 4 ++-- src/i18n/locales/en-US.json | 3 ++- src/i18n/locales/zh-CN.json | 3 ++- src/settings/categories/services.ts | 9 ++++++--- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/electron/main/utils/proxy.ts b/electron/main/utils/proxy.ts index 559f0572..13afe443 100644 --- a/electron/main/utils/proxy.ts +++ b/electron/main/utils/proxy.ts @@ -1,29 +1,34 @@ import { net, session } from "electron"; +import type { ProxyConfig } from "electron"; import { store } from "@main/store"; import { systemLog } from "@main/utils/logger"; const MAIN_PARTITION = "persist:main"; const PROXY_TEST_URL = "https://www.baidu.com"; -/** 当前网络代理规则 */ -export const currentProxyRules = (): string => { +/** + * 将网络代理配置转换为 Electron ProxyConfig。 + * system 模式使用 { mode: "system" } 跟随系统代理;off 直连。 + */ +const buildProxyConfig = (): ProxyConfig => { const config = store.get("system.networkProxy"); - if (config.protocol === "off") return ""; + if (config.protocol === "system") return { mode: "system" }; + if (config.protocol === "off") return { mode: "direct" }; const host = config.host.trim(); const port = Number(config.port); - if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return ""; - return `${config.protocol}://${host}:${port}`; + if (!host || !Number.isInteger(port) || port < 1 || port > 65535) return { mode: "direct" }; + return { proxyRules: `${config.protocol}://${host}:${port}` }; }; /** 应用网络代理配置到 Electron 会话 */ export const applyNetworkProxy = async (): Promise => { - const proxyRules = currentProxyRules(); + const proxyConfig = buildProxyConfig(); try { await Promise.all([ - session.defaultSession.setProxy({ proxyRules }), - session.fromPartition(MAIN_PARTITION).setProxy({ proxyRules }), + session.defaultSession.setProxy(proxyConfig), + session.fromPartition(MAIN_PARTITION).setProxy(proxyConfig), ]); - systemLog.info(proxyRules ? `[proxy] enabled ${proxyRules}` : "[proxy] disabled"); + systemLog.info(`[proxy] mode=${proxyConfig.mode ?? "fixed_servers"}${proxyConfig.proxyRules ? ` rules=${proxyConfig.proxyRules}` : ""}`); } catch (err) { systemLog.warn("[proxy] apply failed", err); } @@ -31,8 +36,9 @@ export const applyNetworkProxy = async (): Promise => { /** 测试当前代理是否可用 */ export const testNetworkProxy = async (): Promise => { - const proxyRules = currentProxyRules(); - if (!proxyRules) return false; + const proxyConfig = buildProxyConfig(); + if (proxyConfig.mode === "system") return true; + if (proxyConfig.mode === "direct") return false; try { await applyNetworkProxy(); const res = await net.fetch(PROXY_TEST_URL, { signal: AbortSignal.timeout(8000) }); diff --git a/shared/defaults/settings.ts b/shared/defaults/settings.ts index 3099d3d3..a7389097 100644 --- a/shared/defaults/settings.ts +++ b/shared/defaults/settings.ts @@ -146,7 +146,7 @@ export const defaultSystemConfig: SystemConfig = { onboardingCompleted: false, neteaseRealIp: false, networkProxy: { - protocol: "off", + protocol: "system", host: "127.0.0.1", port: 80, }, diff --git a/shared/types/settings.ts b/shared/types/settings.ts index fa370885..543d645a 100644 --- a/shared/types/settings.ts +++ b/shared/types/settings.ts @@ -227,11 +227,11 @@ export interface ExternalApiSettings { } /** 网络代理协议 */ -export type NetworkProxyProtocol = "off" | "http" | "https"; +export type NetworkProxyProtocol = "system" | "off" | "http" | "https"; /** 网络代理配置 */ export interface NetworkProxySettings { - /** 代理协议;off 表示关闭 */ + /** 代理协议;system 跟随系统,off 直连 */ protocol: NetworkProxyProtocol; /** 代理服务器地址 */ host: string; diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index f180f955..10367da0 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -1479,7 +1479,8 @@ }, "networkProxyProtocol": { "label": "Network Proxy", - "description": "Configure a proxy for NCM API and main-window network requests", + "description": "Configure a proxy for all network requests", + "system": "Follow system", "off": "Proxy off", "http": "HTTP proxy", "https": "HTTPS proxy" diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 4322738d..f0d5c471 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -1467,7 +1467,8 @@ }, "networkProxyProtocol": { "label": "网络代理", - "description": "为网易云接口与主窗口网络请求配置代理", + "description": "为应用所有网络请求配置代理", + "system": "跟随系统", "off": "关闭代理", "http": "HTTP 代理", "https": "HTTPS 代理" diff --git a/src/settings/categories/services.ts b/src/settings/categories/services.ts index 0fb57853..94e733fb 100644 --- a/src/settings/categories/services.ts +++ b/src/settings/categories/services.ts @@ -25,13 +25,16 @@ const servicesCategory: SettingCategory = { type: "select", binding: { store: "settings", path: "system.system.networkProxy.protocol" }, options: [ + { value: "system", labelKey: "settings.networkProxyProtocol.system" }, { value: "off", labelKey: "settings.networkProxyProtocol.off" }, { value: "http", labelKey: "settings.networkProxyProtocol.http" }, { value: "https", labelKey: "settings.networkProxyProtocol.https" }, ], - defaultValue: "off", - childrenCondition: () => - useSettingsStore().system.system.networkProxy.protocol !== "off", + defaultValue: "system", + childrenCondition: () => { + const p = useSettingsStore().system.system.networkProxy.protocol; + return p !== "off" && p !== "system"; + }, children: [ { key: "networkProxyHost", From 4838a2c5591a608c4dec5e890581f015627b948d Mon Sep 17 00:00:00 2001 From: imsyy Date: Mon, 6 Jul 2026 17:36:56 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E4=BB=A3=E7=90=86=E6=B5=8B=E8=AF=95=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=92=8C=E8=AE=BE=E7=BD=AE=E9=A1=B9=E6=9B=B4=E6=96=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/utils/proxy.ts | 4 +++- src/components/settings/SettingsItem.vue | 3 ++- src/settings/categories/services.ts | 18 ++++++++++-------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/electron/main/utils/proxy.ts b/electron/main/utils/proxy.ts index 13afe443..d5d4bc34 100644 --- a/electron/main/utils/proxy.ts +++ b/electron/main/utils/proxy.ts @@ -28,7 +28,9 @@ export const applyNetworkProxy = async (): Promise => { session.defaultSession.setProxy(proxyConfig), session.fromPartition(MAIN_PARTITION).setProxy(proxyConfig), ]); - systemLog.info(`[proxy] mode=${proxyConfig.mode ?? "fixed_servers"}${proxyConfig.proxyRules ? ` rules=${proxyConfig.proxyRules}` : ""}`); + systemLog.info( + `[proxy] mode=${proxyConfig.mode ?? "fixed_servers"}${proxyConfig.proxyRules ? ` rules=${proxyConfig.proxyRules}` : ""}`, + ); } catch (err) { systemLog.warn("[proxy] apply failed", err); } diff --git a/src/components/settings/SettingsItem.vue b/src/components/settings/SettingsItem.vue index c1dfb2c0..5fdfed36 100644 --- a/src/components/settings/SettingsItem.vue +++ b/src/components/settings/SettingsItem.vue @@ -139,7 +139,8 @@ const descriptionText = computed(() => :disabled="isDisabled" clearable class="w-full" - @update:model-value="applyChange($event)" + @update:model-value="model = $event" + @blur="applyChange(model)" /> => { - const ok = await window.api.system.testNetworkProxy(); - const { t } = i18n.global; - if (ok) toast.success(t("settings.networkProxyTest.success")); - else toast.error(t("settings.networkProxyTest.failed")); -}; - const servicesCategory: SettingCategory = { id: "services", icon: IconLucideGlobe, @@ -56,7 +49,16 @@ const servicesCategory: SettingCategory = { { key: "networkProxyTest", type: "button", - action: testNetworkProxy, + action: async () => { + const { t } = i18n.global; + try { + const ok = await window.api.system.testNetworkProxy(); + if (ok) toast.success(t("settings.networkProxyTest.success")); + else toast.error(t("settings.networkProxyTest.failed")); + } catch { + toast.error(t("settings.networkProxyTest.failed")); + } + }, }, ], }, From aa51dc39653be08a1e173421cddc435846159d7b Mon Sep 17 00:00:00 2001 From: imsyy Date: Tue, 7 Jul 2026 10:49:56 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E6=97=B6=E5=92=8C=E5=A4=B1=E7=84=A6=E6=97=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/settings/SettingsItem.vue | 8 ++- src/components/ui/SInput.vue | 75 +++++++++++++++++++----- src/components/ui/SNumberInput.vue | 63 ++++++++++++++++++-- 3 files changed, 123 insertions(+), 23 deletions(-) diff --git a/src/components/settings/SettingsItem.vue b/src/components/settings/SettingsItem.vue index 5fdfed36..10c163c9 100644 --- a/src/components/settings/SettingsItem.vue +++ b/src/components/settings/SettingsItem.vue @@ -123,24 +123,26 @@ const descriptionText = computed(() => (), { @@ -30,6 +32,7 @@ const props = withDefaults(defineProps(), { resize: "none", size: "medium", status: "default", + updateOn: "input", }); const isTextarea = computed(() => props.type === "textarea"); @@ -66,10 +69,57 @@ const emit = defineEmits<{ }>(); const isFocused = ref(false); -const showClear = computed(() => props.clearable && props.modelValue.length > 0 && !props.disabled); +const draftValue = ref(props.modelValue); +const displayValue = computed(() => + props.updateOn === "blur" ? draftValue.value : props.modelValue, +); +const showClear = computed( + () => props.clearable && displayValue.value.length > 0 && !props.disabled, +); + +watch( + () => props.modelValue, + (value) => { + if (props.updateOn === "input" || !isFocused.value) draftValue.value = value; + }, +); + +const commitValue = (): void => { + if (draftValue.value !== props.modelValue) emit("update:modelValue", draftValue.value); +}; + +const rollbackValue = (): void => { + draftValue.value = props.modelValue; +}; + +const handleInput = (value: string): void => { + if (props.updateOn === "input") { + emit("update:modelValue", value); + return; + } + draftValue.value = value; +}; const handleClear = () => { - emit("update:modelValue", ""); + handleInput(""); +}; + +const handleBlur = (): void => { + isFocused.value = false; + if (props.updateOn === "blur") commitValue(); + emit("blur"); +}; + +const handleEnter = (event: KeyboardEvent): void => { + if (props.updateOn !== "blur") return; + commitValue(); + (event.currentTarget as HTMLInputElement).blur(); +}; + +const handleEscape = (event: KeyboardEvent): void => { + if (props.updateOn !== "blur") return; + rollbackValue(); + (event.currentTarget as HTMLInputElement).blur(); }; @@ -93,22 +143,20 @@ const handleClear = () => {