import { useMemo, useState, useEffect, useRef } from "react"; import { GripVertical, ChevronDown, ChevronUp } from "lucide-react"; import { useTranslation } from "react-i18next"; import type { DraggableAttributes, DraggableSyntheticListeners, } from "@dnd-kit/core"; import type { Provider } from "@/types"; import type { AppId } from "@/lib/api"; import { cn } from "@/lib/utils"; import { ProviderActions } from "@/components/providers/ProviderActions"; import { ProviderIcon } from "@/components/ProviderIcon"; import UsageFooter from "@/components/UsageFooter"; import { ProviderHealthBadge } from "@/components/providers/ProviderHealthBadge"; import { FailoverPriorityBadge } from "@/components/providers/FailoverPriorityBadge"; import { useProviderHealth } from "@/lib/query/failover"; import { useUsageQuery } from "@/lib/query/queries"; interface DragHandleProps { attributes: DraggableAttributes; listeners: DraggableSyntheticListeners; isDragging: boolean; } interface ProviderCardProps { provider: Provider; isCurrent: boolean; appId: AppId; isInConfig?: boolean; // OpenCode: 是否已添加到 opencode.json isOmo?: boolean; isLastOmo?: boolean; onSwitch: (provider: Provider) => void; onEdit: (provider: Provider) => void; onDelete: (provider: Provider) => void; onRemoveFromConfig?: (provider: Provider) => void; onDisableOmo?: () => void; onConfigureUsage: (provider: Provider) => void; onOpenWebsite: (url: string) => void; onDuplicate: (provider: Provider) => void; onTest?: (provider: Provider) => void; onOpenTerminal?: (provider: Provider) => void; isTesting?: boolean; isProxyRunning: boolean; isProxyTakeover?: boolean; // 代理接管模式(Live配置已被接管,切换为热切换) dragHandleProps?: DragHandleProps; isAutoFailoverEnabled?: boolean; // 是否开启自动故障转移 failoverPriority?: number; // 故障转移优先级(1 = P1, 2 = P2, ...) isInFailoverQueue?: boolean; // 是否在故障转移队列中 onToggleFailover?: (enabled: boolean) => void; // 切换故障转移队列 activeProviderId?: string; // 代理当前实际使用的供应商 ID(用于故障转移模式下标注绿色边框) } const extractApiUrl = (provider: Provider, fallbackText: string) => { if (provider.notes?.trim()) { return provider.notes.trim(); } if (provider.websiteUrl) { return provider.websiteUrl; } const config = provider.settingsConfig; if (config && typeof config === "object") { const envBase = (config as Record)?.env?.ANTHROPIC_BASE_URL || (config as Record)?.env?.GOOGLE_GEMINI_BASE_URL; if (typeof envBase === "string" && envBase.trim()) { return envBase; } const baseUrl = (config as Record)?.config; if (typeof baseUrl === "string" && baseUrl.includes("base_url")) { const match = baseUrl.match(/base_url\s*=\s*['"]([^'"]+)['"]/); if (match?.[1]) { return match[1]; } } } return fallbackText; }; export function ProviderCard({ provider, isCurrent, appId, isInConfig = true, isOmo = false, isLastOmo = false, onSwitch, onEdit, onDelete, onRemoveFromConfig, onDisableOmo, onConfigureUsage, onOpenWebsite, onDuplicate, onTest, onOpenTerminal, isTesting, isProxyRunning, isProxyTakeover = false, dragHandleProps, isAutoFailoverEnabled = false, failoverPriority, isInFailoverQueue = false, onToggleFailover, activeProviderId, }: ProviderCardProps) { const { t } = useTranslation(); const { data: health } = useProviderHealth(provider.id, appId); const fallbackUrlText = t("provider.notConfigured", { defaultValue: "未配置接口地址", }); const displayUrl = useMemo(() => { return extractApiUrl(provider, fallbackUrlText); }, [provider, fallbackUrlText]); const isClickableUrl = useMemo(() => { if (provider.notes?.trim()) { return false; } if (displayUrl === fallbackUrlText) { return false; } return true; }, [provider.notes, displayUrl, fallbackUrlText]); const usageEnabled = provider.meta?.usage_script?.enabled ?? false; const shouldAutoQuery = appId === "opencode" ? isInConfig : isCurrent; const autoQueryInterval = shouldAutoQuery ? provider.meta?.usage_script?.autoQueryInterval || 0 : 0; const { data: usage } = useUsageQuery(provider.id, appId, { enabled: usageEnabled, autoQueryInterval, }); const hasMultiplePlans = usage?.success && usage.data && usage.data.length > 1; const [isExpanded, setIsExpanded] = useState(false); const actionsRef = useRef(null); const [actionsWidth, setActionsWidth] = useState(0); useEffect(() => { if (hasMultiplePlans) { setIsExpanded(true); } }, [hasMultiplePlans]); useEffect(() => { if (actionsRef.current) { const updateWidth = () => { const width = actionsRef.current?.offsetWidth || 0; setActionsWidth(width); }; updateWidth(); window.addEventListener("resize", updateWidth); return () => window.removeEventListener("resize", updateWidth); } }, [onTest, onOpenTerminal]); // 按钮数量可能变化时重新计算 const handleOpenWebsite = () => { if (!isClickableUrl) { return; } onOpenWebsite(displayUrl); }; const isActiveProvider = isOmo ? isCurrent : appId === "opencode" ? false : isAutoFailoverEnabled ? activeProviderId === provider.id : isCurrent; const shouldUseGreen = !isOmo && isProxyTakeover && isActiveProvider; const shouldUseBlue = (isOmo && isActiveProvider) || (!isOmo && !isProxyTakeover && isActiveProvider); return (

{provider.name}

{isOmo && ( OMO )} {isProxyRunning && isInFailoverQueue && health && ( )} {isAutoFailoverEnabled && isInFailoverQueue && failoverPriority && ( )} {provider.category === "third_party" && provider.meta?.isPartner && ( )}
{displayUrl && ( )}
{hasMultiplePlans ? (
{t("usage.multiplePlans", { count: usage?.data?.length || 0, defaultValue: `${usage?.data?.length || 0} 个套餐`, })}
) : ( )} {hasMultiplePlans && ( )}
onSwitch(provider)} onEdit={() => onEdit(provider)} onDuplicate={() => onDuplicate(provider)} onTest={onTest ? () => onTest(provider) : undefined} onConfigureUsage={() => onConfigureUsage(provider)} onDelete={() => onDelete(provider)} onRemoveFromConfig={ onRemoveFromConfig ? () => onRemoveFromConfig(provider) : undefined } onDisableOmo={onDisableOmo} onOpenTerminal={ onOpenTerminal ? () => onOpenTerminal(provider) : undefined } isAutoFailoverEnabled={isAutoFailoverEnabled} isInFailoverQueue={isInFailoverQueue} onToggleFailover={onToggleFailover} />
{isExpanded && hasMultiplePlans && (
)}
); }