2025-08-06 16:16:09 +08:00
|
|
|
import { useState, useEffect, useRef } from 'react'
|
2025-08-05 23:28:47 +08:00
|
|
|
import { Provider } from '../shared/types'
|
2025-08-04 22:16:26 +08:00
|
|
|
import ProviderList from './components/ProviderList'
|
|
|
|
|
import AddProviderModal from './components/AddProviderModal'
|
2025-08-05 09:51:41 +08:00
|
|
|
import EditProviderModal from './components/EditProviderModal'
|
2025-08-06 16:29:52 +08:00
|
|
|
import { ConfirmDialog } from './components/ConfirmDialog'
|
2025-08-04 22:16:26 +08:00
|
|
|
import './App.css'
|
|
|
|
|
|
|
|
|
|
function App() {
|
|
|
|
|
const [providers, setProviders] = useState<Record<string, Provider>>({})
|
|
|
|
|
const [currentProviderId, setCurrentProviderId] = useState<string>('')
|
|
|
|
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
|
|
|
|
const [configPath, setConfigPath] = useState<string>('')
|
2025-08-05 09:51:41 +08:00
|
|
|
const [editingProviderId, setEditingProviderId] = useState<string | null>(null)
|
2025-08-06 16:16:09 +08:00
|
|
|
const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' } | null>(null)
|
|
|
|
|
const [isNotificationVisible, setIsNotificationVisible] = useState(false)
|
2025-08-06 16:29:52 +08:00
|
|
|
const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void } | null>(null)
|
2025-08-06 16:16:09 +08:00
|
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
|
|
|
|
|
|
// 设置通知的辅助函数
|
|
|
|
|
const showNotification = (message: string, type: 'success' | 'error', duration = 3000) => {
|
|
|
|
|
// 清除之前的定时器
|
|
|
|
|
if (timeoutRef.current) {
|
|
|
|
|
clearTimeout(timeoutRef.current)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 立即显示通知
|
|
|
|
|
setNotification({ message, type })
|
|
|
|
|
setIsNotificationVisible(true)
|
|
|
|
|
|
|
|
|
|
// 设置淡出定时器
|
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
|
|
|
setIsNotificationVisible(false)
|
|
|
|
|
// 等待淡出动画完成后清除通知
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setNotification(null)
|
|
|
|
|
timeoutRef.current = null
|
|
|
|
|
}, 300) // 与CSS动画时间匹配
|
|
|
|
|
}, duration)
|
|
|
|
|
}
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
// 加载供应商列表
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadProviders()
|
|
|
|
|
loadConfigPath()
|
|
|
|
|
}, [])
|
|
|
|
|
|
2025-08-06 16:16:09 +08:00
|
|
|
// 清理定时器
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
return () => {
|
|
|
|
|
if (timeoutRef.current) {
|
|
|
|
|
clearTimeout(timeoutRef.current)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
const loadProviders = async () => {
|
|
|
|
|
const loadedProviders = await window.electronAPI.getProviders()
|
|
|
|
|
const currentId = await window.electronAPI.getCurrentProvider()
|
|
|
|
|
setProviders(loadedProviders)
|
|
|
|
|
setCurrentProviderId(currentId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const loadConfigPath = async () => {
|
|
|
|
|
const path = await window.electronAPI.getClaudeCodeConfigPath()
|
|
|
|
|
setConfigPath(path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-08-06 07:45:18 +08:00
|
|
|
// 生成唯一ID
|
|
|
|
|
const generateId = () => {
|
|
|
|
|
return Date.now().toString(36) + Math.random().toString(36).substr(2, 9)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
const handleAddProvider = async (provider: Omit<Provider, 'id'>) => {
|
|
|
|
|
const newProvider: Provider = {
|
|
|
|
|
...provider,
|
2025-08-06 07:45:18 +08:00
|
|
|
id: generateId()
|
2025-08-04 22:16:26 +08:00
|
|
|
}
|
|
|
|
|
await window.electronAPI.addProvider(newProvider)
|
|
|
|
|
await loadProviders()
|
|
|
|
|
setIsAddModalOpen(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeleteProvider = async (id: string) => {
|
2025-08-06 16:29:52 +08:00
|
|
|
const provider = providers[id]
|
|
|
|
|
setConfirmDialog({
|
|
|
|
|
isOpen: true,
|
|
|
|
|
title: '删除供应商',
|
|
|
|
|
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
|
|
|
|
|
onConfirm: async () => {
|
|
|
|
|
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 07:59:11 +08:00
|
|
|
const success = await window.electronAPI.switchProvider(id)
|
|
|
|
|
if (success) {
|
|
|
|
|
setCurrentProviderId(id)
|
2025-08-06 16:16:09 +08:00
|
|
|
// 显示重启提示,时间更长
|
|
|
|
|
showNotification('切换成功!请重启 Claude Code 终端以生效', 'success', 4000)
|
2025-08-06 07:59:11 +08:00
|
|
|
} else {
|
2025-08-06 16:16:09 +08:00
|
|
|
showNotification('切换失败,请检查配置', 'error')
|
2025-08-06 07:59:11 +08:00
|
|
|
}
|
2025-08-04 22:16:26 +08:00
|
|
|
}
|
|
|
|
|
|
2025-08-05 09:51:41 +08:00
|
|
|
const handleEditProvider = async (provider: Provider) => {
|
|
|
|
|
try {
|
|
|
|
|
await window.electronAPI.updateProvider(provider)
|
|
|
|
|
await loadProviders()
|
|
|
|
|
setEditingProviderId(null)
|
2025-08-06 16:16:09 +08:00
|
|
|
// 显示编辑成功提示,时间较短
|
|
|
|
|
showNotification('供应商配置已保存', 'success', 2000)
|
2025-08-05 09:51:41 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('更新供应商失败:', error)
|
2025-08-06 15:20:08 +08:00
|
|
|
setEditingProviderId(null)
|
2025-08-06 16:16:09 +08:00
|
|
|
showNotification('保存失败,请重试', 'error')
|
2025-08-05 09:51:41 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-05 20:30:18 +08:00
|
|
|
const handleSelectConfigFile = async () => {
|
|
|
|
|
const selectedPath = await window.electronAPI.selectConfigFile()
|
|
|
|
|
if (selectedPath) {
|
|
|
|
|
setConfigPath(selectedPath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-04 22:16:26 +08:00
|
|
|
return (
|
|
|
|
|
<div className="app">
|
|
|
|
|
<header className="app-header">
|
|
|
|
|
<h1>Claude Code 供应商切换器</h1>
|
|
|
|
|
<div className="header-actions">
|
|
|
|
|
<button
|
|
|
|
|
className="add-btn"
|
|
|
|
|
onClick={() => setIsAddModalOpen(true)}
|
|
|
|
|
>
|
|
|
|
|
添加供应商
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main className="app-main">
|
2025-08-06 16:16:09 +08:00
|
|
|
<div className="provider-section">
|
|
|
|
|
{/* 浮动通知组件 */}
|
|
|
|
|
{notification && (
|
|
|
|
|
<div className={`notification-floating ${notification.type === 'error' ? 'notification-error' : 'notification-success'} ${isNotificationVisible ? 'fade-in' : 'fade-out'}`}>
|
|
|
|
|
{notification.message}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<ProviderList
|
2025-08-04 22:16:26 +08:00
|
|
|
providers={providers}
|
|
|
|
|
currentProviderId={currentProviderId}
|
|
|
|
|
onSwitch={handleSwitchProvider}
|
|
|
|
|
onDelete={handleDeleteProvider}
|
2025-08-05 09:51:41 +08:00
|
|
|
onEdit={setEditingProviderId}
|
2025-08-04 22:16:26 +08:00
|
|
|
/>
|
2025-08-06 16:16:09 +08:00
|
|
|
</div>
|
2025-08-04 22:16:26 +08:00
|
|
|
|
|
|
|
|
{configPath && (
|
|
|
|
|
<div className="config-path">
|
2025-08-05 20:30:18 +08:00
|
|
|
<span>配置文件位置: {configPath}</span>
|
|
|
|
|
<button
|
|
|
|
|
className="browse-btn"
|
|
|
|
|
onClick={handleSelectConfigFile}
|
|
|
|
|
title="浏览选择配置文件"
|
|
|
|
|
>
|
|
|
|
|
浏览
|
|
|
|
|
</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>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default App
|