树形结构复选框【勾选-搜索-再勾选】功能

297 阅读2分钟

接上一篇table列表复现框【勾选-搜索-再勾选】功能,存在数据结构非【数组】而是【树形结构】的数据,在实现【勾选-搜索-再勾选】功能时,有一定的差异点,做记录如下:

一、代码实现

第一步:封装查询企业名称弹窗组件

<template>
  <el-dialog
    width="40%"
    title="企业查询"
    append-to-body
    class="search-enterprise"
    :visible.sync="enterpriseDialogVisible"
    :before-close="closeDialog"
  >
    <div class="search-box">
      <span class="search-box-name">企业名称:</span>
      <el-input
        suffix-icon="el-icon-search"
        v-model.trim="enterpriseName"
        placeholder="请输入"
      ></el-input>
      <el-button type="primary" class="search-btn" @click="handQueryOrgs">
        查 询
      </el-button>
    </div>
    <div class="tree-box" ref="treeBox" @scroll="handleScroll">
      <el-tree
        ref="tree"
        show-checkbox
        :data="treedata"
        node-key="pk_org"
        default-expand-all
        :expand-on-click-node="false"
        :props="defaultProps"
        @check="handleCheckChange"
      ></el-tree>
    </div>
    <div slot="footer">
      <el-button @click="submitVal" type="primary" class="submit-btn">
        确 认
      </el-button>
      <el-button @click="closeDialog()" class="cancel-btn">取 消</el-button>
    </div>
  </el-dialog>
</template>
<script>
import commonService from '@/api/common.js';

