feat: implement drag-drop and data migration (Phase 3 & 5)

Phase 3 - Drag-drop functionality:
- Add useTreeDragDrop composable with drag state management
- Implement drag constraints (prevent dropping on self/descendants)
- Add visual feedback (opacity, drop indicators, highlight)
- Support drop positions: before/after/inside folder

Phase 5 - Data migration and sync:
- Add MigrationPrompt component for first-time users
- Implement bidirectional sync between connections and session tree
- Add syncNewConnections/syncDeletedConnections/syncConnectionName methods
- Create useConnectionSync composable for automatic sync
- Support migration from old layout with user prompt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
liumangmang
2026-04-03 15:46:22 +08:00
parent 2c06329d68
commit caed481d23
7 changed files with 417 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, inject } from 'vue'
import { useSessionTreeStore } from '../stores/sessionTree'
import { useConnectionsStore } from '../stores/connections'
import type { SessionTreeNode as TreeNode } from '../types/sessionTree'
@@ -29,22 +29,88 @@ const connection = computed(() => {
return null
})
// Drag-drop functionality
const dragHandlers = inject<any>('dragHandlers', null)
const isDragging = computed(() =>
dragHandlers?.draggedNode.value?.id === props.node.id
)
const isDropTarget = computed(() =>
dragHandlers?.dropTarget.value?.id === props.node.id
)
const dropPosition = computed(() =>
isDropTarget.value ? dragHandlers?.dropPosition.value : null
)
function handleClick() {
emit('click', props.node.id)
}
function handleDragStart(event: DragEvent) {
if (dragHandlers) {
event.dataTransfer!.effectAllowed = 'move'
dragHandlers.handleDragStart(props.node)
}
}
function handleDragOver(event: DragEvent) {
if (dragHandlers) {
dragHandlers.handleDragOver(event, props.node)
}
}
function handleDragLeave() {
if (dragHandlers) {
dragHandlers.handleDragLeave()
}
}
function handleDrop(event: DragEvent) {
if (dragHandlers) {
dragHandlers.handleDrop(event)
}
}
function handleDragEnd() {
if (dragHandlers) {
dragHandlers.handleDragEnd()
}
}
</script>
<template>
<div>
<div
class="flex items-center gap-1.5 px-2 py-1.5 rounded cursor-pointer hover:bg-slate-800 transition-colors"
draggable="true"
class="flex items-center gap-1.5 px-2 py-1.5 rounded cursor-pointer hover:bg-slate-800 transition-colors relative"
:class="{
'bg-slate-800 text-cyan-300': isSelected,
'text-slate-300': !isSelected,
'opacity-50': isDragging,
'ring-2 ring-cyan-500': isDropTarget && dropPosition === 'inside',
}"
:style="{ paddingLeft: `${level * 16 + 8}px` }"
@click="handleClick"
@dragstart="handleDragStart"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
@dragend="handleDragEnd"
>
<!-- Drop indicator - before -->
<div
v-if="isDropTarget && dropPosition === 'before'"
class="absolute top-0 left-0 right-0 h-0.5 bg-cyan-500"
/>
<!-- Drop indicator - after -->
<div
v-if="isDropTarget && dropPosition === 'after'"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-cyan-500"
/>
<button
v-if="node.type === 'folder'"
class="w-4 h-4 flex items-center justify-center"