2025-10-15 09:15:25 +08:00
|
|
|
|
import React, { useState } from "react";
|
2025-11-10 15:42:36 +08:00
|
|
|
|
import { Play, Wand2, Eye, EyeOff } from "lucide-react";
|
2025-10-17 17:49:16 +08:00
|
|
|
|
import { toast } from "sonner";
|
2025-10-19 11:55:46 +08:00
|
|
|
|
import { useTranslation } from "react-i18next";
|
2025-11-05 21:40:06 +08:00
|
|
|
|
import { Provider, UsageScript } from "@/types";
|
2025-10-30 14:59:15 +08:00
|
|
|
|
import { usageApi, type AppId } from "@/lib/api";
|
2025-10-15 09:15:25 +08:00
|
|
|
|
import JsonEditor from "./JsonEditor";
|
|
|
|
|
|
import * as prettier from "prettier/standalone";
|
|
|
|
|
|
import * as parserBabel from "prettier/parser-babel";
|
|
|
|
|
|
import * as pluginEstree from "prettier/plugins/estree";
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Dialog,
|
|
|
|
|
|
DialogContent,
|
|
|
|
|
|
DialogHeader,
|
|
|
|
|
|
DialogTitle,
|
|
|
|
|
|
DialogFooter,
|
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2025-11-10 15:42:36 +08:00
|
|
|
|
import { Input } from "@/components/ui/input";
|
2025-11-10 15:51:18 +08:00
|
|
|
|
import { Label } from "@/components/ui/label";
|
2025-11-10 16:01:28 +08:00
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
2025-10-15 09:15:25 +08:00
|
|
|
|
|
|
|
|
|
|
interface UsageScriptModalProps {
|
|
|
|
|
|
provider: Provider;
|
2025-10-30 14:59:15 +08:00
|
|
|
|
appId: AppId;
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
isOpen: boolean;
|
2025-10-15 09:15:25 +08:00
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
onSave: (script: UsageScript) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-05 00:07:54 +08:00
|
|
|
|
// 预设模板键名(用于国际化)
|
|
|
|
|
|
const TEMPLATE_KEYS = {
|
|
|
|
|
|
CUSTOM: "custom",
|
|
|
|
|
|
GENERAL: "general",
|
|
|
|
|
|
NEW_API: "newapi",
|
|
|
|
|
|
} as const;
|
|
|
|
|
|
|
|
|
|
|
|
// 生成预设模板的函数(支持国际化)
|
2025-11-05 23:17:34 +08:00
|
|
|
|
const generatePresetTemplates = (
|
|
|
|
|
|
t: (key: string) => string,
|
|
|
|
|
|
): Record<string, string> => ({
|
2025-11-05 00:07:54 +08:00
|
|
|
|
[TEMPLATE_KEYS.CUSTOM]: `({
|
2025-11-04 21:53:42 +08:00
|
|
|
|
request: {
|
|
|
|
|
|
url: "",
|
|
|
|
|
|
method: "GET",
|
|
|
|
|
|
headers: {}
|
|
|
|
|
|
},
|
|
|
|
|
|
extractor: function(response) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
remaining: 0,
|
|
|
|
|
|
unit: "USD"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
})`,
|
|
|
|
|
|
|
2025-11-05 00:07:54 +08:00
|
|
|
|
[TEMPLATE_KEYS.GENERAL]: `({
|
2025-10-15 09:15:25 +08:00
|
|
|
|
request: {
|
|
|
|
|
|
url: "{{baseUrl}}/user/balance",
|
|
|
|
|
|
method: "GET",
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"Authorization": "Bearer {{apiKey}}",
|
|
|
|
|
|
"User-Agent": "cc-switch/1.0"
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
extractor: function(response) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
isValid: response.is_active || true,
|
|
|
|
|
|
remaining: response.balance,
|
|
|
|
|
|
unit: "USD"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
})`,
|
|
|
|
|
|
|
2025-11-05 00:07:54 +08:00
|
|
|
|
[TEMPLATE_KEYS.NEW_API]: `({
|
2025-10-15 09:15:25 +08:00
|
|
|
|
request: {
|
2025-11-04 11:30:14 +08:00
|
|
|
|
url: "{{baseUrl}}/api/user/self",
|
2025-10-15 09:15:25 +08:00
|
|
|
|
method: "GET",
|
|
|
|
|
|
headers: {
|
2025-11-04 11:30:14 +08:00
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
"Authorization": "Bearer {{accessToken}}",
|
|
|
|
|
|
"New-Api-User": "{{userId}}"
|
2025-10-15 09:15:25 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
extractor: function (response) {
|
2025-11-04 11:30:14 +08:00
|
|
|
|
if (response.success && response.data) {
|
2025-10-15 09:15:25 +08:00
|
|
|
|
return {
|
2025-11-05 00:07:54 +08:00
|
|
|
|
planName: response.data.group || "${t("usageScript.defaultPlan")}",
|
2025-11-04 11:30:14 +08:00
|
|
|
|
remaining: response.data.quota / 500000,
|
|
|
|
|
|
used: response.data.used_quota / 500000,
|
|
|
|
|
|
total: (response.data.quota + response.data.used_quota) / 500000,
|
2025-10-15 09:15:25 +08:00
|
|
|
|
unit: "USD",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-11-04 11:30:14 +08:00
|
|
|
|
return {
|
|
|
|
|
|
isValid: false,
|
2025-11-05 00:07:54 +08:00
|
|
|
|
invalidMessage: response.message || "${t("usageScript.queryFailedMessage")}"
|
2025-11-04 11:30:14 +08:00
|
|
|
|
};
|
2025-10-15 09:15:25 +08:00
|
|
|
|
},
|
|
|
|
|
|
})`,
|
2025-11-05 00:07:54 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 模板名称国际化键映射
|
|
|
|
|
|
const TEMPLATE_NAME_KEYS: Record<string, string> = {
|
|
|
|
|
|
[TEMPLATE_KEYS.CUSTOM]: "usageScript.templateCustom",
|
|
|
|
|
|
[TEMPLATE_KEYS.GENERAL]: "usageScript.templateGeneral",
|
|
|
|
|
|
[TEMPLATE_KEYS.NEW_API]: "usageScript.templateNewAPI",
|
2025-10-15 09:15:25 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-30 15:31:08 +08:00
|
|
|
|
const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
|
|
|
|
|
|
provider,
|
|
|
|
|
|
appId,
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
onSave,
|
|
|
|
|
|
}) => {
|
2025-10-19 11:55:46 +08:00
|
|
|
|
const { t } = useTranslation();
|
2025-11-05 00:07:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 生成带国际化的预设模板
|
|
|
|
|
|
const PRESET_TEMPLATES = generatePresetTemplates(t);
|
|
|
|
|
|
|
2025-10-15 09:15:25 +08:00
|
|
|
|
const [script, setScript] = useState<UsageScript>(() => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
provider.meta?.usage_script || {
|
|
|
|
|
|
enabled: false,
|
|
|
|
|
|
language: "javascript",
|
2025-11-05 00:07:54 +08:00
|
|
|
|
code: PRESET_TEMPLATES[TEMPLATE_KEYS.GENERAL],
|
2025-10-15 09:15:25 +08:00
|
|
|
|
timeout: 10,
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [testing, setTesting] = useState(false);
|
|
|
|
|
|
|
2025-11-13 11:28:48 +08:00
|
|
|
|
// 🔧 输入时的格式化(宽松)- 只清理格式,不约束范围
|
|
|
|
|
|
const sanitizeNumberInput = (value: string): string => {
|
|
|
|
|
|
// 移除所有非数字字符
|
|
|
|
|
|
let cleaned = value.replace(/[^\d]/g, "");
|
|
|
|
|
|
|
|
|
|
|
|
// 移除前导零(除非输入的就是 "0")
|
|
|
|
|
|
if (cleaned.length > 1 && cleaned.startsWith("0")) {
|
|
|
|
|
|
cleaned = cleaned.replace(/^0+/, "");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return cleaned;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 失焦时的验证(严格)- 仅确保有效整数
|
|
|
|
|
|
const validateTimeout = (value: string): number => {
|
|
|
|
|
|
// 转换为数字
|
|
|
|
|
|
const num = Number(value);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为有效数字
|
|
|
|
|
|
if (isNaN(num) || value.trim() === "") {
|
|
|
|
|
|
return 10; // 默认值
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为整数
|
|
|
|
|
|
if (!Number.isInteger(num)) {
|
|
|
|
|
|
toast.warning(
|
|
|
|
|
|
t("usageScript.timeoutMustBeInteger") || "超时时间必须为整数",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查负数
|
|
|
|
|
|
if (num < 0) {
|
|
|
|
|
|
toast.error(
|
|
|
|
|
|
t("usageScript.timeoutCannotBeNegative") || "超时时间不能为负数",
|
|
|
|
|
|
);
|
|
|
|
|
|
return 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Math.floor(num);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 🔧 失焦时的验证(严格)- 自动查询间隔
|
|
|
|
|
|
const validateAndClampInterval = (value: string): number => {
|
|
|
|
|
|
// 转换为数字
|
|
|
|
|
|
const num = Number(value);
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为有效数字
|
|
|
|
|
|
if (isNaN(num) || value.trim() === "") {
|
|
|
|
|
|
return 0; // 禁用自动查询
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否为整数
|
|
|
|
|
|
if (!Number.isInteger(num)) {
|
|
|
|
|
|
toast.warning(
|
|
|
|
|
|
t("usageScript.intervalMustBeInteger") || "自动查询间隔必须为整数",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查负数
|
|
|
|
|
|
if (num < 0) {
|
|
|
|
|
|
toast.error(
|
|
|
|
|
|
t("usageScript.intervalCannotBeNegative") || "自动查询间隔不能为负数",
|
|
|
|
|
|
);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 约束到 [0, 1440] 范围(最大24小时)
|
|
|
|
|
|
const clamped = Math.max(0, Math.min(1440, Math.floor(num)));
|
|
|
|
|
|
|
|
|
|
|
|
// 如果值被调整,显示提示
|
|
|
|
|
|
if (clamped !== num && num > 0) {
|
|
|
|
|
|
toast.info(
|
|
|
|
|
|
t("usageScript.intervalAdjusted", { value: clamped }) ||
|
|
|
|
|
|
`自动查询间隔已调整为 ${clamped} 分钟`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return clamped;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-04 21:44:20 +08:00
|
|
|
|
// 跟踪当前选择的模板类型(用于控制高级配置的显示)
|
|
|
|
|
|
// 初始化:如果已有 accessToken 或 userId,说明是 NewAPI 模板
|
|
|
|
|
|
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(
|
|
|
|
|
|
() => {
|
|
|
|
|
|
const existingScript = provider.meta?.usage_script;
|
|
|
|
|
|
if (existingScript?.accessToken || existingScript?.userId) {
|
2025-11-05 00:07:54 +08:00
|
|
|
|
return TEMPLATE_KEYS.NEW_API;
|
2025-11-04 21:44:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
return null;
|
2025-11-05 23:17:34 +08:00
|
|
|
|
},
|
2025-11-04 21:44:20 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-10 15:42:36 +08:00
|
|
|
|
// 控制 API Key 的显示/隐藏
|
|
|
|
|
|
const [showApiKey, setShowApiKey] = useState(false);
|
|
|
|
|
|
const [showAccessToken, setShowAccessToken] = useState(false);
|
|
|
|
|
|
|
2025-10-15 09:15:25 +08:00
|
|
|
|
const handleSave = () => {
|
|
|
|
|
|
// 验证脚本格式
|
|
|
|
|
|
if (script.enabled && !script.code.trim()) {
|
2025-10-19 11:55:46 +08:00
|
|
|
|
toast.error(t("usageScript.scriptEmpty"));
|
2025-10-15 09:15:25 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 基本的 JS 语法检查(检查是否包含 return 语句)
|
|
|
|
|
|
if (script.enabled && !script.code.includes("return")) {
|
2025-10-19 11:55:46 +08:00
|
|
|
|
toast.error(t("usageScript.mustHaveReturn"), { duration: 5000 });
|
2025-10-15 09:15:25 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onSave(script);
|
|
|
|
|
|
onClose();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleTest = async () => {
|
|
|
|
|
|
setTesting(true);
|
|
|
|
|
|
try {
|
2025-11-04 23:04:44 +08:00
|
|
|
|
// 使用当前编辑器中的脚本内容进行测试
|
|
|
|
|
|
const result = await usageApi.testScript(
|
|
|
|
|
|
provider.id,
|
|
|
|
|
|
appId,
|
|
|
|
|
|
script.code,
|
|
|
|
|
|
script.timeout,
|
2025-11-10 15:28:09 +08:00
|
|
|
|
script.apiKey,
|
|
|
|
|
|
script.baseUrl,
|
2025-11-04 23:04:44 +08:00
|
|
|
|
script.accessToken,
|
2025-11-05 23:17:34 +08:00
|
|
|
|
script.userId,
|
2025-11-04 23:04:44 +08:00
|
|
|
|
);
|
2025-10-15 09:15:25 +08:00
|
|
|
|
if (result.success && result.data && result.data.length > 0) {
|
|
|
|
|
|
// 显示所有套餐数据
|
|
|
|
|
|
const summary = result.data
|
|
|
|
|
|
.map((plan) => {
|
|
|
|
|
|
const planInfo = plan.planName ? `[${plan.planName}]` : "";
|
2025-10-19 11:55:46 +08:00
|
|
|
|
return `${planInfo} ${t("usage.remaining")} ${plan.remaining} ${plan.unit}`;
|
2025-10-15 09:15:25 +08:00
|
|
|
|
})
|
|
|
|
|
|
.join(", ");
|
2025-10-24 13:02:35 +08:00
|
|
|
|
toast.success(`${t("usageScript.testSuccess")}${summary}`, {
|
|
|
|
|
|
duration: 3000,
|
2025-10-17 17:49:16 +08:00
|
|
|
|
});
|
2025-10-24 13:02:35 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
toast.error(
|
|
|
|
|
|
`${t("usageScript.testFailed")}: ${result.error || t("endpointTest.noResult")}`,
|
|
|
|
|
|
{
|
|
|
|
|
|
duration: 5000,
|
2025-11-05 23:17:34 +08:00
|
|
|
|
},
|
2025-10-24 13:02:35 +08:00
|
|
|
|
);
|
2025-10-15 09:15:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
2025-10-24 13:02:35 +08:00
|
|
|
|
toast.error(
|
|
|
|
|
|
`${t("usageScript.testFailed")}: ${error?.message || t("common.unknown")}`,
|
|
|
|
|
|
{
|
|
|
|
|
|
duration: 5000,
|
2025-11-05 23:17:34 +08:00
|
|
|
|
},
|
2025-10-24 13:02:35 +08:00
|
|
|
|
);
|
2025-10-15 09:15:25 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setTesting(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleFormat = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formatted = await prettier.format(script.code, {
|
|
|
|
|
|
parser: "babel",
|
|
|
|
|
|
plugins: [parserBabel as any, pluginEstree as any],
|
|
|
|
|
|
semi: true,
|
|
|
|
|
|
singleQuote: false,
|
|
|
|
|
|
tabWidth: 2,
|
|
|
|
|
|
printWidth: 80,
|
|
|
|
|
|
});
|
|
|
|
|
|
setScript({ ...script, code: formatted.trim() });
|
2025-10-19 11:55:46 +08:00
|
|
|
|
toast.success(t("usageScript.formatSuccess"), { duration: 1000 });
|
2025-10-15 09:15:25 +08:00
|
|
|
|
} catch (error: any) {
|
2025-10-24 13:02:35 +08:00
|
|
|
|
toast.error(
|
|
|
|
|
|
`${t("usageScript.formatFailed")}: ${error?.message || t("jsonEditor.invalidJson")}`,
|
|
|
|
|
|
{
|
|
|
|
|
|
duration: 3000,
|
2025-11-05 23:17:34 +08:00
|
|
|
|
},
|
2025-10-24 13:02:35 +08:00
|
|
|
|
);
|
2025-10-15 09:15:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleUsePreset = (presetName: string) => {
|
|
|
|
|
|
const preset = PRESET_TEMPLATES[presetName];
|
|
|
|
|
|
if (preset) {
|
2025-11-10 15:28:09 +08:00
|
|
|
|
// 根据模板类型清空不同的字段
|
|
|
|
|
|
if (presetName === TEMPLATE_KEYS.CUSTOM) {
|
|
|
|
|
|
// 自定义:清空所有凭证字段
|
2025-11-04 21:44:20 +08:00
|
|
|
|
setScript({
|
|
|
|
|
|
...script,
|
|
|
|
|
|
code: preset,
|
2025-11-10 15:28:09 +08:00
|
|
|
|
apiKey: undefined,
|
|
|
|
|
|
baseUrl: undefined,
|
2025-11-04 21:44:20 +08:00
|
|
|
|
accessToken: undefined,
|
|
|
|
|
|
userId: undefined,
|
|
|
|
|
|
});
|
2025-11-10 15:28:09 +08:00
|
|
|
|
} else if (presetName === TEMPLATE_KEYS.GENERAL) {
|
|
|
|
|
|
// 通用:保留 apiKey 和 baseUrl,清空 NewAPI 字段
|
|
|
|
|
|
setScript({
|
|
|
|
|
|
...script,
|
|
|
|
|
|
code: preset,
|
|
|
|
|
|
accessToken: undefined,
|
|
|
|
|
|
userId: undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (presetName === TEMPLATE_KEYS.NEW_API) {
|
|
|
|
|
|
// NewAPI:清空 apiKey(NewAPI 不使用通用的 apiKey)
|
|
|
|
|
|
setScript({
|
|
|
|
|
|
...script,
|
|
|
|
|
|
code: preset,
|
|
|
|
|
|
apiKey: undefined,
|
|
|
|
|
|
});
|
2025-11-04 21:44:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
setSelectedTemplate(presetName); // 记录选择的模板
|
2025-10-15 09:15:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-10 15:28:09 +08:00
|
|
|
|
// 判断是否应该显示凭证配置区域
|
|
|
|
|
|
const shouldShowCredentialsConfig =
|
2025-11-12 10:47:34 +08:00
|
|
|
|
selectedTemplate === TEMPLATE_KEYS.GENERAL ||
|
|
|
|
|
|
selectedTemplate === TEMPLATE_KEYS.NEW_API;
|
2025-11-04 21:44:20 +08:00
|
|
|
|
|
2025-10-15 09:15:25 +08:00
|
|
|
|
return (
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
|
|
|
|
|
<DialogContent className="max-w-3xl max-h-[90vh] flex flex-col">
|
|
|
|
|
|
<DialogHeader>
|
2025-10-24 13:02:35 +08:00
|
|
|
|
<DialogTitle>
|
|
|
|
|
|
{t("usageScript.title")} - {provider.name}
|
|
|
|
|
|
</DialogTitle>
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
</DialogHeader>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
{/* Content - Scrollable */}
|
2025-10-18 16:52:02 +08:00
|
|
|
|
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
|
2025-10-15 09:15:25 +08:00
|
|
|
|
{/* 启用开关 */}
|
2025-11-10 16:01:28 +08:00
|
|
|
|
<div className="flex items-center justify-between gap-4 rounded-lg border border-border-default p-4">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<p className="text-sm font-medium leading-none">
|
|
|
|
|
|
{t("usageScript.enableUsageQuery")}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Switch
|
2025-10-15 09:15:25 +08:00
|
|
|
|
checked={script.enabled}
|
2025-11-10 16:01:28 +08:00
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
|
setScript({ ...script, enabled: checked })
|
2025-10-15 09:15:25 +08:00
|
|
|
|
}
|
2025-11-10 16:01:28 +08:00
|
|
|
|
aria-label={t("usageScript.enableUsageQuery")}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
/>
|
2025-11-10 16:01:28 +08:00
|
|
|
|
</div>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
|
|
|
|
|
|
{script.enabled && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* 预设模板选择 */}
|
|
|
|
|
|
<div>
|
2025-11-10 15:51:18 +08:00
|
|
|
|
<Label className="mb-2">
|
2025-10-19 11:55:46 +08:00
|
|
|
|
{t("usageScript.presetTemplate")}
|
2025-11-10 15:51:18 +08:00
|
|
|
|
</Label>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<div className="flex gap-2">
|
2025-11-04 21:58:25 +08:00
|
|
|
|
{Object.keys(PRESET_TEMPLATES).map((name) => {
|
|
|
|
|
|
const isSelected = selectedTemplate === name;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={name}
|
|
|
|
|
|
onClick={() => handleUsePreset(name)}
|
|
|
|
|
|
className={`px-3 py-1.5 text-xs rounded transition-colors ${
|
|
|
|
|
|
isSelected
|
|
|
|
|
|
? "bg-blue-500 text-white dark:bg-blue-600"
|
|
|
|
|
|
: "bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
2025-11-05 00:07:54 +08:00
|
|
|
|
{t(TEMPLATE_NAME_KEYS[name])}
|
2025-11-04 21:58:25 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-11-10 15:28:09 +08:00
|
|
|
|
{/* 凭证配置区域:通用和 NewAPI 模板显示 */}
|
|
|
|
|
|
{shouldShowCredentialsConfig && (
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-4 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
|
|
|
|
|
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
2025-11-10 15:28:09 +08:00
|
|
|
|
{t("usageScript.credentialsConfig")}
|
|
|
|
|
|
</h4>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 通用模板:显示 apiKey + baseUrl */}
|
|
|
|
|
|
{selectedTemplate === TEMPLATE_KEYS.GENERAL && (
|
|
|
|
|
|
<>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-11-12 10:47:34 +08:00
|
|
|
|
<Label htmlFor="usage-api-key">API Key</Label>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="usage-api-key"
|
|
|
|
|
|
type={showApiKey ? "text" : "password"}
|
|
|
|
|
|
value={script.apiKey || ""}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setScript({ ...script, apiKey: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="sk-xxxxx"
|
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{script.apiKey && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowApiKey(!showApiKey)}
|
|
|
|
|
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
2025-11-12 10:47:34 +08:00
|
|
|
|
aria-label={
|
|
|
|
|
|
showApiKey
|
|
|
|
|
|
? t("apiKeyInput.hide")
|
|
|
|
|
|
: t("apiKeyInput.show")
|
|
|
|
|
|
}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
>
|
2025-11-12 10:47:34 +08:00
|
|
|
|
{showApiKey ? (
|
|
|
|
|
|
<EyeOff size={16} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Eye size={16} />
|
|
|
|
|
|
)}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-11-10 15:28:09 +08:00
|
|
|
|
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-11-12 10:47:34 +08:00
|
|
|
|
<Label htmlFor="usage-base-url">Base URL</Label>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
id="usage-base-url"
|
2025-11-10 15:28:09 +08:00
|
|
|
|
type="text"
|
|
|
|
|
|
value={script.baseUrl || ""}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setScript({ ...script, baseUrl: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="https://api.example.com"
|
2025-11-10 15:42:36 +08:00
|
|
|
|
autoComplete="off"
|
2025-11-10 15:28:09 +08:00
|
|
|
|
/>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
</div>
|
2025-11-10 15:28:09 +08:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* NewAPI 模板:显示 baseUrl + accessToken + userId */}
|
|
|
|
|
|
{selectedTemplate === TEMPLATE_KEYS.NEW_API && (
|
|
|
|
|
|
<>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-11-12 10:47:34 +08:00
|
|
|
|
<Label htmlFor="usage-newapi-base-url">Base URL</Label>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
id="usage-newapi-base-url"
|
2025-11-10 15:28:09 +08:00
|
|
|
|
type="text"
|
|
|
|
|
|
value={script.baseUrl || ""}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setScript({ ...script, baseUrl: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="https://api.newapi.com"
|
2025-11-10 15:42:36 +08:00
|
|
|
|
autoComplete="off"
|
2025-11-10 15:28:09 +08:00
|
|
|
|
/>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
</div>
|
2025-11-10 15:28:09 +08:00
|
|
|
|
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-11-10 15:51:18 +08:00
|
|
|
|
<Label htmlFor="usage-access-token">
|
2025-11-10 15:28:09 +08:00
|
|
|
|
{t("usageScript.accessToken")}
|
2025-11-10 15:51:18 +08:00
|
|
|
|
</Label>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="usage-access-token"
|
|
|
|
|
|
type={showAccessToken ? "text" : "password"}
|
|
|
|
|
|
value={script.accessToken || ""}
|
|
|
|
|
|
onChange={(e) =>
|
2025-11-12 10:47:34 +08:00
|
|
|
|
setScript({
|
|
|
|
|
|
...script,
|
|
|
|
|
|
accessToken: e.target.value,
|
|
|
|
|
|
})
|
2025-11-10 15:42:36 +08:00
|
|
|
|
}
|
2025-11-12 10:47:34 +08:00
|
|
|
|
placeholder={t(
|
|
|
|
|
|
"usageScript.accessTokenPlaceholder",
|
|
|
|
|
|
)}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{script.accessToken && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2025-11-12 10:47:34 +08:00
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setShowAccessToken(!showAccessToken)
|
|
|
|
|
|
}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 transition-colors"
|
2025-11-12 10:47:34 +08:00
|
|
|
|
aria-label={
|
|
|
|
|
|
showAccessToken
|
|
|
|
|
|
? t("apiKeyInput.hide")
|
|
|
|
|
|
: t("apiKeyInput.show")
|
|
|
|
|
|
}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
>
|
2025-11-12 10:47:34 +08:00
|
|
|
|
{showAccessToken ? (
|
|
|
|
|
|
<EyeOff size={16} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Eye size={16} />
|
|
|
|
|
|
)}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-11-04 21:44:20 +08:00
|
|
|
|
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-11-10 15:51:18 +08:00
|
|
|
|
<Label htmlFor="usage-user-id">
|
2025-11-10 15:28:09 +08:00
|
|
|
|
{t("usageScript.userId")}
|
2025-11-10 15:51:18 +08:00
|
|
|
|
</Label>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
id="usage-user-id"
|
2025-11-10 15:28:09 +08:00
|
|
|
|
type="text"
|
|
|
|
|
|
value={script.userId || ""}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setScript({ ...script, userId: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder={t("usageScript.userIdPlaceholder")}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
autoComplete="off"
|
2025-11-10 15:28:09 +08:00
|
|
|
|
/>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
</div>
|
2025-11-10 15:28:09 +08:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-11-04 21:44:20 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-10-15 09:15:25 +08:00
|
|
|
|
{/* 脚本编辑器 */}
|
|
|
|
|
|
<div>
|
2025-11-12 10:47:34 +08:00
|
|
|
|
<Label className="mb-2">{t("usageScript.queryScript")}</Label>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<JsonEditor
|
|
|
|
|
|
value={script.code}
|
|
|
|
|
|
onChange={(code) => setScript({ ...script, code })}
|
|
|
|
|
|
height="300px"
|
|
|
|
|
|
language="javascript"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
2025-10-19 11:55:46 +08:00
|
|
|
|
{t("usageScript.variablesHint", {
|
|
|
|
|
|
apiKey: "{{apiKey}}",
|
2025-10-24 13:02:35 +08:00
|
|
|
|
baseUrl: "{{baseUrl}}",
|
2025-10-19 11:55:46 +08:00
|
|
|
|
})}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 配置选项 */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-11-10 15:51:18 +08:00
|
|
|
|
<Label htmlFor="usage-timeout">
|
2025-10-19 11:55:46 +08:00
|
|
|
|
{t("usageScript.timeoutSeconds")}
|
2025-11-10 15:51:18 +08:00
|
|
|
|
</Label>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
id="usage-timeout"
|
2025-10-15 09:15:25 +08:00
|
|
|
|
type="number"
|
2025-11-13 11:28:48 +08:00
|
|
|
|
value={script.timeout ?? ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
// 输入时:只清理格式,允许临时为空,避免强制回填默认值
|
|
|
|
|
|
const cleaned = sanitizeNumberInput(e.target.value);
|
|
|
|
|
|
setScript((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
timeout:
|
|
|
|
|
|
cleaned === "" ? undefined : parseInt(cleaned, 10),
|
|
|
|
|
|
}));
|
|
|
|
|
|
}}
|
|
|
|
|
|
onBlur={(e) => {
|
|
|
|
|
|
// 失焦时:严格验证并约束范围
|
|
|
|
|
|
const validated = validateTimeout(e.target.value);
|
|
|
|
|
|
setScript({ ...script, timeout: validated });
|
|
|
|
|
|
}}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
/>
|
2025-11-13 11:28:48 +08:00
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
|
{t("usageScript.timeoutHint") || "范围: 2-30 秒"}
|
|
|
|
|
|
</p>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
</div>
|
2025-11-05 15:48:19 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 🆕 自动查询间隔 */}
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<div className="space-y-2">
|
2025-11-10 15:51:18 +08:00
|
|
|
|
<Label htmlFor="usage-auto-interval">
|
2025-11-05 15:48:19 +08:00
|
|
|
|
{t("usageScript.autoQueryInterval")}
|
2025-11-10 15:51:18 +08:00
|
|
|
|
</Label>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<Input
|
|
|
|
|
|
id="usage-auto-interval"
|
2025-11-05 15:48:19 +08:00
|
|
|
|
type="number"
|
2025-11-10 15:42:36 +08:00
|
|
|
|
min={0}
|
|
|
|
|
|
max={1440}
|
|
|
|
|
|
step={1}
|
2025-11-13 11:28:48 +08:00
|
|
|
|
value={script.autoQueryInterval ?? ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
// 输入时:只清理格式,允许临时为空
|
|
|
|
|
|
const cleaned = sanitizeNumberInput(e.target.value);
|
|
|
|
|
|
setScript((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
autoQueryInterval:
|
|
|
|
|
|
cleaned === "" ? undefined : parseInt(cleaned, 10),
|
|
|
|
|
|
}));
|
|
|
|
|
|
}}
|
|
|
|
|
|
onBlur={(e) => {
|
|
|
|
|
|
// 失焦时:严格验证并约束范围
|
|
|
|
|
|
const validated = validateAndClampInterval(
|
|
|
|
|
|
e.target.value,
|
|
|
|
|
|
);
|
|
|
|
|
|
setScript({ ...script, autoQueryInterval: validated });
|
|
|
|
|
|
}}
|
2025-11-05 15:48:19 +08:00
|
|
|
|
/>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
<p className="text-xs text-muted-foreground">
|
2025-11-05 15:48:19 +08:00
|
|
|
|
{t("usageScript.autoQueryIntervalHint")}
|
|
|
|
|
|
</p>
|
2025-11-10 15:42:36 +08:00
|
|
|
|
</div>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 脚本说明 */}
|
|
|
|
|
|
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm text-gray-700 dark:text-gray-300">
|
2025-10-24 13:02:35 +08:00
|
|
|
|
<h4 className="font-medium mb-2">
|
|
|
|
|
|
{t("usageScript.scriptHelp")}
|
|
|
|
|
|
</h4>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<div className="space-y-3 text-xs">
|
|
|
|
|
|
<div>
|
2025-10-19 11:55:46 +08:00
|
|
|
|
<strong>{t("usageScript.configFormat")}</strong>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<pre className="mt-1 p-2 bg-white/50 dark:bg-black/20 rounded text-[10px] overflow-x-auto">
|
2025-10-16 12:13:51 +08:00
|
|
|
|
{`({
|
2025-10-15 09:15:25 +08:00
|
|
|
|
request: {
|
|
|
|
|
|
url: "{{baseUrl}}/api/usage",
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"Authorization": "Bearer {{apiKey}}",
|
|
|
|
|
|
"User-Agent": "cc-switch/1.0"
|
|
|
|
|
|
},
|
2025-11-05 00:07:54 +08:00
|
|
|
|
body: JSON.stringify({ key: "value" }) // ${t("usageScript.commentOptional")}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
},
|
|
|
|
|
|
extractor: function(response) {
|
2025-11-05 00:07:54 +08:00
|
|
|
|
// ${t("usageScript.commentResponseIsJson")}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
return {
|
|
|
|
|
|
isValid: !response.error,
|
|
|
|
|
|
remaining: response.balance,
|
|
|
|
|
|
unit: "USD"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
})`}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
2025-10-19 11:55:46 +08:00
|
|
|
|
<strong>{t("usageScript.extractorFormat")}</strong>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<ul className="mt-1 space-y-0.5 ml-2">
|
2025-10-19 11:55:46 +08:00
|
|
|
|
<li>{t("usageScript.fieldIsValid")}</li>
|
|
|
|
|
|
<li>{t("usageScript.fieldInvalidMessage")}</li>
|
|
|
|
|
|
<li>{t("usageScript.fieldRemaining")}</li>
|
|
|
|
|
|
<li>{t("usageScript.fieldUnit")}</li>
|
|
|
|
|
|
<li>{t("usageScript.fieldPlanName")}</li>
|
|
|
|
|
|
<li>{t("usageScript.fieldTotal")}</li>
|
|
|
|
|
|
<li>{t("usageScript.fieldUsed")}</li>
|
|
|
|
|
|
<li>{t("usageScript.fieldExtra")}</li>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="text-gray-600 dark:text-gray-400">
|
2025-10-19 11:55:46 +08:00
|
|
|
|
<strong>{t("usageScript.tips")}</strong>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<ul className="mt-1 space-y-0.5 ml-2">
|
2025-10-24 13:02:35 +08:00
|
|
|
|
<li>
|
|
|
|
|
|
{t("usageScript.tip1", {
|
|
|
|
|
|
apiKey: "{{apiKey}}",
|
|
|
|
|
|
baseUrl: "{{baseUrl}}",
|
|
|
|
|
|
})}
|
|
|
|
|
|
</li>
|
2025-10-19 11:55:46 +08:00
|
|
|
|
<li>{t("usageScript.tip2")}</li>
|
|
|
|
|
|
<li>{t("usageScript.tip3")}</li>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
<DialogFooter className="flex-col sm:flex-row sm:justify-between gap-3 pt-4">
|
|
|
|
|
|
{/* Left side - Test and Format buttons */}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<div className="flex gap-2">
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
2025-10-15 09:15:25 +08:00
|
|
|
|
onClick={handleTest}
|
|
|
|
|
|
disabled={!script.enabled || testing}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Play size={14} />
|
2025-10-19 11:55:46 +08:00
|
|
|
|
{testing ? t("usageScript.testing") : t("usageScript.testScript")}
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
2025-10-15 09:15:25 +08:00
|
|
|
|
onClick={handleFormat}
|
|
|
|
|
|
disabled={!script.enabled}
|
2025-10-19 11:55:46 +08:00
|
|
|
|
title={t("usageScript.format")}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Wand2 size={14} />
|
2025-10-19 11:55:46 +08:00
|
|
|
|
{t("usageScript.format")}
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
</Button>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
{/* Right side - Cancel and Save buttons */}
|
2025-10-15 09:15:25 +08:00
|
|
|
|
<div className="flex gap-2">
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
<Button variant="ghost" size="sm" onClick={onClose}>
|
2025-10-19 11:55:46 +08:00
|
|
|
|
{t("common.cancel")}
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
</Button>
|
|
|
|
|
|
<Button variant="default" size="sm" onClick={handleSave}>
|
2025-10-19 11:55:46 +08:00
|
|
|
|
{t("usageScript.saveConfig")}
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
</Button>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
</div>
|
refactor: migrate UsageScriptModal to shadcn/ui Dialog component
Migrate the usage script configuration modal from custom modal implementation to shadcn/ui Dialog component to maintain consistent styling across the entire application.
## Changes
### UsageScriptModal.tsx
- Replace custom modal structure (fixed positioning, backdrop) with Dialog component
- Remove X icon import (Dialog includes built-in close button)
- Add Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter imports
- Add Button component import for action buttons
- Update props interface to include isOpen boolean prop
- Restructure component layout:
- Use DialogHeader with DialogTitle for header section
- Apply -mx-6 px-6 pattern for full-width scrollable content
- Use DialogFooter with flex-col sm:flex-row sm:justify-between layout
- Convert custom buttons to Button components:
- Test/Format buttons: variant="outline" size="sm"
- Cancel button: variant="ghost" size="sm"
- Save button: variant="default" size="sm"
- Maintain all existing functionality (preset templates, JSON editor, validation, testing, formatting)
### App.tsx
- Update UsageScriptModal usage to pass isOpen prop
- Use Boolean(usageProvider) to control dialog open state
## Benefits
- **Consistent styling**: All dialogs now use the same shadcn/ui Dialog component
- **Better accessibility**: Automatic focus management, ESC key handling, ARIA attributes
- **Code maintainability**: Reduced custom modal boilerplate, easier to update styling globally
- **User experience**: Unified look and feel across settings, providers, MCP, and usage script dialogs
All TypeScript type checks and Prettier formatting checks pass.
2025-10-16 16:32:50 +08:00
|
|
|
|
</DialogFooter>
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
2025-10-15 09:15:25 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default UsageScriptModal;
|