Vue + TypeScript 请求数据后结合 Element UI 实现树形菜单与下拉选择

1. 背景与需求

在管理后台或复杂表单中,我们经常需要展示层级结构数据,比如部门组织、分类目录等。典型的场景是:从后端获取一组扁平或嵌套的部门数据,经过 Vue + TypeScript 处理后,使用 Element UI 的树形组件展示为菜单,并在表单中提供一个"树形下拉选择器"------点击下拉框后展开一颗树,选择某个节点后收起。

本文将手把手演示如何:

  • 使用 TypeScript 定义接口类型
  • 获取后端数据并递归转换成 el‑tree 所需的结构
  • 展示静态树形菜单
  • 封装一个"树形下拉选择"组件,结合 el‑selectel‑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 字段,但仍建议在前端做一次结构适配,保持 idlabelchildren 的命名一致。

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 的类型约束,整个数据流清晰可靠,有效减少了运行时错误。希望本教程对你的项目有所帮助。

相关推荐
嘟嘟07171 小时前
useRef + Web Worker:React 中的耗时计算不卡页面
javascript·vue.js
万敏1 小时前
Vue3 全栈实战第五周:Vitest 单元测试从零搭建实战记录
vue.js·node.js·全栈
sugar__salt4 小时前
跟着 Demo 学 Pinia:两种仓库写法 + 完整 TodoList 复现
前端·javascript·vue.js·前端框架·vue
To_OC5 小时前
从一个名字编辑组件开始,我把 React + TS 的数据流和副作用彻底搞明白了
前端·react.js·typescript
FungLeo5 小时前
Flutter 把第三方 UI 库渐进迁回 Material 3:组件映射总表 + 四批次替换实战
flutter·ui
鹤卿1236 小时前
「iOS」天气预报仿写总结
ui·ios·objective-c
头茬韭菜8 小时前
5.5 权限交互:渐进式信任的 UI 设计
ui·交互
java1234_小锋8 小时前
Vue3专题 - 条件渲染
前端·javascript·vue.js
鸽鸽8 小时前
Vue 3 API 完全指南:从 Options 到 Composition 的进阶之路
前端·vue.js