2025-08-06 20:48:03 +08:00
|
|
|
import { useState, useEffect, useRef } from "react";
|
2025-08-23 23:11:39 +08:00
|
|
|
import { Provider } from "./types";
|
2025-08-06 20:48:03 +08:00
|
|
|
import ProviderList from "./components/ProviderList";
|
|
|
|
|
import AddProviderModal from "./components/AddProviderModal";
|
|
|
|
|
import EditProviderModal from "./components/EditProviderModal";
|
|
|
|
|
import { ConfirmDialog } from "./components/ConfirmDialog";
|
|
|
|
|
import "./App.css";
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
function App() {
|
2025-08-06 20:48:03 +08:00
|
|
|
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
|
|
|
|
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
|
|
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
|
|
|
|
const [configPath, setConfigPath] = useState<string>("");
|
|
|
|
|
const [editingProviderId, setEditingProviderId] = useState<string | null>(
|
|
|
|
|
null
|
|
|
|
|
);
|
|
|
|
|
const [notification, setNotification] = useState<{
|
|
|
|
|
message: string;
|
|
|
|
|
type: "success" | "error";
|
|
|
|
|
} | null>(null);
|
|
|
|
|
const [isNotificationVisible, setIsNotificationVisible] = useState(false);
|
|
|
|
|
const [confirmDialog, setConfirmDialog] = useState<{
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
title: string;
|
|
|
|
|
message: string;
|
|
|
|
|
onConfirm: () => void;
|
|
|
|
|
} | null>(null);
|
|
|
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
2025-08-06 16:16:09 +08:00
|
|
|
|
|
|
|
|
// 设置通知的辅助函数
|
2025-08-06 20:48:03 +08:00
|
|
|
const showNotification = (
|
|
|
|
|
message: string,
|
|
|
|
|
type: "success" | "error",
|
|
|
|
|
duration = 3000
|
|
|
|
|
) => {
|
2025-08-06 16:16:09 +08:00
|
|
|
// 清除之前的定时器
|
|
|
|
|
if (timeoutRef.current) {
|
2025-08-06 20:48:03 +08:00
|
|
|
clearTimeout(timeoutRef.current);
|
2025-08-06 16:16:09 +08:00
|
|
|
}
|
2025-08-06 20:48:03 +08:00
|
|
|
|
2025-08-06 16:16:09 +08:00
|
|
|
// 立即显示通知
|
2025-08-06 20:48:03 +08:00
|
|
|
setNotification({ message, type });
|
|
|
|
|
setIsNotificationVisible(true);
|
|
|
|
|
|
2025-08-06 16:16:09 +08:00
|
|
|
// 设置淡出定时器
|
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
2025-08-06 20:48:03 +08:00
|
|
|
setIsNotificationVisible(false);
|
2025-08-06 16:16:09 +08:00
|
|
|
// 等待淡出动画完成后清除通知
|
|
|
|
|
setTimeout(() => {
|
2025-08-06 20:48:03 +08:00
|
|
|
setNotification(null);
|
|
|
|
|
timeoutRef.current = null;
|
|
|
|
|
}, 300); // 与CSS动画时间匹配
|
|
|
|
|
}, duration);
|
|
|
|
|
};
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
// 加载供应商列表
|
|
|
|
|
useEffect(() => {
|
2025-08-06 20:48:03 +08:00
|
|
|
loadProviders();
|
|
|
|
|
loadConfigPath();
|
|
|
|
|
}, []);
|
2025-08-04 22:16:26 +08:00
|
|
|
|
2025-08-06 16:16:09 +08:00
|
|
|
// 清理定时器
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (timeoutRef.current) {
|
2025-08-06 20:48:03 +08:00
|
|
|
clearTimeout(timeoutRef.current);
|
2025-08-06 16:16:09 +08:00
|
|
|
}
|
2025-08-06 20:48:03 +08:00
|
|
|
};
|
|
|
|
|
}, []);
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
const loadProviders = async () => {
|
2025-08-06 20:48:03 +08:00
|
|
|
const loadedProviders = await window.electronAPI.getProviders();
|
|
|
|
|
const currentId = await window.electronAPI.getCurrentProvider();
|
|
|
|
|
setProviders(loadedProviders);
|
|
|
|
|
setCurrentProviderId(currentId);
|
2025-08-07 20:27:16 +08:00
|
|
|
|
2025-08-07 23:58:07 +08:00
|
|
|
// 如果供应商列表为空,尝试自动导入现有配置为"default"供应商
|
2025-08-07 20:27:16 +08:00
|
|
|
if (Object.keys(loadedProviders).length === 0) {
|
2025-08-07 21:28:45 +08:00
|
|
|
await handleAutoImportDefault();
|
2025-08-07 20:27:16 +08:00
|
|
|
}
|
2025-08-06 20:48:03 +08:00
|
|
|
};
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
const loadConfigPath = async () => {
|
2025-08-06 20:48:03 +08:00
|
|
|
const path = await window.electronAPI.getClaudeCodeConfigPath();
|
|
|
|
|
setConfigPath(path);
|
|
|
|
|
};
|
2025-08-04 22:16:26 +08:00
|
|
|
|
2025-08-06 07:45:18 +08:00
|
|
|
// 生成唯一ID
|
|
|
|
|
const generateId = () => {
|
2025-08-07 23:58:07 +08:00
|
|
|
return crypto.randomUUID();
|
2025-08-06 20:48:03 +08:00
|
|
|
};
|
2025-08-06 07:45:18 +08:00
|
|
|
|
2025-08-06 20:48:03 +08:00
|
|
|
const handleAddProvider = async (provider: Omit<Provider, "id">) => {
|
2025-08-04 22:16:26 +08:00
|
|
|
const newProvider: Provider = {
|
|
|
|
|
...provider,
|
2025-08-06 20:48:03 +08:00
|
|
|
id: generateId(),
|
|
|
|
|
};
|
|
|
|
|
await window.electronAPI.addProvider(newProvider);
|
|
|
|
|
await loadProviders();
|
|
|
|
|
setIsAddModalOpen(false);
|
|
|
|
|
};
|
2025-08-04 22:16:26 +08:00
|
|
|
|
2025-08-07 23:58:07 +08:00
|
|
|
const handleEditProvider = async (provider: Provider) => {
|
|
|
|
|
try {
|
|
|
|
|
await window.electronAPI.updateProvider(provider);
|
|
|
|
|
await loadProviders();
|
|
|
|
|
setEditingProviderId(null);
|
|
|
|
|
// 显示编辑成功提示
|
|
|
|
|
showNotification("供应商配置已保存", "success", 2000);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("更新供应商失败:", error);
|
|
|
|
|
setEditingProviderId(null);
|
|
|
|
|
showNotification("保存失败,请重试", "error");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
const handleDeleteProvider = async (id: string) => {
|
2025-08-06 20:48:03 +08:00
|
|
|
const provider = providers[id];
|
2025-08-06 16:29:52 +08:00
|
|
|
setConfirmDialog({
|
|
|
|
|
isOpen: true,
|
2025-08-06 20:48:03 +08:00
|
|
|
title: "删除供应商",
|
2025-08-06 16:29:52 +08:00
|
|
|
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
|
|
|
|
|
onConfirm: async () => {
|
2025-08-06 20:48:03 +08:00
|
|
|
await window.electronAPI.deleteProvider(id);
|
|
|
|
|
await loadProviders();
|
|
|
|
|
setConfirmDialog(null);
|
|
|
|
|
showNotification("供应商删除成功", "success");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
const handleSwitchProvider = async (id: string) => {
|
2025-08-06 20:48:03 +08:00
|
|
|
const success = await window.electronAPI.switchProvider(id);
|
2025-08-06 07:59:11 +08:00
|
|
|
if (success) {
|
2025-08-06 20:48:03 +08:00
|
|
|
setCurrentProviderId(id);
|
|
|
|
|
// 显示重启提示
|
|
|
|
|
showNotification(
|
|
|
|
|
"切换成功!请重启 Claude Code 终端以生效",
|
|
|
|
|
"success",
|
|
|
|
|
2000
|
|
|
|
|
);
|
2025-08-06 07:59:11 +08:00
|
|
|
} else {
|
2025-08-06 20:48:03 +08:00
|
|
|
showNotification("切换失败,请检查配置", "error");
|
2025-08-06 07:59:11 +08:00
|
|
|
}
|
2025-08-06 20:48:03 +08:00
|
|
|
};
|
2025-08-04 22:16:26 +08:00
|
|
|
|
2025-08-07 23:58:07 +08:00
|
|
|
// 自动导入现有配置为"default"供应商
|
2025-08-07 21:28:45 +08:00
|
|
|
const handleAutoImportDefault = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await window.electronAPI.importCurrentConfigAsDefault()
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
await loadProviders()
|
2025-08-07 23:58:07 +08:00
|
|
|
showNotification("已自动导入现有配置为 default 供应商", "success", 3000)
|
2025-08-07 21:28:45 +08:00
|
|
|
}
|
|
|
|
|
// 如果导入失败(比如没有现有配置),静默处理,不显示错误
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('自动导入默认配置失败:', error)
|
|
|
|
|
// 静默处理,不影响用户体验
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-07 22:08:47 +08:00
|
|
|
const handleOpenConfigFolder = async () => {
|
|
|
|
|
await window.electronAPI.openConfigFolder();
|
2025-08-06 20:48:03 +08:00
|
|
|
};
|
2025-08-05 20:30:18 +08:00
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
return (
|
|
|
|
|
<div className="app">
|
|
|
|
|
<header className="app-header">
|
|
|
|
|
<h1>Claude Code 供应商切换器</h1>
|
|
|
|
|
<div className="header-actions">
|
2025-08-06 20:48:03 +08:00
|
|
|
<button className="add-btn" onClick={() => setIsAddModalOpen(true)}>
|
2025-08-04 22:16:26 +08:00
|
|
|
添加供应商
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main className="app-main">
|
2025-08-06 16:16:09 +08:00
|
|
|
<div className="provider-section">
|
|
|
|
|
{/* 浮动通知组件 */}
|
|
|
|
|
{notification && (
|
2025-08-06 20:48:03 +08:00
|
|
|
<div
|
|
|
|
|
className={`notification-floating ${
|
|
|
|
|
notification.type === "error"
|
|
|
|
|
? "notification-error"
|
|
|
|
|
: "notification-success"
|
|
|
|
|
} ${isNotificationVisible ? "fade-in" : "fade-out"}`}
|
|
|
|
|
>
|
2025-08-06 16:16:09 +08:00
|
|
|
{notification.message}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-08-06 20:48:03 +08:00
|
|
|
|
2025-08-06 16:16:09 +08:00
|
|
|
<ProviderList
|
2025-08-06 20:48:03 +08:00
|
|
|
providers={providers}
|
|
|
|
|
currentProviderId={currentProviderId}
|
|
|
|
|
onSwitch={handleSwitchProvider}
|
|
|
|
|
onDelete={handleDeleteProvider}
|
|
|
|
|
onEdit={setEditingProviderId}
|
|
|
|
|
/>
|
2025-08-06 16:16:09 +08:00
|
|
|
</div>
|
2025-08-06 20:48:03 +08:00
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
{configPath && (
|
|
|
|
|
<div className="config-path">
|
2025-08-05 20:30:18 +08:00
|
|
|
<span>配置文件位置: {configPath}</span>
|
2025-08-06 20:48:03 +08:00
|
|
|
<button
|
|
|
|
|
className="browse-btn"
|
2025-08-07 22:08:47 +08:00
|
|
|
onClick={handleOpenConfigFolder}
|
|
|
|
|
title="打开配置文件夹"
|
2025-08-05 20:30:18 +08:00
|
|
|
>
|
2025-08-07 22:08:47 +08:00
|
|
|
打开
|
2025-08-05 20:30:18 +08:00
|
|
|
</button>
|
2025-08-04 22:16:26 +08:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
{isAddModalOpen && (
|
|
|
|
|
<AddProviderModal
|
|
|
|
|
onAdd={handleAddProvider}
|
|
|
|
|
onClose={() => setIsAddModalOpen(false)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-08-05 09:51:41 +08:00
|
|
|
|
|
|
|
|
{editingProviderId && providers[editingProviderId] && (
|
|
|
|
|
<EditProviderModal
|
|
|
|
|
provider={providers[editingProviderId]}
|
|
|
|
|
onSave={handleEditProvider}
|
|
|
|
|
onClose={() => setEditingProviderId(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-08-06 16:29:52 +08:00
|
|
|
|
|
|
|
|
{confirmDialog && (
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
isOpen={confirmDialog.isOpen}
|
|
|
|
|
title={confirmDialog.title}
|
|
|
|
|
message={confirmDialog.message}
|
|
|
|
|
onConfirm={confirmDialog.onConfirm}
|
|
|
|
|
onCancel={() => setConfirmDialog(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-08-04 22:16:26 +08:00
|
|
|
</div>
|
2025-08-06 20:48:03 +08:00
|
|
|
);
|
2025-08-04 22:16:26 +08:00
|
|
|
}
|
|
|
|
|
|
2025-08-06 20:48:03 +08:00
|
|
|
export default App;
|