Vue combat: building a universal drop-down tree component

Preface

In vue, if you want to implement a drop-down tree component, you can directly use the component element plusin treeSelect, but if your project is using element 2.Xthe version, then it does not contain treeSelectcomponents, but we can still use some third-party plug-ins or Encapsulate the component yourself to achieve this operation.


1. @riophae/vue-treeselect plug-in

riophae/vue-treeselect is a vue.jsplug-in based on , which provides a tree selector component for selecting tree-structured data. The plug-in supports multiple selection, search, asynchronous loading and other functions, and can customize the style and template of options. Its ease of use and scalability make it suitable for all types of projects.

1.1 Installation

npm/cnpmor yarninstall

npm i @riophae/vue-treeselect
yarn add @riophae/vue-treeselect

1.2 Introduction

Entry file main.jsglobally imported

import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
Vue.component('treeselect', Treeselect)

Use file local import

<script>
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
    
    
  components: {
    
    
    Treeselect
  },
}
</script>

1.3 Basic usage

<template>
  <div class="box">
    <treeselect v-model="selectedItems" placeholder="请选择" :options="treeData"></treeselect>
    <el-button @click="getSelectedItems">获取选中的数据</el-button>
  </div>
</template>

<script>
export default {
      
      
  data() {
      
      
    return {
      
      
      selectedItems: null,
      treeData: [
        {
      
      
          id: 1,
          label: "水果",
          children: [
            {
      
       id: 2, label: "西瓜" },
            {
      
       id: 3, label: "香蕉" },
            {
      
       id: 4, label: "橙子" },
          ],
        },
        {
      
      
          id: 5,
          label: "蔬菜",
          children: [
            {
      
       id: 6, label: "西红柿" },
            {
      
       id: 7, label: "黄瓜" },
            {
      
       id: 8, label: "青菜" },
          ],
        },
        {
      
      
          id: 9,
          label: "零食",
          children: [
            {
      
       id: 10, label: "薯片" },
            {
      
       id: 11, label: "巧克力" },
          ],
        },
      ],
    };
  },
  methods: {
      
      
    getSelectedItems() {
      
      
      console.log(this.selectedItems);
    },
  },
};
</script>

achieve effect

Insert image description here


1.4 Advanced use

  • Commonly used properties
Attributes describe
v-model=“value” Use the v-model directive to bind the selected item
:options=“treeData” Set tree data
:multiple=“true” Allow multiple selections
:clearable=“true” Show clear button
:searchable=“true” Show search box
:disabled=“false” Whether to disable
:openOnFocus=“true” Automatically expand drop-down menu when focused
:openOnClick=“true” Automatically expand drop-down menu on click
:auto-load-root-options=“true” Automatically load root-level options
:async=“true” Asynchronous loading options
:load-options=“loadOptions” How to load options asynchronously
:noChildrenText="'No sub-options'" Text prompt when there are no sub-options
:noOptionsText="'No options'" Text prompt when there are no options
:noResultsText="'No matching results'" Text prompt when search results are empty
:placeholder="'Please select'" placeholder text
:appendToBody=“true” Whether the popup box is attached to the body element
:normalizer=“normalizeOptions” Normalize option data. Through the normalizer attribute, you can customize the structure of the option data to adapt to the requirements of the plug-in
valueFormat=“” Able to determine the format of the value attribute. When set to "id", the format of the value attribute is id or an array of ids. When set to "object", the format of the value attribute is node or node array
  • Commonly used methods
method describe
@open=“handleOpen” Event triggered when drop-down menu is opened
@close=“handleClose” Event triggered when drop-down menu is closed
@deselect=“handleRemove” Event triggered when the selected item is removed
@search-change=“handleSearch” Events triggered when searching
@select=“handleSelect” Event triggered when an item is selected (not triggered when a value is cleared)
@input=“handleInput” Selected trigger (the first echo will trigger, the clear value will trigger, the value is undefined) is mostly used for v-model two-way binding components to update parent components.
  • Practice code
