first commit
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="tn">
|
||||
<div
|
||||
v-if="node.type === 'folder'"
|
||||
class="folder"
|
||||
:style="{ paddingLeft: 8 + depth * 14 + 'px' }"
|
||||
@click="open = !open"
|
||||
>
|
||||
<svg :class="{ open }" width="10" height="10" viewBox="0 0 10 10"><path d="M3 1l5 4-5 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
<span class="label">{{ node.label }}</span>
|
||||
<span class="count" v-if="node.children?.length">{{ countApis(node) }}</span>
|
||||
<div class="row-actions">
|
||||
<button class="xs-btn ghost icon" @click.stop="addApiHere" title="新增接口">+</button>
|
||||
<button class="xs-btn ghost icon" @click.stop="addFolderHere" title="新增子分组">⊟</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="node.type === 'api'"
|
||||
class="api"
|
||||
:class="{ active: node.filePath === selectedFile, hidden: !match }"
|
||||
:style="{ paddingLeft: 8 + depth * 14 + 'px' }"
|
||||
@click="onClick"
|
||||
>
|
||||
<span class="method" :class="'m-' + (node.method || 'GET').toLowerCase()">{{ node.method }}</span>
|
||||
<span class="name">{{ node.label }}</span>
|
||||
<span v-if="node.status === 'developing'" class="status dot" title="开发中">●</span>
|
||||
</div>
|
||||
|
||||
<template v-if="node.type === 'folder' && open">
|
||||
<TreeNode
|
||||
v-for="c in node.children"
|
||||
:key="c.id"
|
||||
:node="c"
|
||||
:depth="depth + 1"
|
||||
:keyword="keyword"
|
||||
:selected-file="selectedFile"
|
||||
@select-api="$emit('select-api', $event)"
|
||||
/>
|
||||
<div
|
||||
v-if="!node.children?.length"
|
||||
class="empty-folder"
|
||||
:style="{ paddingLeft: 12 + (depth + 1) * 14 + 'px' }"
|
||||
>
|
||||
空白分组
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useWorkspaceStore } from '@/stores/workspace'
|
||||
import type { ApiTreeNode } from '@/types'
|
||||
|
||||
const props = defineProps<{
|
||||
node: ApiTreeNode
|
||||
depth: number
|
||||
keyword: string
|
||||
selectedFile?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select-api', n: ApiTreeNode): void
|
||||
}>()
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const open = ref(props.depth < 2)
|
||||
|
||||
const match = computed(() => {
|
||||
if (!props.keyword) return true
|
||||
return props.node.label.toLowerCase().includes(props.keyword.toLowerCase())
|
||||
})
|
||||
|
||||
function countApis(n: ApiTreeNode): number {
|
||||
if (n.type === 'api') return 1
|
||||
return (n.children ?? []).reduce((sum, c) => sum + countApis(c), 0)
|
||||
}
|
||||
|
||||
async function addApiHere() {
|
||||
if (!workspaceStore.rootDir) return
|
||||
const name = prompt('接口名称', '新建接口')
|
||||
if (!name) return
|
||||
const created = await window.xs.api.create(workspaceStore.rootDir, '', {
|
||||
name, method: 'GET', url: '/'
|
||||
})
|
||||
const apiNode: ApiTreeNode = {
|
||||
type: 'api',
|
||||
id: created.id,
|
||||
label: name,
|
||||
method: 'GET',
|
||||
filePath: created.filePath
|
||||
}
|
||||
if (props.node.type === 'folder') {
|
||||
props.node.children = props.node.children ?? []
|
||||
props.node.children.push(apiNode)
|
||||
await workspaceStore.saveProject()
|
||||
}
|
||||
}
|
||||
|
||||
async function addFolderHere() {
|
||||
const name = prompt('分组名称', '新子分组')
|
||||
if (!name) return
|
||||
if (props.node.type === 'folder') {
|
||||
props.node.children = props.node.children ?? []
|
||||
props.node.children.push({
|
||||
type: 'folder', id: `f-${Date.now()}`, label: name, children: []
|
||||
})
|
||||
await workspaceStore.saveProject()
|
||||
}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
emit('select-api', props.node)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tn { user-select: none; }
|
||||
.folder, .api {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 5px 8px;
|
||||
font-size: $font-sm;
|
||||
cursor: pointer;
|
||||
border-radius: $radius-sm;
|
||||
color: $text-secondary;
|
||||
&:hover { background: $bg-hover; color: $text-primary; }
|
||||
.row-actions { display: none; gap: 2px; margin-left: auto; }
|
||||
&:hover .row-actions { display: flex; }
|
||||
}
|
||||
.folder {
|
||||
svg:first-child { transition: transform .15s ease; color: $text-tertiary; }
|
||||
svg.open { transform: rotate(90deg); }
|
||||
svg:nth-child(2) { color: $primary; opacity: .8; }
|
||||
.label { font-weight: 500; }
|
||||
.count {
|
||||
margin-left: 4px;
|
||||
font-size: $font-xs;
|
||||
color: $text-tertiary;
|
||||
background: $bg-elevated;
|
||||
padding: 0 5px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
.api {
|
||||
&.hidden { opacity: .3; pointer-events: none; }
|
||||
&.active {
|
||||
background: linear-gradient(90deg, $primary-bg, transparent);
|
||||
color: $text-primary;
|
||||
border-left: 2px solid $primary;
|
||||
.name { color: $text-primary; }
|
||||
}
|
||||
.method {
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
font-weight: 700;
|
||||
color: #0a1428;
|
||||
letter-spacing: .5px;
|
||||
width: 42px;
|
||||
text-align: center;
|
||||
}
|
||||
.m-get { background: $m-get; }
|
||||
.m-post { background: $m-post; }
|
||||
.m-put { background: $m-put; }
|
||||
.m-delete { background: $m-delete; }
|
||||
.m-patch { background: $m-patch; }
|
||||
.m-head, .m-options { background: #6b85b3; color: #fff; }
|
||||
|
||||
.name { color: $text-primary; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.status { color: $primary; font-size: 10px; }
|
||||
}
|
||||
.empty-folder {
|
||||
font-size: $font-xs;
|
||||
color: $text-tertiary;
|
||||
padding: 4px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.icon { padding: 2px 6px; font-size: 12px; line-height: 1; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user