实现 MobaXterm 风格的界面重构,包含会话树、工作区面板和分屏功能。 新增功能: - 左侧会话树支持文件夹分组和展开/折叠 - 工作区垂直分屏(终端 + SFTP) - 可拖拽调整分割比例 - 状态持久化到 localStorage - 顶部工具栏(样式占位) 技术实现: - 新增 sessionTreeStore 和 workspaceStore 状态管理 - 新增 SessionTree/SessionTreeNode 递归组件 - 新增 SplitPane 可拖拽分割组件 - 重构 SftpPanel 为 props 驱动 - 新增 MobaLayout 主布局 - 路由默认重定向到 /moba 依赖更新: - 安装 @vueuse/core 用于拖拽功能 待实现: - Phase 3: 会话树拖拽排序 - Phase 5: 数据迁移 - Phase 6: 快捷键、右键菜单、搜索等优化 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
86 lines
2.4 KiB
Vue
86 lines
2.4 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useSessionTreeStore } from '../stores/sessionTree'
|
|
import { useConnectionsStore } from '../stores/connections'
|
|
import type { SessionTreeNode as TreeNode } from '../types/sessionTree'
|
|
import { Folder, FolderOpen, ChevronRight, ChevronDown, Server } from 'lucide-vue-next'
|
|
|
|
interface Props {
|
|
node: TreeNode
|
|
level: number
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const emit = defineEmits<{
|
|
click: [nodeId: string]
|
|
}>()
|
|
|
|
const treeStore = useSessionTreeStore()
|
|
const connectionsStore = useConnectionsStore()
|
|
|
|
const children = computed(() => treeStore.getChildren(props.node.id))
|
|
const isSelected = computed(() => treeStore.selectedNodeId === props.node.id)
|
|
const isExpanded = computed(() => props.node.expanded)
|
|
|
|
const connection = computed(() => {
|
|
if (props.node.type === 'connection' && props.node.connectionId) {
|
|
return connectionsStore.connections.find(c => c.id === props.node.connectionId)
|
|
}
|
|
return null
|
|
})
|
|
|
|
function handleClick() {
|
|
emit('click', props.node.id)
|
|
}
|
|
</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"
|
|
:class="{
|
|
'bg-slate-800 text-cyan-300': isSelected,
|
|
'text-slate-300': !isSelected,
|
|
}"
|
|
:style="{ paddingLeft: `${level * 16 + 8}px` }"
|
|
@click="handleClick"
|
|
>
|
|
<button
|
|
v-if="node.type === 'folder'"
|
|
class="w-4 h-4 flex items-center justify-center"
|
|
@click.stop="treeStore.toggleExpanded(node.id)"
|
|
>
|
|
<ChevronDown v-if="isExpanded" class="w-3.5 h-3.5" />
|
|
<ChevronRight v-else class="w-3.5 h-3.5" />
|
|
</button>
|
|
<div v-else class="w-4" />
|
|
|
|
<component
|
|
:is="node.type === 'folder' ? (isExpanded ? FolderOpen : Folder) : Server"
|
|
class="w-4 h-4 flex-shrink-0"
|
|
:class="{
|
|
'text-amber-400': node.type === 'folder',
|
|
'text-cyan-400': node.type === 'connection',
|
|
}"
|
|
/>
|
|
|
|
<span class="text-sm truncate flex-1">{{ node.name }}</span>
|
|
|
|
<span
|
|
v-if="connection"
|
|
class="w-2 h-2 rounded-full bg-slate-600"
|
|
/>
|
|
</div>
|
|
|
|
<template v-if="node.type === 'folder' && isExpanded">
|
|
<SessionTreeNode
|
|
v-for="child in children"
|
|
:key="child.id"
|
|
:node="child"
|
|
:level="level + 1"
|
|
@click="emit('click', $event)"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</template>
|