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 '-'