/** * 账号表格组件 * 支持拖拽排序功能,用户可以通过拖拽行来调整账号顺序 */ import { useMemo, useState } from 'react'; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, DragStartEvent, DragOverlay, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { GripVertical, ArrowRightLeft, RefreshCw, Trash2, Download, Fingerprint, Info, Lock, Ban, Diamond, Gem, Circle, ToggleLeft, ToggleRight, Sparkles, Tag, X, Check, Clock, Bot, } from 'lucide-react'; import { Account } from '../../types/account'; import { useTranslation } from 'react-i18next'; import { cn } from '../../utils/cn'; import { useConfigStore } from '../../stores/useConfigStore'; import { QuotaItem } from './QuotaItem'; import { MODEL_CONFIG, sortModels } from '../../config/modelConfig'; import { getValidationBlockedStatusLabel } from './accountValidationStatus'; // ============================================================================ // 类型定义 // ============================================================================ interface AccountTableProps { accounts: Account[]; selectedIds: Set; refreshingIds: Set; onToggleSelect: (id: string) => void; onToggleAll: () => void; currentAccountId: string | null; switchingAccountId: string | null; onSwitch: (accountId: string) => void; onRefresh: (accountId: string) => void; onViewDevice: (accountId: string) => void; onViewDetails: (accountId: string) => void; onExport: (accountId: string) => void; onDelete: (accountId: string) => void; onToggleProxy: (accountId: string) => void; onWarmup?: (accountId: string) => void; onUpdateLabel?: (accountId: string, label: string) => void; /** 拖拽排序回调,当用户完成拖拽时触发 */ onReorder?: (accountIds: string[]) => void; onViewError: (accountId: string) => void; } interface SortableRowProps { account: Account; selected: boolean; isRefreshing: boolean; isCurrent: boolean; isSwitching: boolean; isDragging?: boolean; onSelect: () => void; onSwitch: () => void; onRefresh: () => void; onViewDevice: () => void; onViewDetails: () => void; onExport: () => void; onDelete: () => void; onToggleProxy: () => void; onWarmup?: () => void; onUpdateLabel?: (label: string) => void; onViewError: () => void; } interface AccountRowContentProps { account: Account; isCurrent: boolean; isRefreshing: boolean; isSwitching: boolean; isDisabled: boolean; onSwitch: () => void; onRefresh: () => void; onViewDevice: () => void; onViewDetails: () => void; onExport: () => void; onDelete: () => void; onToggleProxy: () => void; onWarmup?: () => void; onUpdateLabel?: (label: string) => void; onViewError: () => void; } // ============================================================================ // 辅助函数 // ============================================================================ // ============================================================================ // 模型分组配置 // ============================================================================ const MODEL_GROUPS = { CLAUDE: [ 'claude-opus-4-6-thinking', 'claude' ], GEMINI_PRO: [ 'gemini-3.1-pro-high', 'gemini-3.1-pro-low', 'gemini-3.1-pro-preview', 'gemini-3-pro-high', 'gemini-3-pro-low', 'gemini-3-pro-preview' ], GEMINI_FLASH: [ 'gemini-3-flash' ] }; const MODEL_ID_ALIASES: Record = { 'gemini-3-pro-high': ['gemini-3-pro-high', 'gemini-3.1-pro-high'], 'gemini-3-pro-low': ['gemini-3-pro-low', 'gemini-3.1-pro-low'], 'gemini-3-pro-preview': ['gemini-3-pro-preview', 'gemini-3.1-pro-preview'], 'gemini-3.1-pro-high': ['gemini-3.1-pro-high', 'gemini-3-pro-high'], 'gemini-3.1-pro-low': ['gemini-3.1-pro-low', 'gemini-3-pro-low'], 'gemini-3.1-pro-preview': ['gemini-3.1-pro-preview', 'gemini-3-pro-preview'], }; function getModelAliases(modelId: string): string[] { return MODEL_ID_ALIASES[modelId] || [modelId]; } function isModelProtected(protectedModels: string[] | undefined, modelName: string): boolean { if (!protectedModels || protectedModels.length === 0) return false; const lowerName = modelName.toLowerCase(); // Helper to check if any model in the group is protected const isGroupProtected = (group: string[]) => { return group.some(m => protectedModels.includes(m)); }; // UI Column Keys Mapping (for backward compatibility with hardcoded UI calls) if (lowerName === 'gemini-pro') return isGroupProtected(MODEL_GROUPS.GEMINI_PRO); if (lowerName === 'gemini-flash') return isGroupProtected(MODEL_GROUPS.GEMINI_FLASH); if (lowerName === 'claude-sonnet') return isGroupProtected(MODEL_GROUPS.CLAUDE); // 1. Gemini Pro Group if (MODEL_GROUPS.GEMINI_PRO.some(m => lowerName === m)) { return isGroupProtected(MODEL_GROUPS.GEMINI_PRO); } // 2. Claude Group if (MODEL_GROUPS.CLAUDE.some(m => lowerName === m)) { return isGroupProtected(MODEL_GROUPS.CLAUDE); } // 3. Gemini Flash Group if (MODEL_GROUPS.GEMINI_FLASH.some(m => lowerName === m)) { return isGroupProtected(MODEL_GROUPS.GEMINI_FLASH); } // 兜底直接检查 (Strict check for exact match or normalized ID) return protectedModels.includes(lowerName); } // ============================================================================ // 子组件 // ============================================================================ /** * 可拖拽的表格行组件 * 使用 @dnd-kit/sortable 实现拖拽功能 */ function SortableAccountRow({ account, selected, isRefreshing, isCurrent, isSwitching, isDragging, onSelect, onSwitch, onRefresh, onViewDevice, onViewDetails, onExport, onDelete, onToggleProxy, onWarmup, onUpdateLabel, onViewError, }: SortableRowProps) { const { t } = useTranslation(); const { attributes, listeners, setNodeRef, transform, transition, isDragging: isSortableDragging, } = useSortable({ id: account.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isSortableDragging ? 0.5 : 1, zIndex: isSortableDragging ? 1000 : 'auto', }; return ( {/* 拖拽手柄 */}
{/* 复选框 */} e.stopPropagation()} /> ); } /** * 账号行内容组件 * 渲染邮箱、配额、最后使用时间和操作按钮等列 */ function AccountRowContent({ account, isCurrent, isRefreshing, isSwitching, isDisabled, onSwitch, onRefresh, onViewDevice, onViewDetails, onExport, onDelete, onToggleProxy, onWarmup, onUpdateLabel, onViewError, }: AccountRowContentProps) { const { t } = useTranslation(); const { config, showAllQuotas } = useConfigStore(); const validationBlockedLabel = getValidationBlockedStatusLabel(account.validation_blocked_reason, t); // 自定义标签编辑状态 const [isEditingLabel, setIsEditingLabel] = useState(false); const [labelInput, setLabelInput] = useState(account.custom_label || ''); const handleSaveLabel = () => { if (onUpdateLabel) { onUpdateLabel(labelInput.trim()); } setIsEditingLabel(false); }; const handleCancelLabel = () => { setLabelInput(account.custom_label || ''); setIsEditingLabel(false); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleSaveLabel(); } else if (e.key === 'Escape') { handleCancelLabel(); } }; // 使用统一的模型配置 // 获取要显示的模型列表 const pinnedModels = config?.pinned_quota_models?.models || Object.keys(MODEL_CONFIG); // 根据 show_all 状态决定显示哪些模型 const uniqueLabels = new Set(); const displayModels = sortModels( (showAllQuotas ? (account.quota?.models || []).map(m => { const config = MODEL_CONFIG[m.name.toLowerCase()]; const label = m.display_name || (config?.i18nKey ? t(config.i18nKey) : (config?.shortLabel || config?.label || m.name)); return { id: m.name.toLowerCase(), label: label, protectedKey: config?.protectedKey || m.name.toLowerCase(), data: m }; }) : pinnedModels.map(modelId => { const m = account.quota?.models.find(m => m.name === modelId || getModelAliases(modelId).includes(m.name.toLowerCase())); const config = MODEL_CONFIG[modelId]; if (!config && !m) return null; // Safe guard for unknown models that aren't fetched const label = m?.display_name || (config?.i18nKey ? t(config.i18nKey) : (config?.shortLabel || config?.label || modelId)); return { id: modelId, label: label, protectedKey: config?.protectedKey || modelId, data: m }; }).filter(Boolean) as any[] ).filter(m => { // 过滤特定的 Claude/Gemini 思考变体 (在列表页隐藏) const isHiddenThinking = m.id.includes('thinking'); if (isHiddenThinking) return false; // 基于标签去重 (例如 G3.1 Pro 只显示一次) // 优先显示有配额数据的 ID const labelKey = `${m.label}-${m.protectedKey}`; if (uniqueLabels.has(labelKey)) { return false; } if (m.data) { uniqueLabels.add(labelKey); return true; } return true; }) ).filter((m, index, self) => { // 第二次过滤:确保即使没有数据的重复 Label 也只保留一个 const labelKey = `${m.label}-${m.protectedKey}`; return self.findIndex(t => `${t.label}-${t.protectedKey}` === labelKey) === index; }); return ( <> {/* 邮箱列 */}
{account.email}
{isCurrent && ( {t('accounts.current').toUpperCase()} )} {isDisabled && ( {t('accounts.disabled')} )} {account.proxy_disabled && ( {t('accounts.proxy_disabled')} )} {account.quota?.is_forbidden && ( {t('accounts.forbidden')} )} {account.validation_blocked && ( {validationBlockedLabel} )} {/* 订阅类型徽章 */} {account.quota?.subscription_tier && (() => { const tier = account.quota.subscription_tier.toLowerCase(); if (tier.includes('ultra')) { return ( {t('accounts.ultra')} ); } else if (tier.includes('pro')) { return ( {t('accounts.pro')} ); } else { return ( {t('accounts.free')} ); } })()} {/* 自定义标签 */} {account.custom_label && !isEditingLabel && ( {account.custom_label} )} {/* 标签编辑输入框 */} {isEditingLabel && (
setLabelInput(e.target.value)} onKeyDown={handleKeyDown} autoFocus maxLength={15} onClick={(e) => e.stopPropagation()} />
)}
{/* 模型配额列 */} {isDisabled || account.quota?.is_forbidden || account.validation_blocked ? (
{account.validation_blocked ? : (account.quota?.is_forbidden ? : )} {account.validation_blocked ? validationBlockedLabel : (isDisabled ? t('accounts.status.disabled') : t('accounts.forbidden_msg'))}
) : (
{displayModels.map((model) => { const modelData = model.data; return ( ); })}
)} {/* 最后使用时间列 */}
{new Date(account.last_used * 1000).toLocaleDateString()} {new Date(account.last_used * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
{/* 操作列 */}
{/* 自定义标签按钮 */} {onUpdateLabel && ( )} {onWarmup && ( )}
); } // ============================================================================ // 主组件 // ============================================================================ /** * 账号表格组件 * 支持拖拽排序、多选、批量操作等功能 */ function AccountTable({ accounts, selectedIds, refreshingIds, onToggleSelect, onToggleAll, currentAccountId, switchingAccountId, onSwitch, onRefresh, onViewDevice, onViewDetails, onExport, onDelete, onToggleProxy, onReorder, onWarmup, onUpdateLabel, onViewError, }: AccountTableProps) { const { t } = useTranslation(); const [activeId, setActiveId] = useState(null); // showAllQuotas 已经在 useConfigStore 中解构获取 // 配置拖拽传感器 const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 }, // 需要移动 8px 才触发拖拽 }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); const accountIds = useMemo(() => accounts.map(a => a.id), [accounts]); const activeAccount = useMemo(() => accounts.find(a => a.id === activeId), [accounts, activeId]); const handleDragStart = (event: DragStartEvent) => { setActiveId(event.active.id as string); }; const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; setActiveId(null); if (over && active.id !== over.id) { const oldIndex = accountIds.indexOf(active.id as string); const newIndex = accountIds.indexOf(over.id as string); if (oldIndex !== -1 && newIndex !== -1 && onReorder) { onReorder(arrayMove(accountIds, oldIndex, newIndex)); } } }; if (accounts.length === 0) { return (

