Files
CC-Switch/src/components/UsageScriptModal.tsx
T
Jason f3e7412a14 feat: complete stage 4 cleanup and code formatting
This commit completes stage 4 of the refactoring plan, focusing on cleanup
and optimization of the modernized codebase.

## Key Changes

### Code Cleanup
- Remove legacy `src/lib/styles.ts` (no longer needed)
- Remove old modal components (`ImportProgressModal.tsx`, `ProviderList.tsx`)
- Streamline `src/lib/tauri-api.ts` from 712 lines to 17 lines (-97.6%)
  - Remove global `window.api` pollution
  - Keep only event listeners (`tauriEvents.onProviderSwitched`)
  - All API calls now use modular `@/lib/api/*` layer

### Type System
- Clean up `src/vite-env.d.ts` (remove 156 lines of outdated types)
- Remove obsolete global type declarations
- All TypeScript checks pass with zero errors

### Code Formatting
- Format all source files with Prettier (82 files)
- Fix formatting issues in 15 files:
  - App.tsx and core components
  - MCP management components
  - Settings module components
  - Provider management components
  - UI components

### Documentation Updates
- Update `REFACTORING_CHECKLIST.md` with stage 4 progress
- Mark completed tasks in `REFACTORING_MASTER_PLAN.md`

## Impact

**Code Reduction:**
- Total: -1,753 lines, +384 lines (net -1,369 lines)
- tauri-api.ts: 712 → 17 lines (-97.6%)
- Removed styles.ts: -82 lines
- Removed vite-env.d.ts declarations: -156 lines

**Quality Improvements:**
-  Zero TypeScript errors
-  Zero TODO/FIXME comments
-  100% Prettier compliant
-  Zero `window.api` references
-  Fully modular API layer

## Testing
- [x] TypeScript compilation passes
- [x] Code formatting validated
- [x] No linting errors

Stage 4 completion: 100%
Ready for stage 5 (testing and bug fixes)
2025-10-16 12:13:51 +08:00

