import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { useTranslation } from '@/hooks/useTranslation'; import { Toast, ToastContainer } from "@/components/ui/toast"; import { RelayStation, CreateRelayStationRequest, UpdateRelayStationRequest, RelayStationAdapter, AuthMethod, PackycodeUserQuota, api } from '@/lib/api'; import { Plus, Edit, Trash2, Globe, Server, ArrowLeft, Settings, RefreshCw } from 'lucide-react'; interface RelayStationManagerProps { onBack: () => void; } const RelayStationManager: React.FC = ({ onBack }) => { const [stations, setStations] = useState([]); const [loading, setLoading] = useState(false); const [selectedStation, setSelectedStation] = useState(null); const [showCreateDialog, setShowCreateDialog] = useState(false); const [showEditDialog, setShowEditDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [stationToDelete, setStationToDelete] = useState(null); const [togglingEnable, setTogglingEnable] = useState>({}); const [currentConfig, setCurrentConfig] = useState>({}); const [loadingConfig, setLoadingConfig] = useState(false); const [toast, setToast] = useState<{ message: string; type: "success" | "error" } | null>(null); // PackyCode 额度相关状态 const [quotaData, setQuotaData] = useState>({}); const [loadingQuota, setLoadingQuota] = useState>({}); const { t } = useTranslation(); // 显示Toast const showToast = (message: string, type: "success" | "error" = "success") => { setToast({ message, type }); }; // 加载中转站列表 const loadStations = async () => { try { setLoading(true); const stationList = await api.relayStationsList(); setStations(stationList); } catch (error) { console.error('Failed to load stations:', error); showToast(t('relayStation.loadFailed'), "error"); } finally { setLoading(false); } }; // 加载当前配置状态 const loadCurrentConfig = async () => { try { setLoadingConfig(true); const config = await api.relayStationGetCurrentConfig(); setCurrentConfig(config); } catch (error) { console.error('Failed to load current config:', error); } finally { setLoadingConfig(false); } }; // 手动同步配置 const syncConfig = async () => { try { const result = await api.relayStationSyncConfig(); showToast(result, "success"); loadCurrentConfig(); } catch (error) { console.error('Failed to sync config:', error); showToast(t('relayStation.syncFailed'), "error"); } }; // 查询 PackyCode 额度 const fetchPackycodeQuota = async (stationId: string) => { try { setLoadingQuota(prev => ({ ...prev, [stationId]: true })); const quota = await api.getPackycodeUserQuota(stationId); setQuotaData(prev => ({ ...prev, [stationId]: quota })); } catch (error) { console.error('Failed to fetch PackyCode quota:', error); // 不显示错误 Toast,因为可能是出租车服务或 Token 无效 } finally { setLoadingQuota(prev => ({ ...prev, [stationId]: false })); } }; // 删除中转站 const deleteStation = async () => { if (!stationToDelete) return; try { await api.relayStationDelete(stationToDelete.id); loadStations(); setShowDeleteDialog(false); setStationToDelete(null); showToast(t('relayStation.deleteSuccess'), "success"); } catch (error) { console.error('Failed to delete station:', error); showToast(t('relayStation.deleteFailed'), "error"); } }; // 打开删除确认对话框 const openDeleteDialog = (station: RelayStation) => { setStationToDelete(station); setShowDeleteDialog(true); }; // 获取适配器类型显示名称 const getAdapterDisplayName = (adapter: RelayStationAdapter): string => { switch (adapter) { case 'packycode': return 'PackyCode'; case 'newapi': return 'NewAPI'; case 'oneapi': return 'OneAPI'; case 'yourapi': return 'YourAPI'; case 'custom': return t('relayStation.custom'); default: return adapter; } }; // 切换启用状态 const toggleEnableStatus = async (stationId: string, currentEnabled: boolean) => { try { setTogglingEnable(prev => ({ ...prev, [stationId]: true })); const newEnabled = !currentEnabled; await api.relayStationToggleEnable(stationId, newEnabled); showToast(newEnabled ? t('relayStation.enabledSuccess') : t('relayStation.disabledSuccess'), "success"); loadStations(); loadCurrentConfig(); // 重新加载配置状态 } catch (error) { console.error('Failed to toggle enable status:', error); showToast(t('relayStation.toggleEnableFailed'), "error"); } finally { setTogglingEnable(prev => ({ ...prev, [stationId]: false })); } }; // 获取状态样式 const getStatusBadge = (station: RelayStation) => { const enabled = station.enabled; const isToggling = togglingEnable[station.id]; return (
toggleEnableStatus(station.id, enabled)} className="data-[state=checked]:bg-green-500" /> {isToggling ? ( {t('common.updating')} ) : enabled ? ( {t('status.enabled')} ) : ( {t('status.disabled')} )}
); }; useEffect(() => { loadStations(); loadCurrentConfig(); }, []); // 当中转站加载完成后,自动获取所有 PackyCode 站点的额度 useEffect(() => { stations.forEach(station => { if (station.adapter === 'packycode') { fetchPackycodeQuota(station.id); } }); }, [stations]); return (
{/* 页面标题 */}

