mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-01-11 02:28:30 +08:00
feat: add proxy settings to Settings dialog (Desktop only) (#537)
* feat: add proxy settings to Settings dialog (Desktop only) Fixes #535 - Desktop app now respects HTTP/HTTPS proxy configuration. - Add proxy-manager.ts to handle proxy config storage (JSON file in userData) - Load proxy settings on app startup before Next.js server starts - Add IPC handlers for get-proxy and set-proxy - Add proxy settings UI in Settings dialog (Electron only) - Add translations for en/zh/ja * fix: improve proxy settings reliability and simplify UI - Fix server restart race condition (wait for process exit before starting new server) - Add URL validation (must include http:// or https:// prefix) - Enable Node.js built-in proxy support (NODE_USE_ENV_PROXY=1) - Remove "Proxy Exceptions" field (unnecessary for this app) - Add debug logging for proxy env vars * refactor: remove duplicate ProxyConfig interface, import from electron.d.ts
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { Github, Info, Moon, Sun, Tag } from "lucide-react"
|
import { Github, Info, Moon, Sun, Tag } from "lucide-react"
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
import { Suspense, useEffect, useState } from "react"
|
import { Suspense, useEffect, useState } from "react"
|
||||||
|
import { toast } from "sonner"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -103,6 +104,11 @@ function SettingsContent({
|
|||||||
)
|
)
|
||||||
const [currentLang, setCurrentLang] = useState("en")
|
const [currentLang, setCurrentLang] = useState("en")
|
||||||
|
|
||||||
|
// Proxy settings state (Electron only)
|
||||||
|
const [httpProxy, setHttpProxy] = useState("")
|
||||||
|
const [httpsProxy, setHttpsProxy] = useState("")
|
||||||
|
const [isApplyingProxy, setIsApplyingProxy] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only fetch if not cached in localStorage
|
// Only fetch if not cached in localStorage
|
||||||
if (getStoredAccessCodeRequired() !== null) return
|
if (getStoredAccessCodeRequired() !== null) return
|
||||||
@@ -150,6 +156,14 @@ function SettingsContent({
|
|||||||
setCloseProtection(storedCloseProtection !== "false")
|
setCloseProtection(storedCloseProtection !== "false")
|
||||||
|
|
||||||
setError("")
|
setError("")
|
||||||
|
|
||||||
|
// Load proxy settings (Electron only)
|
||||||
|
if (window.electronAPI?.getProxy) {
|
||||||
|
window.electronAPI.getProxy().then((config) => {
|
||||||
|
setHttpProxy(config.httpProxy || "")
|
||||||
|
setHttpsProxy(config.httpsProxy || "")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
@@ -208,6 +222,46 @@ function SettingsContent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleApplyProxy = async () => {
|
||||||
|
if (!window.electronAPI?.setProxy) return
|
||||||
|
|
||||||
|
// Validate proxy URLs (must start with http:// or https://)
|
||||||
|
const validateProxyUrl = (url: string): boolean => {
|
||||||
|
if (!url) return true // Empty is OK
|
||||||
|
return url.startsWith("http://") || url.startsWith("https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedHttp = httpProxy.trim()
|
||||||
|
const trimmedHttps = httpsProxy.trim()
|
||||||
|
|
||||||
|
if (trimmedHttp && !validateProxyUrl(trimmedHttp)) {
|
||||||
|
toast.error("HTTP Proxy must start with http:// or https://")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (trimmedHttps && !validateProxyUrl(trimmedHttps)) {
|
||||||
|
toast.error("HTTPS Proxy must start with http:// or https://")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsApplyingProxy(true)
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.setProxy({
|
||||||
|
httpProxy: trimmedHttp || undefined,
|
||||||
|
httpsProxy: trimmedHttps || undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(dict.settings.proxyApplied)
|
||||||
|
} else {
|
||||||
|
toast.error(result.error || "Failed to apply proxy settings")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("Failed to apply proxy settings")
|
||||||
|
} finally {
|
||||||
|
setIsApplyingProxy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent className="sm:max-w-lg p-0 gap-0">
|
<DialogContent className="sm:max-w-lg p-0 gap-0">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -370,6 +424,54 @@ function SettingsContent({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
{/* Proxy Settings - Electron only */}
|
||||||
|
{typeof window !== "undefined" &&
|
||||||
|
window.electronAPI?.isElectron && (
|
||||||
|
<div className="py-4 space-y-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label className="text-sm font-medium">
|
||||||
|
{dict.settings.proxy}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{dict.settings.proxyDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
id="http-proxy"
|
||||||
|
type="text"
|
||||||
|
value={httpProxy}
|
||||||
|
onChange={(e) =>
|
||||||
|
setHttpProxy(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder={`${dict.settings.httpProxy}: http://proxy:8080`}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="https-proxy"
|
||||||
|
type="text"
|
||||||
|
value={httpsProxy}
|
||||||
|
onChange={(e) =>
|
||||||
|
setHttpsProxy(e.target.value)
|
||||||
|
}
|
||||||
|
placeholder={`${dict.settings.httpsProxy}: http://proxy:8080`}
|
||||||
|
className="h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleApplyProxy}
|
||||||
|
disabled={isApplyingProxy}
|
||||||
|
className="h-9 px-4 rounded-xl w-full"
|
||||||
|
>
|
||||||
|
{isApplyingProxy
|
||||||
|
? "..."
|
||||||
|
: dict.settings.applyProxy}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
19
electron/electron.d.ts
vendored
19
electron/electron.d.ts
vendored
@@ -25,6 +25,19 @@ interface ApplyPresetResult {
|
|||||||
env?: Record<string, string>
|
env?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Proxy configuration interface */
|
||||||
|
interface ProxyConfig {
|
||||||
|
httpProxy?: string
|
||||||
|
httpsProxy?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result of setting proxy */
|
||||||
|
interface SetProxyResult {
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
devMode?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
/** Main window Electron API */
|
/** Main window Electron API */
|
||||||
@@ -45,6 +58,10 @@ declare global {
|
|||||||
openFile: () => Promise<string | null>
|
openFile: () => Promise<string | null>
|
||||||
/** Save data to file via save dialog */
|
/** Save data to file via save dialog */
|
||||||
saveFile: (data: string) => Promise<boolean>
|
saveFile: (data: string) => Promise<boolean>
|
||||||
|
/** Get proxy configuration */
|
||||||
|
getProxy: () => Promise<ProxyConfig>
|
||||||
|
/** Set proxy configuration (saves and restarts server) */
|
||||||
|
setProxy: (config: ProxyConfig) => Promise<SetProxyResult>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Settings window Electron API */
|
/** Settings window Electron API */
|
||||||
@@ -71,4 +88,4 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ConfigPreset, ApplyPresetResult }
|
export { ConfigPreset, ApplyPresetResult, ProxyConfig, SetProxyResult }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getCurrentPresetEnv } from "./config-manager"
|
|||||||
import { loadEnvFile } from "./env-loader"
|
import { loadEnvFile } from "./env-loader"
|
||||||
import { registerIpcHandlers } from "./ipc-handlers"
|
import { registerIpcHandlers } from "./ipc-handlers"
|
||||||
import { startNextServer, stopNextServer } from "./next-server"
|
import { startNextServer, stopNextServer } from "./next-server"
|
||||||
|
import { applyProxyToEnv } from "./proxy-manager"
|
||||||
import { registerSettingsWindowHandlers } from "./settings-window"
|
import { registerSettingsWindowHandlers } from "./settings-window"
|
||||||
import { createWindow, getMainWindow } from "./window-manager"
|
import { createWindow, getMainWindow } from "./window-manager"
|
||||||
|
|
||||||
@@ -24,6 +25,9 @@ if (!gotTheLock) {
|
|||||||
// Load environment variables from .env files
|
// Load environment variables from .env files
|
||||||
loadEnvFile()
|
loadEnvFile()
|
||||||
|
|
||||||
|
// Apply proxy settings from saved config
|
||||||
|
applyProxyToEnv()
|
||||||
|
|
||||||
// Apply saved preset environment variables (overrides .env)
|
// Apply saved preset environment variables (overrides .env)
|
||||||
const presetEnv = getCurrentPresetEnv()
|
const presetEnv = getCurrentPresetEnv()
|
||||||
for (const [key, value] of Object.entries(presetEnv)) {
|
for (const [key, value] of Object.entries(presetEnv)) {
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import {
|
|||||||
updatePreset,
|
updatePreset,
|
||||||
} from "./config-manager"
|
} from "./config-manager"
|
||||||
import { restartNextServer } from "./next-server"
|
import { restartNextServer } from "./next-server"
|
||||||
|
import {
|
||||||
|
applyProxyToEnv,
|
||||||
|
getProxyConfig,
|
||||||
|
type ProxyConfig,
|
||||||
|
saveProxyConfig,
|
||||||
|
} from "./proxy-manager"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allowed configuration keys for presets
|
* Allowed configuration keys for presets
|
||||||
@@ -209,4 +215,40 @@ export function registerIpcHandlers(): void {
|
|||||||
return setCurrentPreset(id)
|
return setCurrentPreset(id)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ==================== Proxy Settings ====================
|
||||||
|
|
||||||
|
ipcMain.handle("get-proxy", () => {
|
||||||
|
return getProxyConfig()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("set-proxy", async (_event, config: ProxyConfig) => {
|
||||||
|
try {
|
||||||
|
// Save config to file
|
||||||
|
saveProxyConfig(config)
|
||||||
|
|
||||||
|
// Apply to current process environment
|
||||||
|
applyProxyToEnv()
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === "development"
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// In development, env vars are already applied
|
||||||
|
// Next.js dev server may need manual restart
|
||||||
|
return { success: true, devMode: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production: restart Next.js server to pick up new env vars
|
||||||
|
await restartNextServer()
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to apply proxy settings",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export async function startNextServer(): Promise<string> {
|
|||||||
NODE_ENV: "production",
|
NODE_ENV: "production",
|
||||||
PORT: String(port),
|
PORT: String(port),
|
||||||
HOSTNAME: "localhost",
|
HOSTNAME: "localhost",
|
||||||
|
// Enable Node.js built-in proxy support for fetch (Node.js 24+)
|
||||||
|
NODE_USE_ENV_PROXY: "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set cache directory to a writable location (user's app data folder)
|
// Set cache directory to a writable location (user's app data folder)
|
||||||
@@ -85,6 +87,13 @@ export async function startNextServer(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: log proxy-related env vars
|
||||||
|
console.log("Proxy env vars being passed to server:", {
|
||||||
|
HTTP_PROXY: env.HTTP_PROXY || env.http_proxy || "not set",
|
||||||
|
HTTPS_PROXY: env.HTTPS_PROXY || env.https_proxy || "not set",
|
||||||
|
NODE_USE_ENV_PROXY: env.NODE_USE_ENV_PROXY || "not set",
|
||||||
|
})
|
||||||
|
|
||||||
// Use Electron's utilityProcess API for running Node.js in background
|
// Use Electron's utilityProcess API for running Node.js in background
|
||||||
// This is the recommended way to run Node.js code in Electron
|
// This is the recommended way to run Node.js code in Electron
|
||||||
serverProcess = utilityProcess.fork(serverPath, [], {
|
serverProcess = utilityProcess.fork(serverPath, [], {
|
||||||
@@ -114,13 +123,41 @@ export async function startNextServer(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the Next.js server process
|
* Stop the Next.js server process and wait for it to exit
|
||||||
*/
|
*/
|
||||||
export function stopNextServer(): void {
|
export async function stopNextServer(): Promise<void> {
|
||||||
if (serverProcess) {
|
if (serverProcess) {
|
||||||
console.log("Stopping Next.js server...")
|
console.log("Stopping Next.js server...")
|
||||||
|
|
||||||
|
// Create a promise that resolves when the process exits
|
||||||
|
const exitPromise = new Promise<void>((resolve) => {
|
||||||
|
const proc = serverProcess
|
||||||
|
if (!proc) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const onExit = () => {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.once("exit", onExit)
|
||||||
|
|
||||||
|
// Timeout after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
proc.removeListener("exit", onExit)
|
||||||
|
resolve()
|
||||||
|
}, 5000)
|
||||||
|
})
|
||||||
|
|
||||||
serverProcess.kill()
|
serverProcess.kill()
|
||||||
serverProcess = null
|
serverProcess = null
|
||||||
|
|
||||||
|
// Wait for process to exit
|
||||||
|
await exitPromise
|
||||||
|
|
||||||
|
// Additional wait for OS to release port
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,8 +187,8 @@ async function waitForServerStop(timeout = 5000): Promise<void> {
|
|||||||
export async function restartNextServer(): Promise<string> {
|
export async function restartNextServer(): Promise<string> {
|
||||||
console.log("Restarting Next.js server...")
|
console.log("Restarting Next.js server...")
|
||||||
|
|
||||||
// Stop the current server
|
// Stop the current server and wait for it to exit
|
||||||
stopNextServer()
|
await stopNextServer()
|
||||||
|
|
||||||
// Wait for the port to be released
|
// Wait for the port to be released
|
||||||
await waitForServerStop()
|
await waitForServerStop()
|
||||||
|
|||||||
75
electron/main/proxy-manager.ts
Normal file
75
electron/main/proxy-manager.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { app } from "electron"
|
||||||
|
import * as fs from "fs"
|
||||||
|
import * as path from "path"
|
||||||
|
import type { ProxyConfig } from "../electron.d"
|
||||||
|
|
||||||
|
export type { ProxyConfig }
|
||||||
|
|
||||||
|
const CONFIG_FILE = "proxy-config.json"
|
||||||
|
|
||||||
|
function getConfigPath(): string {
|
||||||
|
return path.join(app.getPath("userData"), CONFIG_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load proxy configuration from JSON file
|
||||||
|
*/
|
||||||
|
export function loadProxyConfig(): ProxyConfig {
|
||||||
|
try {
|
||||||
|
const configPath = getConfigPath()
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
const data = fs.readFileSync(configPath, "utf-8")
|
||||||
|
return JSON.parse(data) as ProxyConfig
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load proxy config:", error)
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save proxy configuration to JSON file
|
||||||
|
*/
|
||||||
|
export function saveProxyConfig(config: ProxyConfig): void {
|
||||||
|
try {
|
||||||
|
const configPath = getConfigPath()
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8")
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save proxy config:", error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply proxy configuration to process.env
|
||||||
|
* Must be called BEFORE starting the Next.js server
|
||||||
|
*/
|
||||||
|
export function applyProxyToEnv(): void {
|
||||||
|
const config = loadProxyConfig()
|
||||||
|
|
||||||
|
if (config.httpProxy) {
|
||||||
|
process.env.HTTP_PROXY = config.httpProxy
|
||||||
|
process.env.http_proxy = config.httpProxy
|
||||||
|
} else {
|
||||||
|
delete process.env.HTTP_PROXY
|
||||||
|
delete process.env.http_proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.httpsProxy) {
|
||||||
|
process.env.HTTPS_PROXY = config.httpsProxy
|
||||||
|
process.env.https_proxy = config.httpsProxy
|
||||||
|
} else {
|
||||||
|
delete process.env.HTTPS_PROXY
|
||||||
|
delete process.env.https_proxy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current proxy configuration (from process.env)
|
||||||
|
*/
|
||||||
|
export function getProxyConfig(): ProxyConfig {
|
||||||
|
return {
|
||||||
|
httpProxy: process.env.HTTP_PROXY || process.env.http_proxy || "",
|
||||||
|
httpsProxy: process.env.HTTPS_PROXY || process.env.https_proxy || "",
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,4 +21,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
// File operations
|
// File operations
|
||||||
openFile: () => ipcRenderer.invoke("dialog-open-file"),
|
openFile: () => ipcRenderer.invoke("dialog-open-file"),
|
||||||
saveFile: (data: string) => ipcRenderer.invoke("dialog-save-file", data),
|
saveFile: (data: string) => ipcRenderer.invoke("dialog-save-file", data),
|
||||||
|
|
||||||
|
// Proxy settings
|
||||||
|
getProxy: () => ipcRenderer.invoke("get-proxy"),
|
||||||
|
setProxy: (config: { httpProxy?: string; httpsProxy?: string }) =>
|
||||||
|
ipcRenderer.invoke("set-proxy", config),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -107,7 +107,13 @@
|
|||||||
"diagramActions": "Diagram Actions",
|
"diagramActions": "Diagram Actions",
|
||||||
"diagramActionsDescription": "Manage diagram history and exports",
|
"diagramActionsDescription": "Manage diagram history and exports",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
"download": "Download"
|
"download": "Download",
|
||||||
|
"proxy": "Proxy Settings",
|
||||||
|
"proxyDescription": "Configure HTTP/HTTPS proxy for API requests (Desktop only)",
|
||||||
|
"httpProxy": "HTTP Proxy",
|
||||||
|
"httpsProxy": "HTTPS Proxy",
|
||||||
|
"applyProxy": "Apply",
|
||||||
|
"proxyApplied": "Proxy settings applied"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "Save Diagram",
|
"title": "Save Diagram",
|
||||||
|
|||||||
@@ -107,7 +107,13 @@
|
|||||||
"diagramActions": "ダイアグラム操作",
|
"diagramActions": "ダイアグラム操作",
|
||||||
"diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
|
"diagramActionsDescription": "ダイアグラムの履歴とエクスポートを管理",
|
||||||
"history": "履歴",
|
"history": "履歴",
|
||||||
"download": "ダウンロード"
|
"download": "ダウンロード",
|
||||||
|
"proxy": "プロキシ設定",
|
||||||
|
"proxyDescription": "API リクエスト用の HTTP/HTTPS プロキシを設定(デスクトップ版のみ)",
|
||||||
|
"httpProxy": "HTTP プロキシ",
|
||||||
|
"httpsProxy": "HTTPS プロキシ",
|
||||||
|
"applyProxy": "適用",
|
||||||
|
"proxyApplied": "プロキシ設定が適用されました"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "ダイアグラムを保存",
|
"title": "ダイアグラムを保存",
|
||||||
|
|||||||
@@ -107,7 +107,13 @@
|
|||||||
"diagramActions": "图表操作",
|
"diagramActions": "图表操作",
|
||||||
"diagramActionsDescription": "管理图表历史记录和导出",
|
"diagramActionsDescription": "管理图表历史记录和导出",
|
||||||
"history": "历史记录",
|
"history": "历史记录",
|
||||||
"download": "下载"
|
"download": "下载",
|
||||||
|
"proxy": "代理设置",
|
||||||
|
"proxyDescription": "配置 API 请求的 HTTP/HTTPS 代理(仅桌面版)",
|
||||||
|
"httpProxy": "HTTP 代理",
|
||||||
|
"httpsProxy": "HTTPS 代理",
|
||||||
|
"applyProxy": "应用",
|
||||||
|
"proxyApplied": "代理设置已应用"
|
||||||
},
|
},
|
||||||
"save": {
|
"save": {
|
||||||
"title": "保存图表",
|
"title": "保存图表",
|
||||||
|
|||||||
Reference in New Issue
Block a user