1. 背景与需求
在管理后台或复杂表单中,我们经常需要展示层级结构数据,比如部门组织、分类目录等。典型的场景是:从后端获取一组扁平或嵌套的部门数据,经过 Vue + TypeScript 处理后,使用 Element UI 的树形组件展示为菜单,并在表单中提供一个"树形下拉选择器"------点击下拉框后展开一颗树,选择某个节点后收起。
本文将手把手演示如何:
- 使用 TypeScript 定义接口类型
- 获取后端数据并递归转换成 el‑tree 所需的结构
- 展示静态树形菜单
- 封装一个"树形下拉选择"组件,结合 el‑select 与 el‑tree
2. 数据准备与类型定义
假设后端返回的原始数据结构为:
json
[
{ "id": 1, "name": "总公司", "parentId": null },
{ "id": 2, "name": "研发部", "parentId": 1 },
{ "id": 3, "name": "市场部", "parentId": 1 },
{ "id": 4, "name": "前端组", "parentId": 2 }
]
在 TypeScript 中定义接口:
typescript
// types/department.ts
export interface RawDepartment {
id: number;
name: string;
parentId: number | null;
}
export interface TreeNode {
id: number;
label: string;
children?: TreeNode[];
}
TreeNode 是 Element UI 树组件需要的节点格式。label 为显示文本,children 为子节点数组。
3. 数据转换:扁平列表 → 树形结构
编写一个工具函数,将扁平数据递归构建成多级树:
typescript
// utils/buildTree.ts
import type { RawDepartment, TreeNode } from "@/types/department";
export function buildTree(
list: RawDepartment[],
parentId: number | null = null
): TreeNode[] {
return list
.filter(item => item.parentId === parentId)
.map(item => ({
id: item.id,
label: item.name,
children: buildTree(list, item.id)
}));
}
如果后端已经返回嵌套 JSON,可以直接赋给 children 字段,但仍建议在前端做一次结构适配,保持 id、label 和 children 的命名一致。
4. 在 Vue 组件中请求数据并构建树
以下示例使用 Setup 语法糖 + TypeScript:
html
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import type { RawDepartment, TreeNode } from '@/types/department';
import { buildTree } from '@/utils/buildTree';
const treeData = ref<TreeNode[]>([]);
async function fetchDepartments() {
// 实际请求替换为你的 API
const res = await fetch('/api/departments');
const rawList: RawDepartment[] = await res.json();
treeData.value = buildTree(rawList);
}
onMounted(() => {
fetchDepartments();
});
</script>
<template>
<el-tree :data="treeData" />
</template>
此时页面即可展示一棵静态树。接下来我们把它封装成下拉选择器。
5. 封装"树形下拉选择"组件
核心思路:使用 el‑select 作为外壳,在下拉面板中放置一个 el‑tree,并保持选择状态同步。同时处理"仅叶子节点可选"或"任意节点可选"等需求。
新建组件 TreeSelect.vue:
html
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import type { TreeNode } from '@/types/department';
interface Props {
modelValue: number | null;
data: TreeNode[];
/** 是否只能选择叶子节点 */
leafOnly?: boolean;
}
const props = withDefaults(defineProps<Props>(), { leafOnly: true });
const emit = defineEmits<{ (e: 'update:modelValue', value: number | null): void }>();
// 用于保存选中的节点 ID
const selectedId = ref<number | null>(props.modelValue);
// 监听外部值变化
watch(() => props.modelValue, val => { selectedId.value = val; });
// 同步回父组件
watch(selectedId, val => { emit('update:modelValue', val); });
// 查找节点 label 用于在 select 中显示
function findLabel(id: number | null, nodes: TreeNode[]): string {
if (id == null) return '';
for (const node of nodes) {
if (node.id === id) return node.label;
if (node.children) {
const label = findLabel(id, node.children);
if (label) return label;
}
}
return '';
}
const displayLabel = computed(() => findLabel(selectedId.value, props.data));
// 树内部阻止节点勾选时直接联动关闭下拉,通过 ref 控制手动关闭
const treeRef = ref<InstanceType<typeof ElTree>>();
const selectVisible = ref(false);
function handleNodeClick(data: TreeNode) {
if (props.leafOnly && data.children && data.children.length > 0) {
// 如果只允许选叶子,父节点点击只做展开,不选中
return;
}
selectedId.value = data.id;
selectVisible.value = false; // 选中后关闭下拉
}
// 也可以通过 check 事件获取选中
// 这里演示了 node-click 方式
</script>
<template>
<el-select
v-model="displayLabel"
:model-value="displayLabel"
placeholder="请选择部门"
clearable
@clear="selectedId = null"
@visible-change="(val: boolean) => selectVisible = val"
>
<template #empty>
<el-tree
ref="treeRef"
:data="data"
node-key="id"
highlight-current
:expand-on-click-node="false"
@node-click="handleNodeClick"
/>
</template>
</el-select>
</template>
6. 使用 TreeSelect 组件
在父组件中引入并传入树数据:
html
<script setup lang="ts">
import TreeSelect from '@/components/TreeSelect.vue';
import { ref } from 'vue';
import type { TreeNode } from '@/types/department';
const departmentId = ref<number | null>(null);
const mockTree = ref<TreeNode[]>([
{ id: 1, label: '总公司', children: [
{ id: 2, label: '研发部', children: [
{ id: 4, label: '前端组' }
]},
{ id: 3, label: '市场部' }
]}
]);
</script>
<template>
<el-form>
<el-form-item label="所属部门">
<TreeSelect v-model="departmentId" :data="mockTree" leaf-only />
</el-form-item>
</el-form>
</template>
7. 小结与扩展
本文从类型定义、数据转换到组件封装,完成了一个标准的"树形下拉选择"功能。你可以根据业务需求进一步扩展:
- 支持多选:使用 el‑select multiple + el‑tree show‑checkbox ,并通过
check事件获取选中节点数组。 - 支持异步加载:为 el‑tree 添加
load属性,实现点击展开节点时动态加载子项。 - 支持搜索/过滤:在树上方添加输入框,通过
filter‑node‑method对树进行本地过滤。
通过 TypeScript 的类型约束,整个数据流清晰可靠,有效减少了运行时错误。希望本教程对你的项目有所帮助。