383 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from "react";
import { X, Play, Wand2 } from "lucide-react";
import { Provider, UsageScript } from "../types";
import { usageApi, type AppType } from "@/lib/api";
import JsonEditor from "./JsonEditor";
import * as prettier from "prettier/standalone";
import * as parserBabel from "prettier/parser-babel";
import * as pluginEstree from "prettier/plugins/estree";
interface UsageScriptModalProps {
provider: Provider;
appType: AppType;
onClose: () => void;
onSave: (script: UsageScript) => void;
onNotify?: (
message: string,
type: "success" | "error",
duration?: number,
) => void;
}
// 预设模板(JS 对象字面量格式)
const PRESET_TEMPLATES: Record<string, string> = {
: `({
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"
};
}
})`,
NewAPI: `({
request: {
url: "{{baseUrl}}/api/usage/token",
method: "GET",
headers: {
Authorization: "Bearer {{apiKey}}",
},
},
extractor: function (response) {
if (response.code) {
if (response.data.unlimited_quota) {
return {
planName: response.data.name,
total: -1,
used: response.data.total_used / 500000,
unit: "USD",
};
}
return {
isValid: true,
planName: response.data.name,
total: response.data.total_granted / 500000,
used: response.data.total_used / 500000,
remaining: response.data.total_available / 500000,
unit: "USD",
};
}
if (response.error) {
return {
isValid: false,
invalidMessage: response.error.message,
};
}
},
})`,
};
const UsageScriptModal: React.FC<UsageScriptModalProps> = ({
provider,
appType,
onClose,
onSave,
onNotify,
}) => {
const [script, setScript] = useState<UsageScript>(() => {
return (
provider.meta?.usage_script || {
enabled: false,
language: "javascript",
code: PRESET_TEMPLATES["通用模板"],
timeout: 10,
}
);
});
const [testing, setTesting] = useState(false);
const handleSave = () => {
// 验证脚本格式
if (script.enabled && !script.code.trim()) {
onNotify?.("脚本配置不能为空", "error");
return;
}
// 基本的 JS 语法检查(检查是否包含 return 语句)
if (script.enabled && !script.code.includes("return")) {
onNotify?.("脚本必须包含 return 语句", "error", 5000);
return;
}
onSave(script);
onClose();
onNotify?.("用量查询配置已保存", "success", 2000);
};
const handleTest = async () => {
setTesting(true);
try {
const result = await usageApi.query(provider.id, appType);
if (result.success && result.data && result.data.length > 0) {
// 显示所有套餐数据
const summary = result.data
.map((plan) => {
const planInfo = plan.planName ? `[${plan.planName}]` : "";
return `${planInfo} 剩余: ${plan.remaining} ${plan.unit}`;
})
.join(", ");
onNotify?.(`测试成功!${summary}`, "success", 3000);
} else {
onNotify?.(`测试失败: ${result.error || "无数据返回"}`, "error", 5000);
}
} catch (error: any) {
onNotify?.(`测试失败: ${error?.message || "未知错误"}`, "error", 5000);
} 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() });
onNotify?.("格式化成功", "success", 1000);
} catch (error: any) {
onNotify?.(`格式化失败: ${error?.message || "语法错误"}`, "error", 3000);
}
};
const handleUsePreset = (presetName: string) => {
const preset = PRESET_TEMPLATES[presetName];
if (preset) {
setScript({ ...script, code: preset });
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100">
- {provider.name}
</h2>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<X size={20} />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
{/* 启用开关 */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={script.enabled}
onChange={(e) =>
setScript({ ...script, enabled: e.target.checked })
}
className="w-4 h-4"
/>
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
</span>
</label>
{script.enabled && (
<>
{/* 预设模板选择 */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-900 dark:text-gray-100">
</label>
<div className="flex gap-2">
{Object.keys(PRESET_TEMPLATES).map((name) => (
<button
key={name}
onClick={() => handleUsePreset(name)}
className="px-3 py-1.5 text-xs bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
>
{name}
</button>
))}
</div>
</div>
{/* 脚本编辑器 */}
<div>
<label className="block text-sm font-medium mb-2 text-gray-900 dark:text-gray-100">
JavaScript
</label>
<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">
: <code>{"{{apiKey}}"}</code>,{" "}
<code>{"{{baseUrl}}"}</code> | extractor API
JSON
</p>
</div>
{/* 配置选项 */}
<div className="grid grid-cols-2 gap-4">
<label className="block">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
</span>
<input
type="number"
min="2"
max="30"
value={script.timeout || 10}
onChange={(e) =>
setScript({
...script,
timeout: parseInt(e.target.value),
})
}
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
/>
</label>
</div>
{/* 脚本说明 */}
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm text-gray-700 dark:text-gray-300">
<h4 className="font-medium mb-2"></h4>
<div className="space-y-3 text-xs">
<div>
<strong></strong>
<pre className="mt-1 p-2 bg-white/50 dark:bg-black/20 rounded text-[10px] overflow-x-auto">
{`({
request: {
url: "{{baseUrl}}/api/usage",
method: "POST",
headers: {
"Authorization": "Bearer {{apiKey}}",
"User-Agent": "cc-switch/1.0"
},
body: JSON.stringify({ key: "value" }) // 可选
},
extractor: function(response) {
// response 是 API 返回的 JSON 数据
return {
isValid: !response.error,
remaining: response.balance,
unit: "USD"
};
}
})`}
</pre>
</div>
<div>
<strong>extractor </strong>
<ul className="mt-1 space-y-0.5 ml-2">
<li>
<code>isValid</code>:
</li>
<li>
<code>invalidMessage</code>:
isValid false
</li>
<li>
<code>remaining</code>:
</li>
<li>
<code>unit</code>: "USD"
</li>
<li>
<code>planName</code>:
</li>
<li>
<code>total</code>:
</li>
<li>
<code>used</code>:
</li>
<li>
<code>extra</code>:
</li>
</ul>
</div>
<div className="text-gray-600 dark:text-gray-400">
<strong>💡 </strong>
<ul className="mt-1 space-y-0.5 ml-2">
<li>
<code>{"{{apiKey}}"}</code> {" "}
<code>{"{{baseUrl}}"}</code>
</li>
<li>
extractor ES2020+
</li>
<li>
<code>()</code>{" "}
</li>
</ul>
</div>
</div>
</div>
</>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex gap-2">
<button
onClick={handleTest}
disabled={!script.enabled || testing}
className="inline-flex items-center gap-2 px-4 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<Play size={14} />
{testing ? "测试中..." : "测试脚本"}
</button>
<button
onClick={handleFormat}
disabled={!script.enabled}
className="inline-flex items-center gap-2 px-4 py-2 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="格式化代码 (Prettier)"
>
<Wand2 size={14} />
</button>
</div>
<div className="flex gap-2">
<button
onClick={onClose}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
>
</button>
</div>
</div>
</div>
</div>
);
};
export default UsageScriptModal;