diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index ecc7f27..bfb4811 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -168,13 +168,27 @@ class="divide-y divide-border/40" >
+ +
+ +
{{ key.name || '未命名密钥' }} {{ key.api_key_masked }} @@ -570,7 +584,7 @@ const batchAssignDialogOpen = ref(false) // ModelAliasesTab 组件引用 const modelAliasesTabRef = ref | null>(null) -// 拖动排序相关状态 +// 拖动排序相关状态(旧的端点级别拖拽,保留以兼容) const dragState = ref({ isDragging: false, draggedKeyId: null as string | null, @@ -578,6 +592,13 @@ const dragState = ref({ dragEndpointId: null as string | null }) +// 密钥列表拖拽排序状态 +const keyDragState = ref({ + isDragging: false, + draggedIndex: null as number | null, + targetIndex: null as number | null +}) + // 点击编辑优先级相关状态 const editingPriorityKey = ref(null) const editingPriorityValue = ref(0) @@ -1154,6 +1175,92 @@ async function savePriority(key: EndpointAPIKey) { } } +// ===== 密钥列表拖拽排序 ===== +function handleKeyDragStart(event: DragEvent, index: number) { + keyDragState.value.isDragging = true + keyDragState.value.draggedIndex = index + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move' + event.dataTransfer.setData('text/plain', String(index)) + } +} + +function handleKeyDragEnd() { + keyDragState.value.isDragging = false + keyDragState.value.draggedIndex = null + keyDragState.value.targetIndex = null +} + +function handleKeyDragOver(event: DragEvent, index: number) { + event.preventDefault() + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'move' + } + if (keyDragState.value.draggedIndex !== index) { + keyDragState.value.targetIndex = index + } +} + +function handleKeyDragLeave() { + keyDragState.value.targetIndex = null +} + +async function handleKeyDrop(event: DragEvent, targetIndex: number) { + event.preventDefault() + + const draggedIndex = keyDragState.value.draggedIndex + if (draggedIndex === null || draggedIndex === targetIndex) { + handleKeyDragEnd() + return + } + + const keys = allKeys.value.map(item => item.key) + if (draggedIndex < 0 || draggedIndex >= keys.length || targetIndex < 0 || targetIndex >= keys.length) { + handleKeyDragEnd() + return + } + + const draggedKey = keys[draggedIndex] + const targetKey = keys[targetIndex] + const draggedPriority = draggedKey.internal_priority ?? 0 + const targetPriority = targetKey.internal_priority ?? 0 + + // 如果是同组内拖拽(同优先级),忽略操作 + if (draggedPriority === targetPriority) { + handleKeyDragEnd() + return + } + + // 检查目标 key 是否属于一个"组"(除了被拖拽的 key,还有其他 key 与目标同优先级) + // 组的定义:2 个及以上同优先级的 key + const keysAtTargetPriority = keys.filter(k => + k.id !== draggedKey.id && (k.internal_priority ?? 0) === targetPriority + ) + // 如果有 2 个及以上 key 在目标优先级(不含被拖拽的),说明目标在组内 + const targetIsInGroup = keysAtTargetPriority.length >= 2 + + handleKeyDragEnd() + + try { + if (targetIsInGroup) { + // 目标在组内,被拖拽的 key 加入该组 + await updateProviderKey(draggedKey.id, { internal_priority: targetPriority }) + } else { + // 目标是单独的(或只有目标自己),交换优先级 + await Promise.all([ + updateProviderKey(draggedKey.id, { internal_priority: targetPriority }), + updateProviderKey(targetKey.id, { internal_priority: draggedPriority }) + ]) + } + showSuccess('优先级已更新') + await loadEndpoints() + emit('refresh') + } catch (err: any) { + showError(err.response?.data?.detail || '更新优先级失败', '错误') + await loadEndpoints() + } +} + // 格式化探测时间 function formatProbeTime(probeTime: string): string { if (!probeTime) return '-'