<template>
  <div>
    <treeselect v-model="selectedItems" :options="treeData" :multiple="true" :clearable="true" :searchable="true" :disabled="false"
      :openOnFocus="true" :openOnClick="true" :auto-load-root-options="true" :async="false" :load-options="loadOptions"
      :noChildrenText="'没有子选项'" :noOptionsText="'没有可选项'" :noResultsText="'没有匹配的结果'" :placeholder="'请选择'" :appendToBody="true"
      @open="handleOpen" @close="handleClose" @input="handleInput" @deselect="handleRemove" @search-change="handleSearch"
      @select="handleSelect"></treeselect>
    <button @click="getSelectedItems">获取选中的数据</button>
  </div>
</template>

<script>
export default {
      
      
  data() {
      
      
    return {
      
      
      selectedItems: [], // 选中的项
      treeData: [
        {
      
      
          id: 1,
          label: "水果",
          children: [
            {
      
       id: 2, label: "西瓜" },
            {
      
       id: 3, label: "香蕉" },
            {
      
       id: 4, label: "橙子" },
          ],
        },
        {
      
      
          id: 5,
          label: "蔬菜",
          children: [
            {
      
       id: 6, label: "西红柿" },
            {
      
       id: 7, label: "黄瓜" },
            {
      
       id: 8, label: "青菜" },
          ],
        },
        {
      
      
          id: 9,
          label: "零食",
          children: [
            {
      
       id: 10, label: "薯片" },
            {
      
       id: 11, label: "巧克力" },
          ],
        },
      ],
    };
  },
  methods: {
      
      
    loadOptions({
       
        parentNode, callback }) {
      
      
      // 异步加载选项的方法
      // parentNode: 当前父节点
      // callback: 加载完成后的回调函数
      // 在这里根据需要进行异步加载选项的操作,并在加载完成后调用callback方法传递选项数据
    },
    handleOpen() {
      
      
      // 下拉菜单打开时触发的事件
      console.log("下拉菜单打开");
    },
    handleClose() {
      
      
      // 下拉菜单关闭时触发的事件
      console.log("下拉菜单关闭");
    },
    handleRemove(removedItem) {
      
      
      // 移除选中项时触发的事件
      console.log("移除选中项", removedItem);
    },
    handleSearch(searchText) {
      
      
      // 搜索时触发的事件
      console.log("搜索", searchText);
    },
    handleSelect(selectedItems) {
      
      
      // 选择项时触发的事件
      console.log("选择项select", selectedItems);
    },
    handleInput(selectedItems) {
      
      
      // 选择项时触发的事件
      console.log("选择项input", selectedItems);
    },
    getSelectedItems() {
      
      
      // 获取选中的数据
      console.log(this.selectedItems);
    },
  },
};
</script>

1.5 FAQ

1.5.1 Placeholdersunknown

  • Screenshot of problem
    Insert image description here

  • Solution

    v-modelIt cannot be written as an empty string or empty array, otherwise it will appear unknown. You can default to it null.

1.5.2 Data prompt in English

  • Screenshot of problem

Insert image description here

  • Solution

    Use the noChildrenText, noOptionsTextand noResultsTextproperties of custom text.

    noChildrenText: Used to define the text prompt when an option has no sub-options. For example, when a category has no subcategories, you can use noChildrenTextto display the corresponding prompt text.
    noOptionsText: Used to define text prompts when there are no options. noOptionsTextFor example, you can use to display the corresponding prompt text when the data is empty or there are no matching options .
    noResultsText: Used to define text prompts when the search results are empty. For example, when the user performs a search but no matching results are found, you can use noResultsTextto display the corresponding prompt text.

    <treeselect v-model="selectedItems" :options="treeData"  
    noChildrenText="没有子选项" noOptionsText="没有可选项" noResultsText="没有匹配的结果"></treeselect>
    

