feat: show failover toggle independently on main page with confirm dialog

Add enableFailoverToggle setting to control failover toggle visibility
on the main page, decoupled from proxy takeover state. First-time
enable shows a ConfirmDialog (same pattern as proxy toggle). The toggle
row is placed in the Auto Failover accordion section in settings.
This commit is contained in:
Jason
2026-03-08 22:25:48 +08:00
parent 6d078e7f33
commit 032a8203fd
7 changed files with 79 additions and 13 deletions

View File

@@ -199,6 +199,12 @@ pub struct AppSettings {
/// User has confirmed the stream check first-run notice
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stream_check_confirmed: Option<bool>,
/// Whether to show the failover toggle independently on the main page
#[serde(default)]
pub enable_failover_toggle: bool,
/// User has confirmed the failover toggle first-run notice
#[serde(default, skip_serializing_if = "Option::is_none")]
pub failover_confirmed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
@@ -286,6 +292,8 @@ impl Default for AppSettings {
proxy_confirmed: None,
usage_confirmed: None,
stream_check_confirmed: None,
enable_failover_toggle: false,
failover_confirmed: None,
language: None,
visible_apps: None,
claude_config_dir: None,

View File

@@ -981,23 +981,17 @@ function App() {
<div className="flex flex-1 min-w-0 items-center justify-end gap-1.5">
{currentView === "providers" &&
activeApp !== "opencode" &&
activeApp !== "openclaw" &&
settingsData?.enableLocalProxy && (
activeApp !== "openclaw" && (
<div
className="flex shrink-0 items-center gap-1.5"
style={{ WebkitAppRegion: "no-drag" } as any}
>
<ProxyToggle activeApp={activeApp} />
<div
className={cn(
"transition-all duration-300 ease-in-out overflow-hidden",
isCurrentAppTakeoverActive
? "opacity-100 max-w-[100px] scale-100"
: "opacity-0 max-w-0 scale-75 pointer-events-none",
)}
>
{settingsData?.enableLocalProxy && (
<ProxyToggle activeApp={activeApp} />
)}
{settingsData?.enableFailoverToggle && (
<FailoverToggle activeApp={activeApp} />
</div>
)}
</div>
)}
<div

View File

@@ -1,5 +1,5 @@
import { useState } from "react";
import { Server, Activity, Zap, Globe } from "lucide-react";
import { Server, Activity, Zap, Globe, ShieldAlert } from "lucide-react";
import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import {
@@ -16,6 +16,7 @@ import { FailoverQueueManager } from "@/components/proxy/FailoverQueueManager";
import { RectifierConfigPanel } from "@/components/settings/RectifierConfigPanel";
import { GlobalProxySettings } from "@/components/settings/GlobalProxySettings";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { ToggleRow } from "@/components/ui/toggle-row";
import { useProxyStatus } from "@/hooks/useProxyStatus";
import type { SettingsFormState } from "@/hooks/useSettings";
@@ -30,6 +31,7 @@ export function ProxyTabContent({
}: ProxyTabContentProps) {
const { t } = useTranslation();
const [showProxyConfirm, setShowProxyConfirm] = useState(false);
const [showFailoverConfirm, setShowFailoverConfirm] = useState(false);
const {
isRunning,
@@ -62,6 +64,23 @@ export function ProxyTabContent({
}
};
const handleFailoverToggleChange = (checked: boolean) => {
if (checked && !settings?.failoverConfirmed) {
setShowFailoverConfirm(true);
} else {
void onAutoSave({ enableFailoverToggle: checked });
}
};
const handleFailoverConfirm = async () => {
setShowFailoverConfirm(false);
try {
await onAutoSave({ failoverConfirmed: true, enableFailoverToggle: true });
} catch (error) {
console.error("Failover confirm failed:", error);
}
};
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
@@ -131,6 +150,16 @@ export function ProxyTabContent({
</AccordionTrigger>
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
<div className="space-y-6">
<ToggleRow
icon={<ShieldAlert className="h-4 w-4 text-orange-500" />}
title={t("settings.advanced.proxy.enableFailoverToggle")}
description={t(
"settings.advanced.proxy.enableFailoverToggleDescription",
)}
checked={settings?.enableFailoverToggle ?? false}
onCheckedChange={handleFailoverToggleChange}
/>
{!isRunning && (
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<p className="text-sm text-yellow-600 dark:text-yellow-400">
@@ -274,6 +303,16 @@ export function ProxyTabContent({
onConfirm={() => void handleProxyConfirm()}
onCancel={() => setShowProxyConfirm(false)}
/>
<ConfirmDialog
isOpen={showFailoverConfirm}
variant="info"
title={t("confirm.failover.title")}
message={t("confirm.failover.message")}
confirmText={t("confirm.failover.confirm")}
onConfirm={() => void handleFailoverConfirm()}
onCancel={() => setShowFailoverConfirm(false)}
/>
</motion.div>
);
}

View File

@@ -190,6 +190,11 @@
"message": "Local proxy is an advanced feature. Please make sure you understand how it works before enabling.\n\nWe recommend consulting the relevant documentation or your provider for proper configuration.",
"confirm": "I understand, enable"
},
"failover": {
"title": "Enable Failover",
"message": "Failover is an advanced feature. Please make sure you understand how it works before enabling.\n\nWe recommend configuring provider priorities in the failover queue first.",
"confirm": "I understand, enable"
},
"usage": {
"title": "Configure Usage Query",
"message": "Usage query requires a custom script or API parameters. Please make sure you have obtained the necessary information from your provider.\n\nIf unsure how to configure, please consult your provider's documentation first.",
@@ -222,6 +227,8 @@
"description": "Control proxy service toggle, view status and port info",
"enableFeature": "Show Proxy Toggle on Main Page",
"enableFeatureDescription": "When enabled, the proxy and failover toggles will appear at the top of the main page",
"enableFailoverToggle": "Show Failover Toggle on Main Page",
"enableFailoverToggleDescription": "When enabled, the failover toggle will appear independently at the top of the main page",
"running": "Running",
"stopped": "Stopped"
},

View File

@@ -190,6 +190,11 @@
"message": "ローカルプロキシは上級機能です。有効にする前に、その仕組みを理解していることをご確認ください。\n\n適切な設定方法については、関連ドキュメントまたはプロバイダーにご相談ください。",
"confirm": "理解しました、有効にする"
},
"failover": {
"title": "フェイルオーバーの有効化",
"message": "フェイルオーバーは上級機能です。有効にする前に、その仕組みを理解していることをご確認ください。\n\nフェイルオーバーキューでプロバイダーの優先順位を先に設定することをお勧めします。",
"confirm": "理解しました、有効にする"
},
"usage": {
"title": "使用量クエリの設定",
"message": "使用量クエリにはカスタムスクリプトまたは API パラメータが必要です。プロバイダーから必要な情報を取得していることをご確認ください。\n\n設定方法が不明な場合は、プロバイダーのドキュメントを先にご確認ください。",
@@ -222,6 +227,8 @@
"description": "プロキシサービスの切り替え、ステータスとポート情報を表示",
"enableFeature": "メインページにプロキシ切り替えを表示",
"enableFeatureDescription": "有効にすると、メインページ上部にプロキシとフェイルオーバーの切り替えが表示されます",
"enableFailoverToggle": "メインページにフェイルオーバー切り替えを表示",
"enableFailoverToggleDescription": "有効にすると、メインページ上部にフェイルオーバー切り替えが独立して表示されます",
"running": "実行中",
"stopped": "停止中"
},

View File

@@ -190,6 +190,11 @@
"message": "本地代理是一项高级功能,启用前请确保您已了解其工作原理。\n\n建议先查阅相关文档或咨询您的供应商以获取正确的配置方式。",
"confirm": "我已了解,继续启用"
},
"failover": {
"title": "启用故障转移功能",
"message": "故障转移是一项高级功能,启用前请确保您已了解其工作原理。\n\n建议先在故障转移队列中配置好供应商优先级。",
"confirm": "我已了解,继续启用"
},
"usage": {
"title": "配置用量查询",
"message": "用量查询需要配置专用的查询脚本或 API 参数,请确保您已从供应商处获取相关信息。\n\n如不确定如何配置请先查阅供应商文档。",
@@ -222,6 +227,8 @@
"description": "控制代理服务开关、查看状态与端口信息",
"enableFeature": "在主页面显示本地代理开关",
"enableFeatureDescription": "开启后,主页面顶部将显示代理和故障转移开关",
"enableFailoverToggle": "在主页面显示故障转移开关",
"enableFailoverToggleDescription": "开启后,主页面顶部将独立显示故障转移开关",
"running": "运行中",
"stopped": "已停止"
},

View File

@@ -233,6 +233,10 @@ export interface Settings {
usageConfirmed?: boolean;
// User has confirmed the stream check first-run notice
streamCheckConfirmed?: boolean;
// Whether to show the failover toggle independently on the main page
enableFailoverToggle?: boolean;
// User has confirmed the failover toggle first-run notice
failoverConfirmed?: boolean;
// User has confirmed the auto-sync traffic warning
autoSyncConfirmed?: boolean;
// 首选语言(可选,默认中文)