{t('navigation.relayStations')}

{t('relayStation.description')}

{ setShowCreateDialog(false); loadStations(); showToast(t('relayStation.createSuccess'), "success"); }} />
{/* 当前配置状态 */}
{t('relayStation.currentConfig')}
API URL: {currentConfig.api_url || t('relayStation.notConfigured')}
API Token: {currentConfig.api_token || t('relayStation.notConfigured')}
{t('relayStation.configLocation')}: ~/.claude/settings.json
{/* 中转站列表 */}
{loading ? (

{t('common.loading')}

) : stations.length === 0 ? (

{t('relayStation.noStations')}

{t('relayStation.noStationsDesc')}

) : ( stations.map((station) => (
{station.name} {getAdapterDisplayName(station.adapter)}
{getStatusBadge(station)}
{station.api_url}
{station.description && (

{station.description}

)} {/* PackyCode 额度显示 */} {station.adapter === 'packycode' && (
{loadingQuota[station.id] ? (
加载额度中...
) : quotaData[station.id] ? (
{/* 用户信息和计划 */}
{quotaData[station.id].username && ( {quotaData[station.id].username} )} {quotaData[station.id].plan_type.toUpperCase()} {quotaData[station.id].opus_enabled && ( Opus )}
{quotaData[station.id].plan_expires_at && ( 到期: {new Date(quotaData[station.id].plan_expires_at).toLocaleDateString()} )}
{/* 账户余额 */}
账户余额: ${Number(quotaData[station.id].balance_usd).toFixed(2)}
{/* 日额度 */}
日额度:
{(() => { const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0); const daily_budget = Number(quotaData[station.id].daily_budget_usd); return ( <> daily_budget * 0.8 ? 'text-orange-600' : 'text-green-600'}> ${daily_spent.toFixed(2)} / ${daily_budget.toFixed(2)} ); })()}
{ const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0); const daily_budget = Number(quotaData[station.id].daily_budget_usd); return daily_spent / daily_budget > 0.8; })() ? 'bg-orange-500' : 'bg-green-500' }`} style={{ width: `${Math.min( (() => { const daily_spent = Number(quotaData[station.id].daily_spent_usd || 0); const daily_budget = Number(quotaData[station.id].daily_budget_usd); return (daily_spent / daily_budget) * 100; })(), 100)}%` }} />
{/* 月额度 */}
月额度:
{(() => { const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0); const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); return ( <> monthly_budget * 0.8 ? 'text-orange-600' : 'text-green-600'}> ${monthly_spent.toFixed(2)} / ${monthly_budget.toFixed(2)} ); })()}
{ const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0); const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); return monthly_spent / monthly_budget > 0.8; })() ? 'bg-orange-500' : 'bg-green-500' }`} style={{ width: `${Math.min( (() => { const monthly_spent = Number(quotaData[station.id].monthly_spent_usd || 0); const monthly_budget = Number(quotaData[station.id].monthly_budget_usd); return (monthly_spent / monthly_budget) * 100; })(), 100)}%` }} />
{/* 总消费 */}
总消费: ${Number(quotaData[station.id].total_spent_usd).toFixed(2)}
) : (
)}
)}
)) )}
{/* 编辑对话框 */} {selectedStation && ( { setShowEditDialog(false); setSelectedStation(null); loadStations(); showToast(t('relayStation.updateSuccess'), "success"); }} onCancel={() => { setShowEditDialog(false); setSelectedStation(null); }} /> )} {/* 删除确认对话框 */} {t('relayStation.confirmDeleteTitle')} {t('relayStation.deleteConfirm')} {stationToDelete && (
{stationToDelete.name}
)}
{/* Toast 容器 */} {toast && ( setToast(null)} /> )}
); }; // 创建中转站对话框组件 const CreateStationDialog: React.FC<{ onSuccess: () => void; }> = ({ onSuccess }) => { const [formData, setFormData] = useState({ name: '', description: '', api_url: '', adapter: 'packycode', // 默认使用 PackyCode auth_method: 'api_key', // PackyCode 默认使用 API Key system_token: '', user_id: '', enabled: false, // 默认不启用,需要通过主界面切换 }); const [submitting, setSubmitting] = useState(false); const [formToast, setFormToast] = useState<{ message: string; type: "success" | "error" } | null>(null); const [packycodeService, setPackycodeService] = useState('bus'); // 默认公交车 const [packycodeNode, setPackycodeNode] = useState('https://api.packycode.com'); // 默认节点(公交车用) const { t } = useTranslation(); // 当适配器改变时更新认证方式和 URL useEffect(() => { if (formData.adapter === 'packycode') { setFormData(prev => ({ ...prev, auth_method: 'api_key', // PackyCode 固定使用 API Key api_url: packycodeService === 'taxi' ? 'https://share-api.packycode.com' : packycodeNode })); } else if (formData.adapter === 'custom') { setFormData(prev => ({ ...prev, auth_method: 'custom' })); } else { setFormData(prev => ({ ...prev, auth_method: 'bearer_token' })); } }, [formData.adapter, packycodeService, packycodeNode]); // 自动填充中转站名称 const fillStationName = (serviceType: string) => { const serviceName = serviceType === 'taxi' ? t('relayStation.taxiService') : t('relayStation.busService'); const newName = `PackyCode ${serviceName}`; // 如果名称为空,或者当前名称是之前自动生成的PackyCode名称,则更新 if (!formData.name.trim() || formData.name.startsWith('PackyCode ') || formData.name === `PackyCode ${t('relayStation.taxiService')}` || formData.name === `PackyCode ${t('relayStation.busService')}`) { setFormData(prev => ({ ...prev, name: newName })); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.name.trim()) { setFormToast({ message: t('relayStation.nameRequired'), type: "error" }); return; } if (!formData.api_url.trim()) { setFormToast({ message: t('relayStation.apiUrlRequired'), type: "error" }); return; } if (!formData.system_token.trim()) { setFormToast({ message: t('relayStation.tokenRequired'), type: "error" }); return; } try { setSubmitting(true); await api.relayStationCreate(formData); onSuccess(); } catch (error) { console.error('Failed to create station:', error); setFormToast({ message: t('relayStation.createFailed'), type: "error" }); } finally { setSubmitting(false); } }; return ( <> {t('relayStation.createTitle')}
setFormData(prev => ({ ...prev, name: e.target.value }))} placeholder={t('relayStation.namePlaceholder')} className="w-full" />
{formData.adapter === 'packycode' && (

{packycodeService === 'taxi' ? `${t('relayStation.fixedUrl')}: https://share-api.packycode.com` : t('relayStation.busServiceNote') }

)} {formData.adapter === 'packycode' && packycodeService === 'bus' && (

{t('relayStation.selectedNode') + ': ' + packycodeNode}

)}