mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
refactor: move proxy toggle into panel and surface app takeover options
Move the proxy on/off switch from the accordion header into the panel content area, placing it right above the app takeover section. This ensures users see the takeover options immediately after enabling the proxy, preventing the common pitfall of running the proxy without actually taking over any app. - Simplify accordion trigger to standard style with Badge only - Add AnimatePresence animation for takeover section reveal - Remove duplicate takeover switches from running info card - Update stoppedDescription i18n to reference "above toggle" - Add proxy.takeover.hint key in zh/en/ja
This commit is contained in:
@@ -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<void>;
|
||||
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 (
|
||||
<>
|
||||
<section className="space-y-6">
|
||||
<section className="space-y-4">
|
||||
{/* [1] Enable proxy button on main page — always visible */}
|
||||
<ToggleRow
|
||||
icon={<Zap className="h-4 w-4 text-green-500" />}
|
||||
title={t("settings.advanced.proxy.enableFeature")}
|
||||
description={t("settings.advanced.proxy.enableFeatureDescription")}
|
||||
checked={enableLocalProxy}
|
||||
onCheckedChange={onEnableLocalProxyChange}
|
||||
/>
|
||||
|
||||
{/* [2] Proxy service toggle — always visible */}
|
||||
<div className="flex items-center justify-between rounded-xl border border-border bg-card/50 p-4 transition-colors hover:bg-muted/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-background ring-1 ring-border">
|
||||
<Power className="h-4 w-4 text-green-500" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
{t("proxyConfig.proxyEnabled", {
|
||||
defaultValue: "代理服务",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isRunning
|
||||
? t("settings.advanced.proxy.running")
|
||||
: t("settings.advanced.proxy.stopped")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isRunning}
|
||||
onCheckedChange={onToggleProxy}
|
||||
disabled={isProxyPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* [3] App takeover switches — animated, visible only when proxy is running */}
|
||||
<AnimatePresence>
|
||||
{isRunning && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: "auto" }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.25, ease: "easeInOut" }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="rounded-xl border-2 border-primary/20 bg-primary/5 p-4 space-y-3">
|
||||
<p className="text-xs font-medium text-primary">
|
||||
{t("proxyConfig.appTakeover", {
|
||||
defaultValue: "应用接管",
|
||||
})}
|
||||
</p>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{(["claude", "codex", "gemini"] as const).map((appType) => {
|
||||
const isEnabled =
|
||||
takeoverStatus?.[
|
||||
appType as keyof typeof takeoverStatus
|
||||
] ?? false;
|
||||
return (
|
||||
<div
|
||||
key={appType}
|
||||
className="flex items-center justify-between rounded-md border border-primary/20 bg-background/60 px-3 py-2"
|
||||
>
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{appType}
|
||||
</span>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
handleTakeoverChange(appType, checked)
|
||||
}
|
||||
disabled={setTakeoverForApp.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.takeover.hint", {
|
||||
defaultValue:
|
||||
"选择要接管的应用,启用后该应用的请求将通过本地代理转发",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Running state: service info + stats */}
|
||||
{isRunning && status ? (
|
||||
<div className="space-y-6">
|
||||
{/* [4] Running info: address + current provider */}
|
||||
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-4">
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground mb-2">
|
||||
@@ -263,41 +368,7 @@ export function ProxyPanel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 应用接管开关 */}
|
||||
<div className="pt-3 border-t border-border space-y-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxyConfig.appTakeover", {
|
||||
defaultValue: "应用接管",
|
||||
})}
|
||||
</p>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
{(["claude", "codex", "gemini"] as const).map((appType) => {
|
||||
const isEnabled =
|
||||
takeoverStatus?.[
|
||||
appType as keyof typeof takeoverStatus
|
||||
] ?? false;
|
||||
return (
|
||||
<div
|
||||
key={appType}
|
||||
className="flex items-center justify-between rounded-md border border-border bg-background/60 px-3 py-2"
|
||||
>
|
||||
<span className="text-sm font-medium capitalize">
|
||||
{appType}
|
||||
</span>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
handleTakeoverChange(appType, checked)
|
||||
}
|
||||
disabled={setTakeoverForApp.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 日志记录开关 */}
|
||||
{/* [5] Logging toggle */}
|
||||
<div className="pt-3 border-t border-border">
|
||||
<div className="flex items-center justify-between rounded-md border border-border bg-background/60 px-3 py-2">
|
||||
<div className="space-y-0.5">
|
||||
@@ -320,7 +391,7 @@ export function ProxyPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 供应商队列 - 按应用类型分组展示 */}
|
||||
{/* [6] Provider queues */}
|
||||
{(claudeQueue.length > 0 ||
|
||||
codexQueue.length > 0 ||
|
||||
geminiQueue.length > 0) && (
|
||||
@@ -332,7 +403,6 @@ export function ProxyPanel() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Claude 队列 */}
|
||||
{claudeQueue.length > 0 && (
|
||||
<ProviderQueueGroup
|
||||
appType="claude"
|
||||
@@ -345,7 +415,6 @@ export function ProxyPanel() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Codex 队列 */}
|
||||
{codexQueue.length > 0 && (
|
||||
<ProviderQueueGroup
|
||||
appType="codex"
|
||||
@@ -358,7 +427,6 @@ export function ProxyPanel() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Gemini 队列 */}
|
||||
{geminiQueue.length > 0 && (
|
||||
<ProviderQueueGroup
|
||||
appType="gemini"
|
||||
@@ -374,6 +442,7 @@ export function ProxyPanel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* [7] Stats cards */}
|
||||
<div className="grid gap-3 md:grid-cols-4">
|
||||
<StatCard
|
||||
icon={<Activity className="h-4 w-4" />}
|
||||
@@ -408,7 +477,7 @@ export function ProxyPanel() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{/* 基础设置 - 监听地址/端口 */}
|
||||
{/* [8] Basic settings — address/port (only when stopped) */}
|
||||
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold">
|
||||
@@ -496,7 +565,7 @@ export function ProxyPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 代理服务已停止提示 */}
|
||||
{/* Stopped hint */}
|
||||
<div className="text-center py-6 text-muted-foreground">
|
||||
<div className="mx-auto w-16 h-16 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||
<Server className="h-8 w-8" />
|
||||
@@ -508,7 +577,7 @@ export function ProxyPanel() {
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("proxy.panel.stoppedDescription", {
|
||||
defaultValue: "使用右上角开关即可启动服务",
|
||||
defaultValue: "使用上方开关即可启动服务",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user