树形结构渲染 + 选择(Vue3 + ElementPlus)

javascript 复制代码
后端数据
   ├─ 平铺数组  →  listToTree  →  deptTree
   └─ 树形数组  →  直接使用

<el-tree-select>
   ├─ v-model        = 选中id
   ├─ :data          = deptTree
   ├─ :props         = 字段映射
   └─ @change        = 递归拿节点 → 回显名称

1. 完整 mini Demo(可运行)

javascript 复制代码
<template>
  <el-form :model="form" label-width="80">
    <el-form-item label="部门" prop="deptId" :rules="[{ required: true }]">
      <el-tree-select
        v-model="form.deptId"
        :data="deptTree"
        placeholder="请选择部门"
        check-strictly
        :props="{ label: 'name', value: 'id' }"
        clearable
        @change="onChange" />
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { ref } from 'vue';

// 模拟平铺数据
const flat = [
  { id: 1, name: '总部',  parentId: 0 },
  { id: 11, name: '技术部', parentId: 1 },
  { id: 12, name: '财务部', parentId: 1 }
];

// 工具:转树
const listToTree = (list: any[], root = 0, key = 'id', parentKey = 'parentId') => {
  const map: any = {};
  list.forEach(item => map[item[key]] = item);
  const tree: any[] = [];
  list.forEach(item => {
    const parent = map[item[parentKey]];
    if (parent) (parent.children ||= []).push(item);
    else if (item[parentKey] === root) tree.push(item);
  });
  return tree;
};

const deptTree = ref(listToTree(flat));
const form = ref({ deptId: null as number | null, deptName: '' });

const findNodeDeep = (tree: any[] | undefined, id: number): any | undefined => {
  if (!tree) return;
  for (const node of tree) {
    if (node.id === id) return node;
    const found = findNodeDeep(node.children, id);
    if (found) return found;
  }
};

const onChange = (id: number) => {
  const node = findNodeDeep(deptTree.value, id);
  form.value.deptName = node?.name ?? '';
};
</script>

1. 第 1 步:先看清后端数据长啥样

① 平铺数组(最常见)

javascript 复制代码
[
  { id: 1, name: '总部',  parentId: 0 },
  { id: 11, name: '技术部', parentId: 1 },
  { id: 12, name: '财务部', parentId: 1 }
]

→ 需要 转树

② 已经是树

javascript 复制代码
[
  { id: 1, name: '总部', children: [
      { id: 11, name: '技术部', children: [] },
      { id: 12, name: '财务部', children: [] }
  ]}
]

→ 直接可用。

2. 第 2 步:平铺转树工具函数

javascript 复制代码
function listToTree(list: any[], root = 0, key = 'id', parentKey = 'parentId', childKey = 'children') {
  const map: any = {};
  list.forEach(item => map[item[key]] = item);
  const tree: any[] = [];
  list.forEach(item => {
    const parent = map[item[parentKey]];
    if (parent) {
      (parent[childKey] ||= []).push(item);
    } else if (item[parentKey] === root) {
      tree.push(item);
    }
  });
  return tree;
}    

用法:

javascript 复制代码
const deptTree = listToTree(flatList);  // 得到 ElementPlus 直接能用的数组

3. 第 3 步:选对组件 & 记住 4 个核心属性

组件 属性 含义
<el-tree-select> v-model 绑定 选中值 (一般是 id
:data 树形数组
:props 字段映射 { label: 'name', value: 'id', children: 'children' }
@change 选中变化回调,参数就是 value
html 复制代码
<el-tree-select
  v-model="form.deptId"
  :data="deptTree"
  placeholder="请选择部门"
  check-strictly
  :props="{ label: 'name', value: 'id' }"
  clearable
  @change="onChange" />

4. 第 4 步:选中后拿"名称"或其他字段

组件只返回 id,要名称 → 递归找节点

javascript 复制代码
function findNodeDeep(tree: any[] | undefined, id: number): any | undefined {
  if (!tree) return;
  for (const node of tree) {
    if (node.id === id) return node;
    const found = findNodeDeep(node.children, id);
    if (found) return found;
  }
}

const onChange = (id: number) => {
  const node = findNodeDeep(deptTree, id);
  form.deptName = node?.name ?? '';  // 回显名称
  form.deptId   = id;                // 保存 id
};

5. 第 5 步:默认值 / 回显(编辑场景)

javascript 复制代码
// 新增:空值
form.deptId = null;

// 编辑:把后端返回的 id 丢进去即可
form.deptId = row.deptId;   // 组件会自动高亮对应节点

6. 第 6 步:校验 & 清空

场景 说明
必填 <el-form-item label="部门" prop="deptId" :rules="[{ required: true, message: '请选择' }]">
清空 组件自带 clearable,清空后 form.deptId = null,无需额外处理
相关推荐
冬奇Lab3 分钟前
每日一个开源项目(第137篇):Penpot - 真正开源的设计协作工具,SVG 原生格式消灭设计-开发鸿沟
前端·开源·设计
nuIl21 分钟前
实现一个 Coding Agent(7):Skills
前端·agent·cursor
nuIl26 分钟前
实现一个 Coding Agent(8):会话持久化与多会话
前端·agent·cursor
jt君424261 小时前
React Native JSI 深入剖析 — 第 5 部分中文技术整理:用 HostObject 把 C++ 类暴露给 JavaScript
前端·react native
胡萝卜术2 小时前
滑动窗口最大值:从暴力到单调队列,层层优化全解析
前端·javascript·面试
fluffyox2 小时前
Notion 的公式栏里,藏着一台虚拟机——逆向 + 用 600 行 JS 复刻它的编译器与栈式 VM
前端
kyriewen3 小时前
2026 年了,这 6 个 npm 包可以卸载了——浏览器原生 API 已经能替代
前端·javascript·npm
铁皮饭盒4 小时前
bun直接tsx,优雅!
javascript·后端
Csvn5 小时前
Monorepo 迁移血泪史:从 Multi-Repo 到 Turborepo,这 3 个坑我帮你踩完了
前端
星栈6 小时前
Dioxus 多页面怎么做:`dioxus-router`、嵌套路由、`Outlet` 和页面组织,一篇给你讲顺
前端·rust·前端框架