mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-17 10:29:11 +08:00
feat(settings): add first-run confirmation dialogs for proxy and usage features
Prevent accidental activation of advanced features by showing a one-time info dialog. Once confirmed, the flag is persisted in settings.json and the dialog never appears again. - Proxy: confirmation when toggling proxy server ON for the first time - Usage: confirmation when enabling usage query inside UsageScriptModal - Enhanced ConfirmDialog with "info" variant (blue icon + default button) - Added i18n translations for zh, en, ja
This commit is contained in:
@@ -190,6 +190,12 @@ pub struct AppSettings {
|
||||
/// 是否在主页面启用本地代理功能(默认关闭)
|
||||
#[serde(default)]
|
||||
pub enable_local_proxy: bool,
|
||||
/// User has confirmed the local proxy first-run notice
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub proxy_confirmed: Option<bool>,
|
||||
/// User has confirmed the usage query first-run notice
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub usage_confirmed: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<String>,
|
||||
|
||||
@@ -266,6 +272,8 @@ impl Default for AppSettings {
|
||||
launch_on_startup: false,
|
||||
silent_startup: false,
|
||||
enable_local_proxy: false,
|
||||
proxy_confirmed: None,
|
||||
usage_confirmed: None,
|
||||
language: None,
|
||||
visible_apps: None,
|
||||
claude_config_dir: None,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { AlertTriangle, Info } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
@@ -16,6 +16,7 @@ interface ConfirmDialogProps {
|
||||
message: string;
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
variant?: "destructive" | "info";
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
@@ -26,11 +27,16 @@ export function ConfirmDialog({
|
||||
message,
|
||||
confirmText,
|
||||
cancelText,
|
||||
variant = "destructive",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const IconComponent = variant === "info" ? Info : AlertTriangle;
|
||||
const iconClass =
|
||||
variant === "info" ? "h-5 w-5 text-blue-500" : "h-5 w-5 text-destructive";
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
@@ -43,7 +49,7 @@ export function ConfirmDialog({
|
||||
<DialogContent className="max-w-sm" zIndex="alert">
|
||||
<DialogHeader className="space-y-3 border-b-0 bg-transparent pb-0">
|
||||
<DialogTitle className="flex items-center gap-2 text-lg font-semibold">
|
||||
<AlertTriangle className="h-5 w-5 text-destructive" />
|
||||
<IconComponent className={iconClass} />
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="whitespace-pre-line text-sm leading-relaxed">
|
||||
@@ -54,7 +60,10 @@ export function ConfirmDialog({
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
{cancelText || t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm}>
|
||||
<Button
|
||||
variant={variant === "info" ? "default" : "destructive"}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText || t("common.confirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Provider, UsageScript, UsageData } from "@/types";
|
||||
import { usageApi, type AppId } from "@/lib/api";
|
||||
import { usageApi, settingsApi, type AppId } from "@/lib/api";
|
||||
import { useSettingsQuery } from "@/lib/query";
|
||||
import { extractCodexBaseUrl } from "@/utils/providerConfigUtils";
|
||||
import JsonEditor from "./JsonEditor";
|
||||
import * as prettier from "prettier/standalone";
|
||||
@@ -15,6 +16,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface UsageScriptModalProps {
|
||||
@@ -112,6 +114,8 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { data: settingsData } = useSettingsQuery();
|
||||
const [showUsageConfirm, setShowUsageConfirm] = useState(false);
|
||||
|
||||
// 生成带国际化的预设模板
|
||||
const PRESET_TEMPLATES = generatePresetTemplates(t);
|
||||
@@ -247,6 +251,27 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [showAccessToken, setShowAccessToken] = useState(false);
|
||||
|
||||
const handleEnableToggle = (checked: boolean) => {
|
||||
if (checked && !settingsData?.usageConfirmed) {
|
||||
setShowUsageConfirm(true);
|
||||
} else {
|
||||
setScript({ ...script, enabled: checked });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsageConfirm = async () => {
|
||||
setShowUsageConfirm(false);
|
||||
try {
|
||||
if (settingsData) {
|
||||
await settingsApi.save({ ...settingsData, usageConfirmed: true });
|
||||
await queryClient.invalidateQueries({ queryKey: ["settings"] });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to save usage confirmed:", error);
|
||||
}
|
||||
setScript({ ...script, enabled: true });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (script.enabled && !script.code.trim()) {
|
||||
toast.error(t("usageScript.scriptEmpty"));
|
||||
@@ -436,9 +461,7 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</p>
|
||||
<Switch
|
||||
checked={script.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setScript({ ...script, enabled: checked })
|
||||
}
|
||||
onCheckedChange={handleEnableToggle}
|
||||
aria-label={t("usageScript.enableUsageQuery")}
|
||||
/>
|
||||
</div>
|
||||
@@ -844,6 +867,16 @@ const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={showUsageConfirm}
|
||||
variant="info"
|
||||
title={t("confirm.usage.title")}
|
||||
message={t("confirm.usage.message")}
|
||||
confirmText={t("confirm.usage.confirm")}
|
||||
onConfirm={() => void handleUsageConfirm()}
|
||||
onCancel={() => setShowUsageConfirm(false)}
|
||||
/>
|
||||
</FullScreenPanel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { Server, Activity, ChevronDown, Zap, Globe } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
@@ -17,6 +18,7 @@ import { AutoFailoverConfigPanel } from "@/components/proxy/AutoFailoverConfigPa
|
||||
import { FailoverQueueManager } from "@/components/proxy/FailoverQueueManager";
|
||||
import { RectifierConfigPanel } from "@/components/settings/RectifierConfigPanel";
|
||||
import { GlobalProxySettings } from "@/components/settings/GlobalProxySettings";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import type { SettingsFormState } from "@/hooks/useSettings";
|
||||
|
||||
@@ -30,6 +32,7 @@ export function ProxyTabContent({
|
||||
onAutoSave,
|
||||
}: ProxyTabContentProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showProxyConfirm, setShowProxyConfirm] = useState(false);
|
||||
|
||||
const {
|
||||
isRunning,
|
||||
@@ -42,6 +45,8 @@ export function ProxyTabContent({
|
||||
try {
|
||||
if (!checked) {
|
||||
await stopWithRestore();
|
||||
} else if (!settings?.proxyConfirmed) {
|
||||
setShowProxyConfirm(true);
|
||||
} else {
|
||||
await startProxyServer();
|
||||
}
|
||||
@@ -50,6 +55,16 @@ export function ProxyTabContent({
|
||||
}
|
||||
};
|
||||
|
||||
const handleProxyConfirm = async () => {
|
||||
setShowProxyConfirm(false);
|
||||
try {
|
||||
await onAutoSave({ proxyConfirmed: true });
|
||||
await startProxyServer();
|
||||
} catch (error) {
|
||||
console.error("Proxy confirm failed:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
@@ -269,6 +284,16 @@ export function ProxyTabContent({
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={showProxyConfirm}
|
||||
variant="info"
|
||||
title={t("confirm.proxy.title")}
|
||||
message={t("confirm.proxy.message")}
|
||||
confirmText={t("confirm.proxy.confirm")}
|
||||
onConfirm={() => void handleProxyConfirm()}
|
||||
onCancel={() => setShowProxyConfirm(false)}
|
||||
/>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,7 +180,17 @@
|
||||
"deleteProvider": "Delete Provider",
|
||||
"deleteProviderMessage": "Are you sure you want to delete provider \"{{name}}\"? This action cannot be undone.",
|
||||
"removeProvider": "Remove Provider",
|
||||
"removeProviderMessage": "Are you sure you want to remove provider \"{{name}}\" from the configuration?\n\nAfter removal, this provider will no longer be active, but the configuration data will be retained in CC Switch. You can re-add it at any time."
|
||||
"removeProviderMessage": "Are you sure you want to remove provider \"{{name}}\" from the configuration?\n\nAfter removal, this provider will no longer be active, but the configuration data will be retained in CC Switch. You can re-add it at any time.",
|
||||
"proxy": {
|
||||
"title": "Enable Local Proxy",
|
||||
"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"
|
||||
},
|
||||
"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.",
|
||||
"confirm": "I understand, configure"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
|
||||
@@ -180,7 +180,17 @@
|
||||
"deleteProvider": "プロバイダーを削除",
|
||||
"deleteProviderMessage": "プロバイダー「{{name}}」を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"removeProvider": "プロバイダーを解除",
|
||||
"removeProviderMessage": "プロバイダー「{{name}}」を設定から解除してもよろしいですか?\n\n解除後、このプロバイダーは無効になりますが、設定データは CC Switch に保持されます。いつでも再追加できます。"
|
||||
"removeProviderMessage": "プロバイダー「{{name}}」を設定から解除してもよろしいですか?\n\n解除後、このプロバイダーは無効になりますが、設定データは CC Switch に保持されます。いつでも再追加できます。",
|
||||
"proxy": {
|
||||
"title": "ローカルプロキシの有効化",
|
||||
"message": "ローカルプロキシは上級機能です。有効にする前に、その仕組みを理解していることをご確認ください。\n\n適切な設定方法については、関連ドキュメントまたはプロバイダーにご相談ください。",
|
||||
"confirm": "理解しました、有効にする"
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用量クエリの設定",
|
||||
"message": "使用量クエリにはカスタムスクリプトまたは API パラメータが必要です。プロバイダーから必要な情報を取得していることをご確認ください。\n\n設定方法が不明な場合は、プロバイダーのドキュメントを先にご確認ください。",
|
||||
"confirm": "理解しました、設定する"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
|
||||
@@ -180,7 +180,17 @@
|
||||
"deleteProvider": "删除供应商",
|
||||
"deleteProviderMessage": "确定要删除供应商 \"{{name}}\" 吗?此操作无法撤销。",
|
||||
"removeProvider": "移除供应商",
|
||||
"removeProviderMessage": "确定要从配置中移除供应商 \"{{name}}\" 吗?\n\n移除后该供应商将不再生效,但配置数据会保留在 CC Switch 中,您可以随时重新添加。"
|
||||
"removeProviderMessage": "确定要从配置中移除供应商 \"{{name}}\" 吗?\n\n移除后该供应商将不再生效,但配置数据会保留在 CC Switch 中,您可以随时重新添加。",
|
||||
"proxy": {
|
||||
"title": "启用本地代理服务",
|
||||
"message": "本地代理是一项高级功能,启用前请确保您已了解其工作原理。\n\n建议先查阅相关文档或咨询您的供应商,以获取正确的配置方式。",
|
||||
"confirm": "我已了解,继续启用"
|
||||
},
|
||||
"usage": {
|
||||
"title": "配置用量查询",
|
||||
"message": "用量查询需要配置专用的查询脚本或 API 参数,请确保您已从供应商处获取相关信息。\n\n如不确定如何配置,请先查阅供应商文档。",
|
||||
"confirm": "我已了解,继续配置"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
|
||||
@@ -214,6 +214,10 @@ export interface Settings {
|
||||
silentStartup?: boolean;
|
||||
// 是否启用主页面本地代理功能(默认关闭)
|
||||
enableLocalProxy?: boolean;
|
||||
// User has confirmed the local proxy first-run notice
|
||||
proxyConfirmed?: boolean;
|
||||
// User has confirmed the usage query first-run notice
|
||||
usageConfirmed?: boolean;
|
||||
// 首选语言(可选,默认中文)
|
||||
language?: "en" | "zh" | "ja";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user