diff --git a/frontend/src/components/ContextMenu.vue b/frontend/src/components/ContextMenu.vue
new file mode 100644
index 0000000..b24f8a8
--- /dev/null
+++ b/frontend/src/components/ContextMenu.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/SessionTree.vue b/frontend/src/components/SessionTree.vue
index 7e749e9..600b8a7 100644
--- a/frontend/src/components/SessionTree.vue
+++ b/frontend/src/components/SessionTree.vue
@@ -3,21 +3,94 @@ import { ref, computed, provide } from 'vue'
import { useSessionTreeStore } from '../stores/sessionTree'
import { useWorkspaceStore } from '../stores/workspace'
import { useTreeDragDrop } from '../composables/useTreeDragDrop'
+import { useKeyboardShortcuts } from '../composables/useKeyboardShortcuts'
import SessionTreeNode from './SessionTreeNode.vue'
-import { FolderPlus } from 'lucide-vue-next'
+import ContextMenu from './ContextMenu.vue'
+import type { ContextMenuItem } from './ContextMenu.vue'
+import { FolderPlus, Edit2, Trash2 } from 'lucide-vue-next'
const treeStore = useSessionTreeStore()
const workspaceStore = useWorkspaceStore()
const showNewFolderDialog = ref(false)
const newFolderName = ref('')
+const showRenameDialog = ref(false)
+const renameNodeId = ref(null)
+const renameValue = ref('')
+
+// Context menu
+const showContextMenu = ref(false)
+const contextMenuX = ref(0)
+const contextMenuY = ref(0)
+const contextMenuNodeId = ref(null)
const rootNodes = computed(() => treeStore.rootNodes)
+const contextMenuItems = computed(() => {
+ if (!contextMenuNodeId.value) return []
+
+ const node = treeStore.nodes.find(n => n.id === contextMenuNodeId.value)
+ if (!node) return []
+
+ const items: ContextMenuItem[] = [
+ {
+ label: '重命名',
+ icon: Edit2,
+ action: () => startRename(node.id),
+ },
+ ]
+
+ if (node.type === 'folder') {
+ items.push({
+ label: '新建子文件夹',
+ icon: FolderPlus,
+ action: () => createSubFolder(node.id),
+ })
+ }
+
+ items.push(
+ { divider: true } as ContextMenuItem,
+ {
+ label: '删除',
+ icon: Trash2,
+ action: () => deleteNode(node.id),
+ }
+ )
+
+ return items
+})
+
// Provide drag-drop handlers to child components
const dragHandlers = useTreeDragDrop()
provide('dragHandlers', dragHandlers)
+// Keyboard shortcuts
+useKeyboardShortcuts([
+ {
+ key: 'F2',
+ handler: () => {
+ if (treeStore.selectedNodeId) {
+ startRename(treeStore.selectedNodeId)
+ }
+ },
+ },
+ {
+ key: 'Delete',
+ handler: () => {
+ if (treeStore.selectedNodeId) {
+ deleteNode(treeStore.selectedNodeId)
+ }
+ },
+ },
+ {
+ key: 'n',
+ ctrl: true,
+ handler: () => {
+ showNewFolderDialog.value = true
+ },
+ },
+])
+
function handleNodeClick(nodeId: string) {
const node = treeStore.nodes.find(n => n.id === nodeId)
if (!node) return
@@ -31,12 +104,56 @@ function handleNodeClick(nodeId: string) {
treeStore.selectNode(nodeId)
}
+function handleNodeContextMenu(nodeId: string, event: MouseEvent) {
+ event.preventDefault()
+ contextMenuNodeId.value = nodeId
+ contextMenuX.value = event.clientX
+ contextMenuY.value = event.clientY
+ showContextMenu.value = true
+ treeStore.selectNode(nodeId)
+}
+
function createFolder() {
if (!newFolderName.value.trim()) return
treeStore.createFolder(newFolderName.value.trim())
newFolderName.value = ''
showNewFolderDialog.value = false
}
+
+function createSubFolder(_parentId: string) {
+ showNewFolderDialog.value = true
+ // TODO: Set parent context for new folder
+}
+
+function startRename(nodeId: string) {
+ const node = treeStore.nodes.find(n => n.id === nodeId)
+ if (!node) return
+
+ renameNodeId.value = nodeId
+ renameValue.value = node.name
+ showRenameDialog.value = true
+}
+
+function confirmRename() {
+ if (!renameNodeId.value || !renameValue.value.trim()) return
+ treeStore.renameNode(renameNodeId.value, renameValue.value.trim())
+ showRenameDialog.value = false
+ renameNodeId.value = null
+ renameValue.value = ''
+}
+
+function deleteNode(nodeId: string) {
+ const node = treeStore.nodes.find(n => n.id === nodeId)
+ if (!node) return
+
+ const confirmMsg = node.type === 'folder'
+ ? `确定要删除文件夹 "${node.name}" 及其所有内容吗?`
+ : `确定要删除连接 "${node.name}" 吗?`
+
+ if (confirm(confirmMsg)) {
+ treeStore.deleteNode(nodeId)
+ }
+}
@@ -59,9 +176,11 @@ function createFolder() {
:node="node"
:level="0"
@click="handleNodeClick"
+ @contextmenu="handleNodeContextMenu"
/>
+
+
+
+
+
+
重命名
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/SessionTreeNode.vue b/frontend/src/components/SessionTreeNode.vue
index d116886..5d84ad4 100644
--- a/frontend/src/components/SessionTreeNode.vue
+++ b/frontend/src/components/SessionTreeNode.vue
@@ -13,6 +13,7 @@ interface Props {
const props = defineProps()
const emit = defineEmits<{
click: [nodeId: string]
+ contextmenu: [nodeId: string, event: MouseEvent]
}>()
const treeStore = useSessionTreeStore()
@@ -48,6 +49,10 @@ function handleClick() {
emit('click', props.node.id)
}
+function handleContextMenu(event: MouseEvent) {
+ emit('contextmenu', props.node.id, event)
+}
+
function handleDragStart(event: DragEvent) {
if (dragHandlers) {
event.dataTransfer!.effectAllowed = 'move'
@@ -93,6 +98,7 @@ function handleDragEnd() {
}"
:style="{ paddingLeft: `${level * 16 + 8}px` }"
@click="handleClick"
+ @contextmenu="handleContextMenu"
@dragstart="handleDragStart"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@@ -145,6 +151,7 @@ function handleDragEnd() {
:node="child"
:level="level + 1"
@click="emit('click', $event)"
+ @contextmenu="(nodeId, event) => emit('contextmenu', nodeId, event)"
/>
diff --git a/frontend/src/composables/useKeyboardShortcuts.ts b/frontend/src/composables/useKeyboardShortcuts.ts
new file mode 100644
index 0000000..8af2811
--- /dev/null
+++ b/frontend/src/composables/useKeyboardShortcuts.ts
@@ -0,0 +1,34 @@
+import { onMounted, onUnmounted } from 'vue'
+
+export interface KeyboardShortcut {
+ key: string
+ ctrl?: boolean
+ shift?: boolean
+ alt?: boolean
+ handler: (event: KeyboardEvent) => void
+}
+
+export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
+ function handleKeyDown(event: KeyboardEvent) {
+ for (const shortcut of shortcuts) {
+ const ctrlMatch = shortcut.ctrl === undefined || shortcut.ctrl === (event.ctrlKey || event.metaKey)
+ const shiftMatch = shortcut.shift === undefined || shortcut.shift === event.shiftKey
+ const altMatch = shortcut.alt === undefined || shortcut.alt === event.altKey
+ const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase()
+
+ if (ctrlMatch && shiftMatch && altMatch && keyMatch) {
+ event.preventDefault()
+ shortcut.handler(event)
+ break
+ }
+ }
+ }
+
+ onMounted(() => {
+ window.addEventListener('keydown', handleKeyDown)
+ })
+
+ onUnmounted(() => {
+ window.removeEventListener('keydown', handleKeyDown)
+ })
+}