export default {
  props: {
    enterpriseDialogVisible: {
      type: [Boolean],
      default: false,
    },
    pkGroups: {
      type: [Array],
      default: [], // 企业基于集团
    },
    pkPlans: {
      type: [Array],
      default: [], // 集团基于计划
    },
    pkOrgs: {
      type: [Array],
      default: [], // 企业
    },
  },

  data() {
    return {
      scrollTop: 0,
      treedata: [],
      searchSaveSelectId: [],
      searchSaveSelectData: [],
      selectedId: [], // 选中的节点的主键集合
      selectedData: [], // 选中的树结构
      enterpriseName: '',
      defaultProps: {
        children: 'children',
        label: 'org_name',
      },
    };
  },

  watch: {
    // 弹窗出现标识
    enterpriseDialogVisible(val) {
      if (val) {
        this.enterpriseName = '';
        if (this.pkGroups && this.pkGroups.length) {
          this.queryOrgs();
        } else {
          this.$nextTick(() => {
            this.$refs.tree.setCheckedKeys([]);
            this.treedata = [];
          });
        }
      }
    },

    // 集团选中pks
    pkGroups(val, oldval) {
      let haveOldValFlag = oldval && oldval.length; // 老数据有值
      let haveValFlag = val && val.length && val.join(',') != oldval.join(','); // 新老数据不同
      this.$nextTick(() => {
        if (haveOldValFlag && haveValFlag) {
          this.$refs.tree.setCheckedKeys([]);
        }
      });
    },

    // 企业选中id
    selectedId(val, old) {
      if (this.pkGroups && this.pkGroups.length) {
        this.$refs.tree.setCheckedKeys(val);
      }
    },
  },

  methods: {
    // 手动搜索
    handQueryOrgs() {
      this.pagenumber = 1; // 重置页码
      if (this.pkGroups && this.pkGroups.length) {
        this.searchSaveSelectId = this.selectedId; // 手动搜索时保存之前选中的id
        this.searchSaveSelectData = this.selectedData; // 手动搜索时保存之前选中的数据
        this.queryOrgs();
      } else {
        this.$message.error('请先选择集团名称!');
      }
    },

    // 获取企业名称
    queryOrgs() {
      this.treedata = [];
      let params = {
        pk_groups: this.pkGroups,
        pk_plans: this.pkPlans,
        org_name: this.enterpriseName ? this.enterpriseName : '',
      };

      commonService.queryOrgs(params).then((res) => {
        if (res && res.code == 200) {
          if (res.data) {
            this.treedata = res.data;
          }
        } else {
          this.$message.error(res.message);
        }
      });
    },

    // 实现的是勾选-搜索-再勾选的逻辑
    handleCheckChange() {
      this.selectedId = this.$refs.tree.getCheckedKeys(); // 旧的id
      this.selectedId = this.searchSaveSelectId.concat(this.selectedId); // 再次赋值id
      this.selectedData = this.$refs.tree.getCheckedNodes(); // 旧的数据
      this.selectedData = this.searchSaveSelectData.concat(this.selectedData); // 再次赋值数据
    },

    // el-tree将选中的节点以tree结构形式数组返回
    getDeepTree() {
      const getC = this.$refs.tree.getCheckedNodes();
      const getHC = this.$refs.tree.getHalfCheckedNodes();
      let selected_nodes = getHC.concat(getC);
      let selected_ids = new Set(); // 记录选中的节点tree_id
      let used_ids = new Set(); // 记录已经查询过的节点
      // 遍历初始化selected_ids
      for (let node of selected_nodes) {
        selected_ids.add(node.$treeNodeId);
      }
      // 最终的结果数据用一个根节点存储, 为了对齐递归的数据结构
      let root_node = {
        treeNodeId: -1,
        children: [],
      };
      // 添加给parent_node节点一个新节点 node
      // 注意:node 的类型是原始的 tree 节点
      // 而parent_node 的类型是真正的结果节点
      function AddNode(node, parent_node) {
        if (
          !selected_ids.has(node.$treeNodeId) ||
          used_ids.has(node.$treeNodeId)
        ) {
          return;
        }
        used_ids.add(node.$treeNodeId); // 加过的要存入 used_ids 做标记
        let real_node = {
          ...node,
          children: [],
          treeNodeId: node.$treeNodeId,
          // 存储一下节点的数据
        };
        // // 添加子节点
        if (node.children) {
          for (let child_node of node.children) {
            AddNode(child_node, real_node);
          }
        }
        if (real_node.children.length === 0) {
          delete real_node.children;
        }
        // 如果数据没有时,删除数据
        if (!real_node.pk_org) {
          delete real_node.pk_org;
        }
        if (!real_node.org_name) {
          delete real_node.org_name;
        }
        if (!real_node.org_code) {
          delete real_node.org_code;
        }
        if (!real_node.org_level) {
          delete real_node.org_level;
        }
        if (!real_node.parent_pk) {
          delete real_node.parent_pk;
        }
        if (!real_node.one_level_org) {
          delete real_node.one_level_org;
        }
        parent_node.children.push(real_node);
      }
      for (let node of selected_nodes) {
        AddNode(node, root_node);
      }
      return root_node.children;
    },

    // 确认选中的企业,回显在父组件输入框
    submitVal() {
      if (this.selectedData.length == 0) {
        this.$message.error('请选择企业名称!');
        return false;
      } else {
        let sendObjInfo = this.getDeepTree();
        this.$emit('submit', this.selectedData, sendObjInfo);
        this.closeDialog();
      }
    },

    // 关闭弹窗
    closeDialog() {
      this.$emit('close');
    },

    // 弹窗高度
    handleScroll() {
      this.scrollTop = this.$refs.treeBox.scrollTop;
    },
  },
};
</script>
<style scoped>
.search-enterprise >>> .el-dialog__body {
  padding: 10px 30px 0px;
  border-top: 1px solid #ececec;
}
.search-enterprise >>> .el-dialog__header {
  padding: 15px 20px;
}
.search-enterprise >>> .el-dialog__headerbtn {
  top: 12px;
}

.search-enterprise >>> .el-dialog__headerbtn .el-dialog__close {
  font-size: 25px;
  display: block;
}

.search-box >>> .el-input {
  width: 230px;
}
.search-box >>> .el-input--suffix .el-input__inner {
  height: 32px;
}

.search-box {
  display: flex;
  flex-direction: row;
  align-items: center;
  padding-left: 10px;
}

.search-box >>> .el-input {
  width: 266px;
}

.search-box >>> .el-input input {
  width: 266px;
  height: 32px;
}