1.5.3 Data structure does not comply with

  • Problem Description

    Many times, the fields of the data structure returned by the background are not id, label, childrenand these. At this time, we need to replace it with a data structure that meets the requirements.

  • Solution

    Use normalizerthe attribute, which is used to normalize option data. Through normalizerproperties, you can customize the structure of the options data to suit the plug-in's requirements.

    <template>
      <div class="box">
        <treeselect v-model="selectedItems" :normalizer="normalizeOptions" :options="treeData"></treeselect>
      </div>
    </template>
    
    <script>
    export default {
            
            
      data() {
            
            
        return {
            
            
          selectedItems: null,
          treeData: [
            {
            
            
              id: 1,
              name: "水果",
              children: [
                {
            
            
                  id: 2,
                  name: "苹果",
                  children: [
                    {
            
             id: 21, name: "红苹果" },
                    {
            
             id: 22, name: "绿苹果" },
                  ],
                },
                {
            
            
                  id: 3,
                  name: "香蕉",
                  children: [
                    {
            
             id: 31, name: "大香蕉" },
                    {
            
             id: 32, name: "小香蕉" },
                  ],
                },
              ],
            },
            {
            
            
              id: 5,
              name: "蔬菜",
              children: [
                {
            
            
                  id: 6,
                  name: "叶菜类",
                  children: [
                    {
            
             id: 61, name: "菠菜" },
                    {
            
             id: 62, name: "生菜" },
                  ],
                },
                {
            
            
                  id: 7,
                  name: "根茎类",
                  children: [],
                },
              ],
            },
          ],
        };
      },
      methods: {
            
            
        // 规范化选项数据的方法
        normalizeOptions(node) {
            
            
          // node: 原始的选项数据
          // 在这里根据需要进行选项数据的规范化操作,并返回规范化后的选项数据
          // 例如,可以将原始的选项数据转换为符合插件要求的结构
          if (node.children && !node.children.length) {
            
            
            // 去掉children=[]的children属性
            delete node.children;
          }
          return {
            
            
            id: node.id,
            label: node.name,
            children: node.children,
          };
        },
      },
    };
    </script>
    

    Of course, you can also implement it manually, recursively.

    <template>
      <div class="box">
        <treeselect v-model="selectedItems" :options="treeData"></treeselect>
      </div>
    </template>
    
    <script>
    export default {
            
            
      data() {
            
            
        return {
            
            
          selectedItems: null,
          treeData: [],
        };
      },
      mounted() {
            
            
        let list = [
          {
            
            
            id: 1,
            name: "层级1",
            children: [
              {
            
            
                id: 2,
                name: "层级2",
                children: [
                  {
            
            
                    id: 3,
                    name: "层级3",
                    children: [
                      {
            
            
                        id: 4,
                        name: "层级4",
                      },
                    ],
                  },
                ],
              },
            ],
          },
        ];
        this.treeData = this.normalizeOptions(list);
      },
      methods: {
            
            
        normalizeOptions(options) {
            
            
          const normalizedOptions = [];
          if (options) {
            
            
            for (const option of options) {
            
            
              // 创建一个规范化选项对象,将id和name属性映射到该对象中
              const normalizedOption = {
            
            
                id: option.id,
                label: option.name,
              };
              // 检查当前选项是否有子选项
              if (option.children && option.children.length > 0) {
            
            
                // 如果有子选项,递归调用normalizeOptions方法对子选项进行规范化
                // 并将规范化后的子选项数组赋值给当前选项的children属性
                normalizedOption.children = this.normalizeOptions(option.children);
              }
              // 将规范化后的选项对象添加到normalizedOptions数组中
              normalizedOptions.push(normalizedOption);
            }
          }
          return normalizedOptions;
        },
      },
    };
    </script>
    

1.5.4 Style adjustment

