-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApiTree.vue
More file actions
194 lines (170 loc) · 5.61 KB
/
ApiTree.vue
File metadata and controls
194 lines (170 loc) · 5.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
<script lang="ts" setup>
import type { ApiBrief, GroupNodeWithApis } from '@/types/api'
import type { DropItem } from '@/types/api-drag'
import {
FolderPlus,
FolderRoot,
Inbox,
Loader2,
} from 'lucide-vue-next'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref, watch } from 'vue'
import { toast } from 'vue-sonner'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useApiTreeDrag } from '@/composables/useApiTreeDrag'
import { cn } from '@/lib/utils'
import { useApiDragStore } from '@/stores/useApiDragStore'
import { useApiTreeStore } from '@/stores/useApiTreeStore'
import ApiTreeGroup from './ApiTreeGroup.vue'
import ApiTreeToolbar from './ApiTreeToolbar.vue'
const emits = defineEmits<{
(e: 'selectApi', api: ApiBrief): void
(e: 'createGroup', parentId?: string): void
(e: 'createApi', groupId?: string): void
(e: 'renameGroup', group: GroupNodeWithApis): void
(e: 'deleteGroup', group: GroupNodeWithApis): void
(e: 'cloneApi', api: ApiBrief): void
(e: 'deleteApi', api: ApiBrief): void
}>()
const apiTreeStore = useApiTreeStore()
const { projectId, loadingStatus, filteredTree, hasTree } = storeToRefs(apiTreeStore)
const isLoading = computed(() => loadingStatus.value === 'loading')
const hasData = computed(() => filteredTree.value.length > 0)
const apiDragStore = useApiDragStore()
const { isDragging } = storeToRefs(apiDragStore)
const dragger = useApiTreeDrag(apiDragStore, apiTreeStore)
const searchInput = ref('')
// 工具栏创建 API 时先进行分组校验
function handleToolbarCreateApi() {
if (!hasTree.value) {
toast.error('接口必须归属于某个分组,请先创建分组')
emits('createGroup')
return
}
emits('createApi')
}
/** 是否正在拖拽到 Root 区域 */
const isRootDragOver = ref(false)
watch(searchInput, (newV) => {
apiTreeStore.setSearchQuery(newV)
})
watch(projectId, (newV) => {
if (newV) {
apiTreeStore.fetchTree(newV)
}
}, { immediate: true })
/** 根级别区域拖拽经过 */
function handleRootDragOver(e: DragEvent) {
e.preventDefault()
if (!dragger.canMoveToRoot.value)
return
e.dataTransfer!.dropEffect = 'move'
const target: DropItem = { type: 'root' }
if (dragger.isValidDrop(target)) {
isRootDragOver.value = true
apiDragStore.setDropTarget(target)
}
}
function handleRootDragLeave() {
isRootDragOver.value = false
}
async function handleRootDrop(e: DragEvent) {
e.preventDefault()
e.stopPropagation()
if (isRootDragOver.value) {
await dragger.executeDrop()
}
isRootDragOver.value = false
}
onMounted(() => {
if (projectId.value) {
apiTreeStore.fetchTree(projectId.value)
}
})
</script>
<template>
<div class="h-full flex flex-col">
<ApiTreeToolbar
@create-api="handleToolbarCreateApi"
@create-group="emits('createGroup')"
/>
<ScrollArea class="flex-1 px-2 min-h-0">
<div
v-if="isLoading && !hasData"
class="flex items-center justify-center py-12"
>
<Loader2 class="h-6 w-6 animate-spin text-muted-foreground" />
</div>
<div
v-else-if="!hasData"
class="flex flex-col items-center justify-center py-12 px-4"
>
<div class="w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-3">
<Inbox class="h-6 w-6 text-muted-foreground" />
</div>
<p class="text-sm text-muted-foreground text-center mb-4">
{{ searchInput ? '没有找到匹配的接口' : '暂无接口,点击上方按钮创建' }}
</p>
<div
v-if="!searchInput"
class="flex gap-2"
>
<Button
variant="outline"
size="sm"
@click="emits('createGroup')"
>
<FolderPlus class="h-4 w-4 mr-1" />
新建分组
</Button>
</div>
</div>
<div v-else class="py-2">
<ApiTreeGroup
v-for="group in filteredTree"
:key="group.id"
:group="group"
:level="0"
@create-group="emits('createGroup', $event)"
@create-api="emits('createApi', $event)"
@rename-group="emits('renameGroup', $event)"
@delete-group="emits('deleteGroup', $event)"
@select-api="emits('selectApi', $event)"
@clone-api="emits('cloneApi', $event)"
@delete-api="emits('deleteApi', $event)"
/>
<Transition
enter-active-class="transition-all duration-200 ease-out"
leave-active-class="transition-all duration-150 ease-in"
enter-from-class="opacity-0 max-h-0"
enter-to-class="opacity-100 max-h-20"
leave-from-class="opacity-100 max-h-20"
leave-to-class="opacity-0 max-h-0"
>
<div
v-if="isDragging && dragger.canMoveToRoot"
:class="cn(
'mx-2 mt-2 px-3 py-3 rounded-lg border-2 border-dashed transition-all duration-150',
'flex items-center justify-center gap-2 text-sm',
isRootDragOver
? 'border-primary bg-primary/10 text-primary'
: 'border-muted-foreground/30 text-muted-foreground',
)"
@dragover="handleRootDragOver"
@dragleave="handleRootDragLeave"
@drop="handleRootDrop"
>
<FolderRoot
:class="cn(
'h-4 w-4 transition-transform duration-150',
isRootDragOver && 'scale-110',
)"
/>
<span>移动到根目录</span>
</div>
</Transition>
</div>
</ScrollArea>
</div>
</template>