/** * 故障转移队列管理组件 * * 允许用户管理代理模式下的故障转移队列,支持: * - 拖拽排序 * - 添加/移除供应商 * - 启用/禁用队列项 */ import { useState, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { CSS } from "@dnd-kit/utilities"; import { DndContext, closestCenter } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { toast } from "sonner"; import { GripVertical, Plus, Trash2, Loader2, Info, AlertTriangle, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import type { FailoverQueueItem } from "@/types/proxy"; import type { AppId } from "@/lib/api"; import { useFailoverQueue, useAvailableProvidersForFailover, useAddToFailoverQueue, useRemoveFromFailoverQueue, useReorderFailoverQueue, useSetFailoverItemEnabled, } from "@/lib/query/failover"; interface FailoverQueueManagerProps { appType: AppId; disabled?: boolean; } export function FailoverQueueManager({ appType, disabled = false, }: FailoverQueueManagerProps) { const { t } = useTranslation(); const [selectedProviderId, setSelectedProviderId] = useState(""); // 查询数据 const { data: queue, isLoading: isQueueLoading, error: queueError, } = useFailoverQueue(appType); const { data: availableProviders, isLoading: isProvidersLoading } = useAvailableProvidersForFailover(appType); // Mutations const addToQueue = useAddToFailoverQueue(); const removeFromQueue = useRemoveFromFailoverQueue(); const reorderQueue = useReorderFailoverQueue(); const setItemEnabled = useSetFailoverItemEnabled(); // 拖拽配置 const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); // 排序后的队列 const sortedQueue = useMemo(() => { if (!queue) return []; return [...queue].sort((a, b) => a.queueOrder - b.queueOrder); }, [queue]); // 处理拖拽结束 const handleDragEnd = useCallback( async (event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id || !sortedQueue) return; const oldIndex = sortedQueue.findIndex( (item) => item.providerId === active.id, ); const newIndex = sortedQueue.findIndex( (item) => item.providerId === over.id, ); if (oldIndex === -1 || newIndex === -1) return; const reordered = arrayMove(sortedQueue, oldIndex, newIndex); const providerIds = reordered.map((item) => item.providerId); try { await reorderQueue.mutateAsync({ appType, providerIds }); toast.success( t("proxy.failoverQueue.reorderSuccess", "队列顺序已更新"), { closeButton: true }, ); } catch (error) { toast.error( t("proxy.failoverQueue.reorderFailed", "更新顺序失败") + ": " + String(error), ); } }, [sortedQueue, appType, reorderQueue, t], ); // 添加供应商到队列 const handleAddProvider = async () => { if (!selectedProviderId) return; try { await addToQueue.mutateAsync({ appType, providerId: selectedProviderId, }); setSelectedProviderId(""); toast.success( t("proxy.failoverQueue.addSuccess", "已添加到故障转移队列"), { closeButton: true }, ); } catch (error) { toast.error( t("proxy.failoverQueue.addFailed", "添加失败") + ": " + String(error), ); } }; // 从队列移除供应商 const handleRemoveProvider = async (providerId: string) => { try { await removeFromQueue.mutateAsync({ appType, providerId }); toast.success( t("proxy.failoverQueue.removeSuccess", "已从故障转移队列移除"), { closeButton: true }, ); } catch (error) { toast.error( t("proxy.failoverQueue.removeFailed", "移除失败") + ": " + String(error), ); } }; // 切换启用状态 const handleToggleEnabled = async (providerId: string, enabled: boolean) => { try { await setItemEnabled.mutateAsync({ appType, providerId, enabled }); } catch (error) { toast.error( t("proxy.failoverQueue.toggleFailed", "状态更新失败") + ": " + String(error), ); } }; if (isQueueLoading) { return (
); } if (queueError) { return ( {String(queueError)} ); } return (
{/* 说明信息 */} {t( "proxy.failoverQueue.info", "当前激活的供应商始终优先。当请求失败时,系统会按队列顺序依次尝试其他供应商。", )} {/* 添加供应商 */}
{/* 队列列表 */} {sortedQueue.length === 0 ? (

{t( "proxy.failoverQueue.empty", "故障转移队列为空。添加供应商以启用自动故障转移。", )}

) : ( item.providerId)} strategy={verticalListSortingStrategy} >
{sortedQueue.map((item, index) => ( ))}
)} {/* 队列说明 */} {sortedQueue.length > 0 && (

{t( "proxy.failoverQueue.dragHint", "拖拽供应商可调整故障转移顺序,序号越小优先级越高。", )}

)}
); } interface SortableQueueItemProps { item: FailoverQueueItem; index: number; disabled: boolean; onToggleEnabled: (providerId: string, enabled: boolean) => void; onRemove: (providerId: string) => void; isRemoving: boolean; isToggling: boolean; } function SortableQueueItem({ item, index, disabled, onToggleEnabled, onRemove, isRemoving, isToggling, }: SortableQueueItemProps) { const { t } = useTranslation(); const { setNodeRef, attributes, listeners, transform, transition, isDragging, } = useSortable({ id: item.providerId, disabled }); const style = { transform: CSS.Transform.toString(transform), transition, }; return (
{/* 拖拽手柄 */} {/* 序号 */}
{index + 1}
{/* 供应商名称 */}
{item.providerName}
{/* 启用开关 */} onToggleEnabled(item.providerId, checked)} disabled={disabled || isToggling} aria-label={t("proxy.failoverQueue.toggleEnabled", "启用/禁用")} /> {/* 删除按钮 */}
); }