/* 组件样式 */
::v-deep .vue-treeselect {
    
    
  width: 200px;
  height: 30px;
  line-height: 30px;
  font-size: 18px;
}
/* 内容样式 */
::v-deep .vue-treeselect__control {
    
    
  height: 30px;
  color: blue;
}
/* 占位符样式 */
::v-deep .vue-treeselect__placeholder,
::v-deep .vue-treeselect__single-value {
    
    
  color: red;
}

1.5.5 Get the selected node object instead of a single value

valueFormatProperties determine valuethe format of the property. When set to id, valuethe format of the attribute is idan or idarray. When set to object, valuethe format of the attribute is nodean or nodearray.

  • single value

  • node object

    <treeselect v-model="selectedItems" valueFormat="object" :options="treeData" @input="handleInput"></treeselect>
    

Insert image description here


2. Custom components

Custom properties and methods

Properties/Methods describe type
data display data array
props Configuration options. For specific configuration, please refer to the configuration of el-tree in the element ui library. object
show-checkbox Whether the node can be selected boolean
check-strictly When the check box is displayed, whether to strictly follow the practice of parent and child not being related to each other, the default is false boolean
icon-class Customize icons for tree nodes string
load Method for loading subtree data, only effective when lazy attribute is true function(node, resolve)
lazy Whether to lazily load child nodes needs to be used in conjunction with the load method. boolean
disabled Whether the drop-down box is disabled boolean
getCheckedKeys If the node can be selected (that is, show-checkbox is true), an array consisting of the keys of the currently selected node is returned.
getCurrentNode Get the data of the currently selected node. If no node is selected, return null.
collapse-tags Whether to display the selected value as text when making multiple selections
select-last-node Whether only the last node can be selected during single selection
show-count If there are children in the node, the number of children will be displayed on the parent node. Note that show-count will be invalid when setting the slot.
clearable Is it possible to clear the options when making a single selection? boolean
filterable Enable search boolean

Package file

<template>
  <el-select :value="valueFilter(value)" :placeholder="$attrs['placeholder']" :multiple="$attrs['show-checkbox']"
    :disabled="$attrs['disabled']" :filterable="$attrs['filterable']" :clearable="$attrs['clearable']"
    :collapse-tags="$attrs['collapse-tags']" @change="selectChange" @clear="selectClear" ref="mySelect" :filter-method="remoteMethod">
    <template slot="empty">
      <div class="selecTree">
        <el-tree :data="data" :props="props" @node-click="handleNodeClick" :show-checkbox="$attrs['show-checkbox']"
          :check-strictly="$attrs['check-strictly']" :icon-class="$attrs['icon-class']" :lazy="$attrs['lazy']" :load="$attrs['load']"
          :node-key="props.value" :filter-node-method="filterNode" @check-change="handleCheckChange"
          :default-expanded-keys="defaultExpandedKeys" ref="myTree">
          <template slot-scope="{ node, data }">
            <slot :node="node" :data="data">
              <span class="slotSpan">
                <span>
                  {
   
   { data[props.label] }}
                  <b v-if="$attrs['show-count'] != undefined && data[props.children]">({
   
   { data[props.children].length }})</b>
                </span>
              </span>
            </slot>
          </template>
        </el-tree>
      </div>
    </template>
  </el-select>