.search-box >>> .el-input__icon {
  line-height: 32px;
}
.search-box-name {
  flex-shrink: 0;
}
.tree-box {
  margin-top: 16px;
  padding: 0 12px;
  min-height: 300px;
  max-height: 500px;
  overflow: auto;
}

.search-btn {
  width: 85px;
  height: 32px;
  padding: 0;
  margin-left: 16px;
  background-color: #2e78ff;
}

.submit-btn {
  width: 64px;
  height: 32px;
  padding: 0;
  background-color: #2e78ff;
}

.cancel-btn {
  width: 64px;
  height: 32px;
  padding: 0;
  background-color: #fafafa;
  border-color: #ddd;
  color: #666;
  margin-left: 4px;
}
</style>

第二步:组件应用

<template>
    <EnterpriseDialog
      :enterpriseDialogVisible="enterpriseDialogVisible"
      @close="closeEnterpriseDialog"
      @submit="getEnterpriseData"
      :pkGroups="form.pk_groups"
      :pkPlans="form.pk_plans"
      :pkOrgs="form.pk_orgs"
    ></EnterpriseDialog>
</template>

第三步:组件方法

<script>
import EnterpriseDialog from '@/components/searchEnterprise';
export default {
 components: {
   EnterpriseDialog
 },
 data(){
  return {
    form: {
       pk_plans: [],
       plan_name: '',
       pk_groups: [],
       group_name: '',
       org_name: '',
       pk_orgs: [],
    },
    enterpriseDialogVisible: false, // 企业弹窗
  }
 },
 methods:{
     // 企业名称递归
     getAllOrgPks (tree) => {
        let pksResult = [];
        let pkNamesResult = [];
        for (const i in tree) {
            pksResult.push(tree[i].pk_org); // 遍历项目满足条件后的操作
            pkNamesResult.push(tree[i].org_name); // 遍历项目满足条件后的操作
            if (tree[i].children) {
                this.getAllOrgPks(tree[i].children); // 存在子节点就递归
            }
        }
        return [pksResult, pkNamesResult];
    },
    
    // 清空企业组件数据
    clearEnterpriseInfo() {
      this.form.org_name = '';
      this.form.pk_orgs = [];
    },
    
    // 选中企业
    getEnterpriseData(val, info) {
      this.clearEnterpriseInfo(); // 清空企业组件数据
      let [pksResult, pkNamesResult] = this.getAllOrgPks(val);
      this.form.pk_orgs = pksResult;
      this.form.org_name = pkNamesResult.join(',');
      this.treedata = info;
    },

    // 关闭企业弹框
    closeEnterpriseDialog() {
      this.enterpriseDialogVisible = false;
    },
 }
}
</script>

第四步:企业数据树再次展示(具体问题具体分析:不确定反复【扁平化】或者【成树操作】方法的相互影响)

  1. 在企业名称弹窗中拿到的数据,需要再次以树形结构的形式展示出来
    1.1 现象:存在父子关系的children数据会被单独拎出来作为一项展示;
    1.2 思路:打印企业名称选择时获取到的数据和后续拿到数据后再次进行数据checkId去重数据递归成数操作等各个方法处理时得到的数据;
    1.3 结论:在最初选中企业名称时使用的getCheckNodes方法,拿到的数据就是【现象】呈现出来的效果
  2. 由得出的结论,思考解决问题
    2.1 递归数据:拿到所有的有children的每一个数据项(思路梳理不清晰,无法找出合适的节点,找到对应的筛选条件;将满足条件的数据给存放起来)
    2.2找度娘:el-tree将选中的节点以tree结构形式数组返回,整理思路,方法getDeepTree拿到的就是纯选中数据的树形结构
二、总结
  1. 组件封装,关注每次弹窗数据【重置】或【清空】按钮时,组件中各个方面数据的情况问题;
  2. 遇到问题时,除了梳理自己思路以外,借助网络查询到的结果,切勿眼高手低嫌麻烦不去尝试,试着理解别人文章中的注释,放置自己场景中进行打印,查看效果