refactor(ui): redesign proxy panel with inline config controls

Replace ProxySettingsDialog with inline controls in ProxyPanel.
Add per-app takeover switches and global address/port settings.
Simplify AutoFailoverConfigPanel by removing timeout settings.
This commit is contained in:
YoVinchen
2025-12-24 23:47:07 +08:00
parent 5fcddbd096
commit 298d19af89
4 changed files with 469 additions and 732 deletions
+261 -46
View File
@@ -1,26 +1,54 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import {
Activity,
Clock,
TrendingUp,
Server,
ListOrdered,
Settings,
Save,
Loader2,
} 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 { useProxyStatus } from "@/hooks/useProxyStatus";
import { ProxySettingsDialog } from "./ProxySettingsDialog";
import { toast } from "sonner";
import { useFailoverQueue } from "@/lib/query/failover";
import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge";
import { useProviderHealth } from "@/lib/query/failover";
import {
useProxyTakeoverStatus,
useSetProxyTakeoverForApp,
useGlobalProxyConfig,
useUpdateGlobalProxyConfig,
} from "@/lib/query/proxy";
import type { ProxyStatus } from "@/types/proxy";
import { useTranslation } from "react-i18next";
export function ProxyPanel() {
const { t } = useTranslation();
const { status, isRunning } = useProxyStatus();
const [showSettings, setShowSettings] = useState(false);
// 获取应用接管状态
const { data: takeoverStatus } = useProxyTakeoverStatus();
const setTakeoverForApp = useSetProxyTakeoverForApp();
// 获取全局代理配置
const { data: globalConfig } = useGlobalProxyConfig();
const updateGlobalConfig = useUpdateGlobalProxyConfig();
// 监听地址/端口的本地状态
const [listenAddress, setListenAddress] = useState("127.0.0.1");
const [listenPort, setListenPort] = useState(5000);
// 同步全局配置到本地状态
useEffect(() => {
if (globalConfig) {
setListenAddress(globalConfig.listenAddress);
setListenPort(globalConfig.listenPort);
}
}, [globalConfig]);
// 获取所有三个应用类型的故障转移队列(不包含当前供应商)
// 当前供应商始终优先,队列仅用于失败后的备用顺序
@@ -28,6 +56,69 @@ export function ProxyPanel() {
const { data: codexQueue = [] } = useFailoverQueue("codex");
const { data: geminiQueue = [] } = useFailoverQueue("gemini");
const handleTakeoverChange = async (appType: string, enabled: boolean) => {
try {
await setTakeoverForApp.mutateAsync({ appType, enabled });
toast.success(
enabled
? t("proxy.takeover.enabled", {
app: appType,
defaultValue: `${appType} 接管已启用`,
})
: t("proxy.takeover.disabled", {
app: appType,
defaultValue: `${appType} 接管已关闭`,
}),
{ closeButton: true },
);
} catch (error) {
toast.error(
t("proxy.takeover.failed", {
defaultValue: "切换接管状态失败",
}),
);
}
};
const handleLoggingChange = async (enabled: boolean) => {
if (!globalConfig) return;
try {
await updateGlobalConfig.mutateAsync({
...globalConfig,
enableLogging: enabled,
});
toast.success(
enabled
? t("proxy.logging.enabled", { defaultValue: "日志记录已启用" })
: t("proxy.logging.disabled", { defaultValue: "日志记录已关闭" }),
{ closeButton: true },
);
} catch (error) {
toast.error(
t("proxy.logging.failed", { defaultValue: "切换日志状态失败" }),
);
}
};
const handleSaveBasicConfig = async () => {
if (!globalConfig) return;
try {
await updateGlobalConfig.mutateAsync({
...globalConfig,
listenAddress,
listenPort,
});
toast.success(
t("proxy.settings.configSaved", { defaultValue: "代理配置已保存" }),
{ closeButton: true },
);
} catch (error) {
toast.error(
t("proxy.settings.configSaveFailed", { defaultValue: "保存配置失败" }),
);
}
};
const formatUptime = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
@@ -49,22 +140,11 @@ export function ProxyPanel() {
<div className="space-y-6">
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-4">
<div>
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-muted-foreground">
{t("proxy.panel.serviceAddress", {
defaultValue: "服务地址",
})}
</p>
<Button
size="sm"
variant="ghost"
onClick={() => setShowSettings(true)}
className="h-7 gap-1.5 text-xs"
>
<Settings className="h-3.5 w-3.5" />
{t("common.settings")}
</Button>
</div>
<p className="text-xs text-muted-foreground mb-2">
{t("proxy.panel.serviceAddress", {
defaultValue: "服务地址",
})}
</p>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<code className="flex-1 text-sm bg-background px-3 py-2 rounded border border-border/60">
http://{status.address}:{status.port}
@@ -87,6 +167,11 @@ export function ProxyPanel() {
{t("common.copy")}
</Button>
</div>
<p className="text-xs text-muted-foreground mt-2">
{t("proxy.settings.restartRequired", {
defaultValue: "修改监听地址/端口需要先停止代理服务",
})}
</p>
</div>
<div className="pt-3 border-t border-border space-y-2">
@@ -130,6 +215,63 @@ 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>
{/* 日志记录开关 */}
<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">
<Label className="text-sm font-medium">
{t("proxy.settings.fields.enableLogging.label", {
defaultValue: "启用日志记录",
})}
</Label>
<p className="text-xs text-muted-foreground">
{t("proxy.settings.fields.enableLogging.description", {
defaultValue: "记录所有代理请求,便于排查问题",
})}
</p>
</div>
<Switch
checked={globalConfig?.enableLogging ?? true}
onCheckedChange={handleLoggingChange}
disabled={updateGlobalConfig.isPending}
/>
</div>
</div>
{/* 供应商队列 - 按应用类型分组展示 */}
{(claudeQueue.length > 0 ||
codexQueue.length > 0 ||
@@ -217,36 +359,109 @@ export function ProxyPanel() {
</div>
</div>
) : (
<div className="text-center py-10 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" />
<div className="space-y-6">
{/* 空白区域避免冲突 */}
<div className="h-4"></div>
{/* 基础设置 - 监听地址/端口 */}
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-4">
<div>
<h4 className="text-sm font-semibold">
{t("proxy.settings.basic.title", {
defaultValue: "基础设置",
})}
</h4>
<p className="text-xs text-muted-foreground">
{t("proxy.settings.basic.description", {
defaultValue: "配置代理服务监听的地址与端口。",
})}
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="listen-address">
{t("proxy.settings.fields.listenAddress.label", {
defaultValue: "监听地址",
})}
</Label>
<Input
id="listen-address"
value={listenAddress}
onChange={(e) => setListenAddress(e.target.value)}
placeholder="127.0.0.1"
/>
<p className="text-xs text-muted-foreground">
{t("proxy.settings.fields.listenAddress.description", {
defaultValue:
"代理服务器监听的 IP 地址(推荐 127.0.0.1",
})}
</p>
</div>
<div className="space-y-2">
<Label htmlFor="listen-port">
{t("proxy.settings.fields.listenPort.label", {
defaultValue: "监听端口",
})}
</Label>
<Input
id="listen-port"
type="number"
value={listenPort}
onChange={(e) =>
setListenPort(parseInt(e.target.value) || 5000)
}
placeholder="5000"
/>
<p className="text-xs text-muted-foreground">
{t("proxy.settings.fields.listenPort.description", {
defaultValue: "代理服务器监听的端口号(1024 ~ 65535",
})}
</p>
</div>
</div>
<div className="flex justify-end">
<Button
size="sm"
onClick={handleSaveBasicConfig}
disabled={updateGlobalConfig.isPending}
>
{updateGlobalConfig.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
{t("common.saving", { defaultValue: "保存中..." })}
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
{t("common.save", { defaultValue: "保存" })}
</>
)}
</Button>
</div>
</div>
{/* 代理服务已停止提示 */}
<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" />
</div>
<p className="text-base font-medium text-foreground mb-1">
{t("proxy.panel.stoppedTitle", {
defaultValue: "代理服务已停止",
})}
</p>
<p className="text-sm text-muted-foreground">
{t("proxy.panel.stoppedDescription", {
defaultValue: "使用右上角开关即可启动服务",
})}
</p>
</div>
<p className="text-base font-medium text-foreground mb-1">
{t("proxy.panel.stoppedTitle", {
defaultValue: "代理服务已停止",
})}
</p>
<p className="text-sm text-muted-foreground mb-4">
{t("proxy.panel.stoppedDescription", {
defaultValue: "使用右上角开关即可启动服务",
})}
</p>
<Button
size="sm"
variant="outline"
onClick={() => setShowSettings(true)}
className="gap-1.5"
>
<Settings className="h-4 w-4" />
{t("proxy.panel.openSettings", {
defaultValue: "配置代理服务",
})}
</Button>
</div>
)}
</section>
<ProxySettingsDialog open={showSettings} onOpenChange={setShowSettings} />
</>
);
}