</template>
<script>
export default {
      
      
  props: {
      
      
    value: {
      
      
      type: undefined,
      default: null,
    },
    data: {
      
      
      type: Array,
      default: null,
    },
    props: {
      
      
      type: Object,
      default: null,
    },
  },
  data() {
      
      
    return {
      
      
      defaultExpandedKeys: [],
    };
  },
  created() {
      
      
    this.propsInit();
  },
  mounted() {
      
      
    setTimeout(this.initData, 10);
  },
  beforeUpdate() {
      
      
    this.propsInit();
    this.initData();
  },

  methods: {
      
      
    initData() {
      
      
      if (this.$attrs["show-checkbox"] === undefined) {
      
      
        let newItem = this.recurrenceQuery(
          this.data,
          this.props.value,
          this.value
        );
        if (newItem.length) {
      
      
          if (this.props.value && newItem[0][this.props.value]) {
      
      
            this.defaultExpandedKeys = [newItem[0][this.props.value]];
          }
          this.$nextTick(() => {
      
      
            this.$refs.myTree.setCurrentNode(newItem[0]);
          });
        }
      } else {
      
      
        let newValue = JSON.parse(JSON.stringify(this.value));
        if (!(newValue instanceof Array)) {
      
      
          newValue = [newValue];
        }
        if (newValue?.length) {
      
      
          let checkList = newValue.map((key) => {
      
      
            if (key) {
      
      
              let newItem = this.recurrenceQuery(
                this.data,
                this.props.value,
                key
              );
              return newItem[0] || "";
            }
          });
          if (checkList?.length) {
      
      
            let defaultExpandedKeys = checkList.map(
              (item) => item?.[this.props.value || ""]
            );
            if (defaultExpandedKeys.length)
              this.defaultExpandedKeys = defaultExpandedKeys;
            this.$nextTick(() => {
      
      
              this.$refs.myTree.setCheckedNodes(checkList);
            });
          }
        }
      }
      this.$forceUpdate();
    },
    // 多选
    handleCheckChange(data, e, ev) {
      
      
      let checkList = this.$refs.myTree.getCheckedNodes();
      let setList = null;
      if (checkList.length) {
      
      
        setList = checkList.map((item) => item[this.props.value]);
      }
      this.$emit("input", setList);
      // 共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点本身是否被选中、节点的子树中是否有被选中的节点
      this.$emit("change", data, e, ev);
    },
    // 单选事件
    handleNodeClick(data, e) {
      
      
      if (!(this.$attrs["select-last-node"] === undefined)) {
      
      
        if (data[this.props.children] && data[this.props.children]?.length) {
      
      
          return false;
        }
      }
      if (this.$attrs["show-checkbox"] === undefined) {
      
      
        this.$emit("input", data[this.props.value]);
        this.$refs.mySelect.blur();
      }
      this.$emit("change", data, e);
    },
    //   递归查找通用方法
    recurrenceQuery(list, key, value) {
      
      
      if (!list || !key || !value) return [];
      let queryData = [];
      list.map((item) => {
      
      
        if (item[this.props.children] && item[this.props.children].length) {
      
      
          queryData.push(
            ...this.recurrenceQuery(item[this.props.children], key, value)
          );
        }
        if (item[key] == value) {
      
      
          queryData.push(item);
        }
        return item;
      });
      return queryData;
    },
    selectChange(e) {
      
      
      if (this.$attrs["show-checkbox"] !== undefined) {
      
      
        let checkList = e.map((key) => {
      
      
          let newItem = this.recurrenceQuery(this.data, this.props.label, key);
          return newItem[0] || "";
        });
        this.$refs.myTree.setCheckedNodes(checkList);
        this.$emit("input", e);
      }
    },
    selectClear(flag) {
      
      
      if (this.$attrs["show-checkbox"] === undefined) {
      
      
        if (!flag) this.$emit("input", "");
        this.$refs.myTree.setCurrentKey(null);
      } else {
      
      
        if (!flag) this.$emit("input", []);
        this.$refs.myTree.setCheckedKeys([]);
      }
      this.remoteMethod("");
    },
    getCheckedNodes() {
      
      
      if (
        this.value !== null &&
        this.value !== undefined &&
        this.value !== ""
      ) {
      
      
        return this.$refs.myTree.getCheckedNodes();
      }
      return [];
    },
    getCurrentNode() {
      
      
      if (
        this.value !== null &&
        this.value !== undefined &&
        this.value !== ""
      ) {
      
      
        return this.$refs.myTree.getCurrentNode();
      }
      return null;
    },
    valueFilter(val) {
      
      
      if (this.$attrs["show-checkbox"] === undefined) {
      
      
        let res = "";
        [res] = this.recurrenceQuery(this.data, this.props.value, val);
        return res?.[this.props.label] || "";
      } else {
      
      
        if (!val?.length) return [];
        let res = val.map((item) => {
      
      
          let [newItem] = this.recurrenceQuery(
            this.data,
            this.props.value,
            item
          );
          return newItem?.[this.props.label] || "";
        });
        if (!res?.length) return [];
        res = res.filter((item) => item);
        return res;
      }
    },
    propsInit() {
      
      
      this.props.label = this.props.label || "label";
      this.props.value = this.props.value || "value";
      this.props.children = this.props.children || "children";
      if (
        this.$attrs["select-last-node"] !== undefined &&
        !this.props.disabled
      ) {
      
      
        this.props.disabled = (data) => data?.[this.props.children]?.length;
        this.$attrs["check-strictly"] = true;
      }
    },

    remoteMethod(query) {
      
      
      this.$refs.myTree.filter(query);
    },
    filterNode(value, data) {
      
      
      if (!value) return true;
      return data[this.props.label].indexOf(value) !== -1;
    },
  },

  watch: {
      
      
    value: {
      
      
      deep: true,
      handler(val) {
      
      
        if (!val || !val?.length) {
      
      
          this.selectClear(true);
        }
      },
    },
  },
};
</script>

