feat(ui): 密钥管理支持拖拽调整优先级

- 添加拖拽手柄和视觉反馈效果
- 拖拽到组内(2+同优先级)时加入该组
- 拖拽到单独密钥时交换优先级
- 同组内拖拽忽略操作
This commit is contained in:
fawney19
2026-01-11 00:22:21 +08:00
parent 7faca5512a
commit aec0326d40

View File

@@ -168,13 +168,27 @@
class="divide-y divide-border/40"
>
<div
v-for="{ key, endpoint } in allKeys"
v-for="({ key, endpoint }, index) in allKeys"
:key="key.id"
class="px-4 py-2.5 hover:bg-muted/30 transition-colors"
class="px-4 py-2.5 hover:bg-muted/30 transition-colors group/item"
:class="{
'opacity-50': keyDragState.isDragging && keyDragState.draggedIndex === index,
'bg-primary/5 border-l-2 border-l-primary': keyDragState.targetIndex === index && keyDragState.isDragging
}"
draggable="true"
@dragstart="handleKeyDragStart($event, index)"
@dragend="handleKeyDragEnd"
@dragover="handleKeyDragOver($event, index)"
@dragleave="handleKeyDragLeave"
@drop="handleKeyDrop($event, index)"
>
<!-- 第一行名称 + 状态 + 操作按钮 -->
<div class="flex items-center justify-between gap-2">
<div class="flex items-center gap-2 flex-1 min-w-0">
<!-- 拖拽手柄 -->
<div class="cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover/item:text-muted-foreground transition-colors shrink-0">
<GripVertical class="w-4 h-4" />
</div>
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span>
<span class="text-xs font-mono text-muted-foreground">
{{ key.api_key_masked }}
@@ -570,7 +584,7 @@ const batchAssignDialogOpen = ref(false)
// ModelAliasesTab 组件引用
const modelAliasesTabRef = ref<InstanceType<typeof ModelAliasesTab> | 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<string | null>(null)
const editingPriorityValue = ref<number>(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 '-'