{t('accounts.empty.title')}

{t('accounts.empty.desc')}

); } return (
{accounts.map((account) => ( onToggleSelect(account.id)} onSwitch={() => onSwitch(account.id)} onRefresh={() => onRefresh(account.id)} onViewDevice={() => onViewDevice(account.id)} onViewDetails={() => onViewDetails(account.id)} onExport={() => onExport(account.id)} onDelete={() => onDelete(account.id)} onToggleProxy={() => onToggleProxy(account.id)} onWarmup={onWarmup ? () => onWarmup(account.id) : undefined} onUpdateLabel={onUpdateLabel ? (label: string) => onUpdateLabel(account.id, label) : undefined} onViewError={() => onViewError(account.id)} /> ))}
{t('accounts.drag_to_reorder')} 0 && selectedIds.size === accounts.length} onChange={onToggleAll} /> {t('accounts.table.email')} {t('accounts.table.quota')} {t('accounts.table.last_used')} {t('accounts.table.actions')}
{/* 拖拽悬浮预览层 */} { activeAccount ? ( { }} onRefresh={() => { }} onViewDevice={() => { }} onViewDetails={() => { }} onExport={() => { }} onDelete={() => { }} onToggleProxy={() => { }} isDisabled={Boolean(activeAccount.disabled)} onViewError={() => { }} />
) : null }
); } export default AccountTable;