working with files

<template>
  <div class="box">
    <tree-select @change="sendSelectedValue" v-model="value" :data="treeData" :props="treeProps" filterable clearable></tree-select>
  </div>
</template>

<script>
import TreeSelect from "@/components/treeSelect";
export default {
      
      
  components: {
      
      
    TreeSelect,
  },
  data() {
      
      
    return {
      
      
      value: "",
      treeData: [
        {
      
      
          id: 1,
          name: "水果",
          children: [
            {
      
      
              id: 2,
              name: "苹果",
              children: [
                {
      
       id: 21, name: "红苹果" },
                {
      
       id: 22, name: "绿苹果" },
              ],
            },
            {
      
      
              id: 3,
              name: "香蕉",
              children: [
                {
      
       id: 31, name: "大香蕉" },
                {
      
       id: 32, name: "小香蕉" },
              ],
            },
          ],
        },
        {
      
      
          id: 5,
          name: "蔬菜",
          children: [
            {
      
      
              id: 6,
              name: "叶菜类",
              children: [
                {
      
       id: 61, name: "菠菜" },
                {
      
       id: 62, name: "生菜" },
              ],
            },
            {
      
      
              id: 7,
              name: "根茎类",
              children: [],
            },
          ],
        },
      ],
      // 配置项
      treeProps: {
      
      
        label: "name", // 树节点的文本字段
        value: "id", // 树节点的值字段
        children: "children", // 树节点的子节点字段
        disabled: (data) => data.disabled, // 禁用节点的条件函数,接收一个参数 data,返回一个布尔值
        iconClass: "custom-icon", // 自定义树节点的图标样式
        checkStrictly: true, // 在显示复选框的情况下,是否严格遵循父子节点不互相关联的做法
        load: (node, resolve) => {
      
      }, // 加载子树数据的方法,仅当 lazy 属性为 true 时生效
        lazy: true, // 是否懒加载子节点,需与 load 方法结合使用
        collapseTags: true, // 多选时是否将选中值按文字的形式展示
        selectLastNode: true, // 单选时是否只能选择最后一个节点
        showCount: true, // 若节点中存在 children,则在父节点展示所属 children 的数量
      },
    };
  },
  methods: {
      
      
    sendSelectedValue(e) {
      
      
      console.log(e);
    },
  },
};
</script>

achieve effect

Insert image description here

Guess you like

Origin blog.csdn.net/Shids_/article/details/134872897