Vue树选择

Element ui树选择

TreeSelect.vue

复制代码
<template>
  <div class="tree-select">
    <el-popover
      placement="bottom-start"
      width="300"
      trigger="click"
      v-model="visible"
      popper-class="tree-select-popper"
    >
      <!-- 搜索框 -->
      <el-input
        v-model="filterText"
        placeholder="搜索..."
        size="small"
        clearable
        style="margin-bottom: 8px;"
      />

      <!-- 树形结构 -->
      <el-tree
        ref="tree"
        :data="data"
        :props="defaultProps"
        node-key="id"
        :show-checkbox="multiple"
        highlight-current
        :filter-node-method="filterNode"
        @node-click="handleNodeClick"
        @check="handleCheck"
        default-expand-all
      />

      <!-- 输入框触发器 -->
      <el-input
        slot="reference"
        v-model="selectedLabels"
        :placeholder="placeholder"
        readonly
        suffix-icon="el-icon-caret-bottom"
      />
    </el-popover>
  </div>
</template>

<script>
export default {
  name: "TreeSelect",
  props: {
    value: [String, Number, Array],
    data: { type: Array, required: true },
    multiple: { type: Boolean, default: false },
    placeholder: { type: String, default: "请选择" }
  },
  data() {
    return {
      visible: false,
      filterText: "",
      selectedLabels: "",
      defaultProps: { children: "children", label: "label" }
    };
  },
  watch: {
    filterText(val) {
      this.$refs.tree?.filter(val);
    },
    value: {
      immediate: true,
      handler(val) {
        if (!val) {
          this.selectedLabels = "";
          return;
        }
        if (this.multiple && Array.isArray(val)) {
          const nodes = this.$refs.tree?.getCheckedNodes() || [];
          this.selectedLabels = this.formatGroupedLabels(nodes);
        } else {
          const node = this.findNodeById(this.data, val);
          this.selectedLabels = node ? node.label : "";
        }
      }
    },
    visible(val) {
      const popper = document.querySelector('.tree-select-popper');
      if (val) {
        // 弹窗打开,允许交互
        popper?.removeAttribute('inert');
      } else {
        // 弹窗关闭前,移除焦点
        this.$nextTick(() => {
          document.activeElement?.blur();
        });
        // 延迟设置 inert,确保动画完成后 DOM 被隐藏
        setTimeout(() => {
          popper?.setAttribute('inert', '');
        }, 300); // 与 Element UI 的 fade-in-linear 动画时长保持一致
      }
    }
  },
  methods: {
    filterNode(value, data) {
      if (!value) return true;
      return data.label.includes(value);
    },
    handleNodeClick(node) {
      if (!this.multiple) {
        this.selectedLabels = node.label;
        this.$emit("input", node.id);
        this.visible = false;
      }
    },
    handleCheck() {
      if (this.multiple) {
        const nodes = this.$refs.tree.getCheckedNodes();
        this.selectedLabels = this.formatGroupedLabels(nodes);
        this.$emit("input", nodes.map(n => n.id));
      }
    },
    formatGroupedLabels(nodes) {
      const grouped = {};
      nodes.forEach(node => {
        const topParent = this.findTopParent(this.data, node.id);
        if (topParent && node.id !== topParent.id) {
          if (!grouped[topParent.label]) {
            grouped[topParent.label] = [];
          }
          grouped[topParent.label].push(node.label);
        }
      });
      return Object.keys(grouped)
        .map(parent => `${parent}: ${grouped[parent].join(", ")}`)
        .join("; ");
    },
    findTopParent(list, id, parent = null) {
      for (let item of list) {
        if (item.id === id) return parent || item;
        if (item.children) {
          const found = this.findTopParent(item.children, id, parent || item);
          if (found) return found;
        }
      }
      return null;
    },
    findNodeById(list, id) {
      for (let item of list) {
        if (item.id === id) return item;
        if (item.children) {
          const found = this.findNodeById(item.children, id);
          if (found) return found;
        }
      }
      return null;
    }
  }
};
</script>

<style scoped>
.tree-select {
  width: 300px;
}
.tree-select-popper {
  max-height: 300px;
  overflow: auto;
}
</style>

使用示例

复制代码
<template>
  <div>
    <h3>单选</h3>
    <tree-select v-model="singleValue" :data="treeData" />

    <h3 style="margin-top:20px;">多选</h3>
    <tree-select v-model="multiValue" :data="treeData" multiple />
  </div>
</template>

<script>
import TreeSelect from "./TreeSelect.vue";

export default {
  components: { TreeSelect },
  data() {
    return {
      singleValue: null,
      multiValue: [],
      treeData: [
        {
          id: 1,
          label: "水果",
          children: [
            {
              id: 11,
              label: "苹果",
              children: [
                { id: 111, label: "红富士" },
                { id: 112, label: "奶苹果" }
              ]
            },
            { id: 12, label: "香蕉" }
          ]
        },
        {
          id: 2,
          label: "蔬菜",
          children: [
            { id: 21, label: "西红柿" },
            { id: 22, label: "黄瓜" }
          ]
        }
      ]
    };
  }
};
</script>

效果

扩展

v-model 的底层机制(Vue 2)

在 Vue 2 中,v-model 是语法糖,它自动做了两件事:

复制代码
<tree-select v-model="form.singleValue" />

等价于:

复制代码
<tree-select
  :value="form.singleValue"
  @input="form.singleValue = $event"
/>

所以你虽然没在父组件写 @input,Vue 帮你自动加上了!

子组件只需 $emit('input', newValue) 就能触发更新

你在子组件中写了:

复制代码
this.$emit('input', node.id);

Vue 会自动把这个 input 事件接到父组件的 v-model 上,并执行:

复制代码
form.singleValue = node.id;

为什么叫"语法糖"?

因为你没显式写 :value@input,但 Vue 自动帮你做了:

  • :value → 绑定到 props.value

  • @input → 自动更新绑定的变量

你只要在子组件里 $emit('input', newValue),父组件就会自动更新。

总结一句话

Vue 的 v-model 本质上是 :value + @input 的组合,子组件只要 $emit('input', ...),父组件就能自动更新绑定值。

相关推荐
毕设十刻3 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
牧杉-惊蛰4 小时前
纯flex布局来写瀑布流
前端·javascript·css
王同学要变强7 小时前
【深入学习Vue丨第二篇】构建动态Web应用的基础
前端·vue.js·学习
社恐的下水道蟑螂7 小时前
从字符串到像素:深度解析 HTML/CSS/JS 的页面渲染全过程
javascript·css·html
程序定小飞7 小时前
基于springboot的web的音乐网站开发与设计
java·前端·数据库·vue.js·spring boot·后端·spring
武昌库里写JAVA7 小时前
element-ui 2.x 及 vxe-table 2.x 使用 css 定制主题
java·vue.js·spring boot·sql·学习
行走的陀螺仪8 小时前
uni-app + Vue3 实现折叠文本(超出省略 + 展开收起)
前端·javascript·css·uni-app·vue3
冴羽8 小时前
JavaScript 异步循环踩坑指南
前端·javascript·node.js
Mr.Jessy8 小时前
Web APIs 学习第四天:DOM事件进阶
开发语言·前端·javascript·学习·ecmascript
醉方休8 小时前
开发一个完整的Electron应用程序
前端·javascript·electron