diff --git a/src/components/proxy/ProxyPanel.tsx b/src/components/proxy/ProxyPanel.tsx index e874e0cdb..1052f2ece 100644 --- a/src/components/proxy/ProxyPanel.tsx +++ b/src/components/proxy/ProxyPanel.tsx @@ -7,11 +7,14 @@ import { ListOrdered, Save, Loader2, + Zap, + Power, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; +import { ToggleRow } from "@/components/ui/toggle-row"; import { useProxyStatus } from "@/hooks/useProxyStatus"; import { toast } from "sonner"; import { useFailoverQueue } from "@/lib/query/failover"; @@ -25,8 +28,21 @@ import { } from "@/lib/query/proxy"; import type { ProxyStatus } from "@/types/proxy"; import { useTranslation } from "react-i18next"; +import { AnimatePresence, motion } from "framer-motion"; -export function ProxyPanel() { +interface ProxyPanelProps { + enableLocalProxy: boolean; + onEnableLocalProxyChange: (checked: boolean) => void; + onToggleProxy: (checked: boolean) => Promise; + isProxyPending: boolean; +} + +export function ProxyPanel({ + enableLocalProxy, + onEnableLocalProxyChange, + onToggleProxy, + isProxyPending, +}: ProxyPanelProps) { const { t } = useTranslation(); const { status, isRunning } = useProxyStatus(); @@ -183,9 +199,98 @@ export function ProxyPanel() { return ( <> -
+
+ {/* [1] Enable proxy button on main page — always visible */} + } + title={t("settings.advanced.proxy.enableFeature")} + description={t("settings.advanced.proxy.enableFeatureDescription")} + checked={enableLocalProxy} + onCheckedChange={onEnableLocalProxyChange} + /> + + {/* [2] Proxy service toggle — always visible */} +
+
+
+ +
+
+

+ {t("proxyConfig.proxyEnabled", { + defaultValue: "代理服务", + })} +

+

+ {isRunning + ? t("settings.advanced.proxy.running") + : t("settings.advanced.proxy.stopped")} +

+
+
+ +
+ + {/* [3] App takeover switches — animated, visible only when proxy is running */} + + {isRunning && ( + +
+

+ {t("proxyConfig.appTakeover", { + defaultValue: "应用接管", + })} +

+
+ {(["claude", "codex", "gemini"] as const).map((appType) => { + const isEnabled = + takeoverStatus?.[ + appType as keyof typeof takeoverStatus + ] ?? false; + return ( +
+ + {appType} + + + handleTakeoverChange(appType, checked) + } + disabled={setTakeoverForApp.isPending} + /> +
+ ); + })} +
+

+ {t("proxy.takeover.hint", { + defaultValue: + "选择要接管的应用,启用后该应用的请求将通过本地代理转发", + })} +

+
+
+ )} +
+ + {/* Running state: service info + stats */} {isRunning && status ? (
+ {/* [4] Running info: address + current provider */}

@@ -263,41 +368,7 @@ export function ProxyPanel() { )}

- {/* 应用接管开关 */} -
-

- {t("proxyConfig.appTakeover", { - defaultValue: "应用接管", - })} -

-
- {(["claude", "codex", "gemini"] as const).map((appType) => { - const isEnabled = - takeoverStatus?.[ - appType as keyof typeof takeoverStatus - ] ?? false; - return ( -
- - {appType} - - - handleTakeoverChange(appType, checked) - } - disabled={setTakeoverForApp.isPending} - /> -
- ); - })} -
-
- - {/* 日志记录开关 */} + {/* [5] Logging toggle */}
@@ -320,7 +391,7 @@ export function ProxyPanel() {
- {/* 供应商队列 - 按应用类型分组展示 */} + {/* [6] Provider queues */} {(claudeQueue.length > 0 || codexQueue.length > 0 || geminiQueue.length > 0) && ( @@ -332,7 +403,6 @@ export function ProxyPanel() {

- {/* Claude 队列 */} {claudeQueue.length > 0 && ( )} - {/* Codex 队列 */} {codexQueue.length > 0 && ( )} - {/* Gemini 队列 */} {geminiQueue.length > 0 && ( + {/* [7] Stats cards */}
} @@ -408,7 +477,7 @@ export function ProxyPanel() {
) : (
- {/* 基础设置 - 监听地址/端口 */} + {/* [8] Basic settings — address/port (only when stopped) */}

@@ -496,7 +565,7 @@ export function ProxyPanel() {

- {/* 代理服务已停止提示 */} + {/* Stopped hint */}
@@ -508,7 +577,7 @@ export function ProxyPanel() {

{t("proxy.panel.stoppedDescription", { - defaultValue: "使用右上角开关即可启动服务", + defaultValue: "使用上方开关即可启动服务", })}

diff --git a/src/components/settings/ProxyTabContent.tsx b/src/components/settings/ProxyTabContent.tsx index 825f34b3e..3d99aae35 100644 --- a/src/components/settings/ProxyTabContent.tsx +++ b/src/components/settings/ProxyTabContent.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { Server, Activity, ChevronDown, Zap, Globe } from "lucide-react"; +import { Server, Activity, Zap, Globe } from "lucide-react"; import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; import { @@ -10,8 +9,6 @@ import { AccordionTrigger, } from "@/components/ui/accordion"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Switch } from "@/components/ui/switch"; -import { ToggleRow } from "@/components/ui/toggle-row"; import { Badge } from "@/components/ui/badge"; import { ProxyPanel } from "@/components/proxy"; import { AutoFailoverConfigPanel } from "@/components/proxy/AutoFailoverConfigPanel"; @@ -76,28 +73,22 @@ export function ProxyTabContent({ {/* Local Proxy */} - - -
- -
-

- {t("settings.advanced.proxy.title")} -

-

- {t("settings.advanced.proxy.description")} -

-
+ +
+ +
+

+ {t("settings.advanced.proxy.title")} +

+

+ {t("settings.advanced.proxy.description")} +

- - - -
-
- + - } - title={t("settings.advanced.proxy.enableFeature")} - description={t( - "settings.advanced.proxy.enableFeatureDescription", - )} - checked={settings?.enableLocalProxy ?? false} - onCheckedChange={(checked) => + onAutoSave({ enableLocalProxy: checked }) } + onToggleProxy={handleToggleProxy} + isProxyPending={isProxyPending} /> -
- -
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6ed487f3d..fccbaa456 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1582,7 +1582,7 @@ "currentProvider": "Current Provider:", "waitingFirstRequest": "Current Provider: Waiting for first request...", "stoppedTitle": "Proxy Service Stopped", - "stoppedDescription": "Use the toggle in the top right to start the service", + "stoppedDescription": "Use the toggle above to start the service", "openSettings": "Configure Proxy Service", "stats": { "activeConnections": "Active Connections", @@ -1673,6 +1673,12 @@ "restartRequired": "Restart proxy service for address or port changes to take effect" }, "switchFailed": "Switch failed: {{error}}", + "takeover": { + "hint": "Select apps to take over — once enabled, requests from that app will be routed through the local proxy", + "enabled": "{{app}} takeover enabled", + "disabled": "{{app}} takeover disabled", + "failed": "Failed to toggle takeover" + }, "failover": { "proxyRequired": "Proxy service must be started to configure failover", "autoSwitch": "Auto Failover", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 0b985a535..f6b5508d6 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1582,7 +1582,7 @@ "currentProvider": "現在のプロバイダー:", "waitingFirstRequest": "現在のプロバイダー: 最初のリクエスト待ち...", "stoppedTitle": "プロキシサービス停止中", - "stoppedDescription": "右上のトグルでサービスを開始できます", + "stoppedDescription": "上のトグルでサービスを開始できます", "openSettings": "プロキシサービスを設定", "stats": { "activeConnections": "アクティブ接続", @@ -1673,6 +1673,12 @@ "restartRequired": "アドレスまたはポートの変更を反映するにはプロキシサービスの再起動が必要です" }, "switchFailed": "切り替えに失敗しました: {{error}}", + "takeover": { + "hint": "テイクオーバーするアプリを選択します。有効にすると、そのアプリのリクエストはローカルプロキシ経由で転送されます", + "enabled": "{{app}} テイクオーバー有効", + "disabled": "{{app}} テイクオーバー無効", + "failed": "テイクオーバーの切り替えに失敗しました" + }, "failover": { "proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります", "autoSwitch": "自動フェイルオーバー", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d4351bdf6..0fe3901c3 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1582,7 +1582,7 @@ "currentProvider": "当前 Provider:", "waitingFirstRequest": "当前 Provider:等待首次请求…", "stoppedTitle": "代理服务已停止", - "stoppedDescription": "使用右上角开关即可启动服务", + "stoppedDescription": "使用上方开关即可启动服务", "openSettings": "配置代理服务", "stats": { "activeConnections": "活跃连接", @@ -1673,6 +1673,12 @@ "restartRequired": "修改地址或端口后需要重启代理服务才能生效" }, "switchFailed": "切换失败: {{error}}", + "takeover": { + "hint": "选择要接管的应用,启用后该应用的请求将通过本地代理转发", + "enabled": "{{app}} 接管已启用", + "disabled": "{{app}} 接管已关闭", + "failed": "切换接管状态失败" + }, "failover": { "proxyRequired": "需要先启动代理服务才能配置故障转移", "autoSwitch": "自动故障转移",