preface

Hello everyone, I am Xu Zhu, some time ago I was busy looking for the trace of eldest brother and third brother, delayed the output of technical article, while halfway habitat time hurriedly fill a hydrological article. Today, I would like to tell you about my digging trip, because I recently developed a project and encountered a complex requirement (directory tree + check box + paging + search). Most of the big cattle looked at the functional requirements, will immediately jump out, is not difficult, this is the development of a network disk yao, similar to Baidu network disk that kind. No, no, no, no! Let’s see!

At that time, there were two options to use either El-Tree or El-Table. After a brief review of the usage of these two UI components, I started to use El-Table in ElementUI based on the actual business needs and just for the sake of leisure. The development went well in the middle of the process, but I found something wrong with it, so I pulled it down and started from scratch. Oh, my God, that’s how you make a programmer. Come on! After careful research, the combination of el-Tree and El-Table was finally selected for the business component. After repeated trials and trials, it could meet most of the business requirements. After making some requirements felt not very reasonable, I communicated with the back end of the product to make some adjustments in business implementation.

Application scenarios

According to project the actual needs of the business made a simple Demo version, Demo address 👉 : http://106.55.168.13:8083/

Welcome to discuss and exchange with you, give some suggestions or other optimal solutions, also can put forward some bugs, put forward some strange requirements, see if I can achieve, ha ha. 🤟

At present, there is a new requirement to seek guidance from the big guys, is such a total of two levels of directory nesting, the first and second level of directory list should be paginated display with retrieval function, and ensure that the second level of directory can also be paginated (load more). 👋

Functional Requirement Description

  • Display of directory tree structure list (backend does not support paging)
  • The directory tree is fully expanded by default
  • Handles the timedate JS library day.js
  • Different ICONS are displayed according to the file suffix
  • Configure the global pipe method
  • File size unit change
  • The directory tree check box is an option
  • All the selected
  • Mouse over icon file name prompt
  • Recursive algorithm counts the total number of files
  • Quick retrieval of directories or files (backend does not support bulk downloading and sharing)
  • Batch download and share
  • Download and share a single file

The development of this function involves recursive algorithms. Let’s first briefly popularize what recursive algorithms are, what scenes they will appear in and what problems they can solve.

The notion of recursion

That is, a function calls itself, or calls itself from a function that is a descendant of its own function.

Recursive steps

  • So let’s say the recursive function is already written
  • Look for recursive relationships
  • Transform the structure of a recursive relationship into a recursive body
  • Add critical conditions to the recursive body

The introduction of recursion articles can be found everywhere, xiaobi recommend a few to see:

  • Recursion of JavaScript topics
  • Designing recursive functions is surprisingly easy
  • Understand recursion in JavaScript

Code implementation

Technology stack: Vue2.6 + VUE-Router + ElementUI + Less + Flex layout

El – tree components

The el-Tree component in ElementUI was used for secondary development, and the form was customized. Features: very clear hierarchical nesting of display information, selectable, lazy loading, expansion and collapse, or drag-and-drop nodes. For example, if you select a parent node, all its children will be selected, and if you select a child node, all its children will be retained. In general, the parent node can be selected to meet most service requirements. The only downside is that El-Tree is not suitable for pagination and keyword retrieval.

The template code looks like this:

<div class="tree-box">
  <div class="tree-nav">
    <div class="item">
      <el-checkbox
        v-model="isCheckedAll"
        :indeterminate="isIndeterminate"
        :disabled="treeData.length === 0"
        class="checkbox-style"
        @change="handleCheckAllChange"> </el-checkbox > name </div> <div class="item"> size </div> <div class="item"<div > <div class="item"<div > <div class="item"> Encryption level </div> <div class="item"> Download level </div> <div class="item"> action </div> </div> <div v-loading="loading" class="tree-content">
    <el-tree
      ref="tree"
      :data="treeData"
      node-key="directoryId"
      :props="props"
      show-checkbox
      default-expand-all
      @check="handleCheckChange"
      @check-change="handleCurChange"
    >
      <span slot-scope="{ node, data }" class="custom-tree-node">
        <template>
          <div v-if="data.directoryType === 1" class="node_div">
            <span class="name-box">
              <el-tooltip effect="dark" placement="left">
                <div slot="content">
                  {{ node.label }}
                </div>
                <i class="file-icon icon-folder"></i>
              </el-tooltip>
              {{ node.label }}
            </span>
          </div>
          <div v-if="data.directoryType === 2" class="node_div">
            <span class="name-box" :title="node.label">
              <el-tooltip effect="dark" placement="left">
                <div slot="content">
                  {{ node.label }}
                </div>
                <i :class="node.label | getIcon"></i>
              </el-tooltip>
              {{ node.label }}
            </span>
            <span class="size-box">
              {{ data.size | renderSize }}
            </span>
            <span class="time-box">
              {{ $DayTime(data.gmtUpdate).format("YYYY-MM-DD HH:mm") }}
            </span>
            <span class="upload-box">
              {{ $DayTime(data.gmtUpload).format("YYYY-MM-DD HH:mm") }}
            </span>
            <span class="secret-box">
              {{ data.secretType | secretType }}
            </span>
            <span class="download-box">
              {{ data.downloadType | downloadStatus }}
            </span>
            <span class="operate-box">
              <el-button
                v-if="data.downloadType === 1 && data.directoryType === 2"
                type="text"
                size="small"
                @click="() => handleDownload(data, 2)"</el-button > <el-button v-if="data.directoryType === 2"
                type="text"
                size="small"
                @click="() => handleShare(data, 2)"> share < / el - button > < / span > < / div > < / template > < / span > < / el - tree > < / div > < / div >Copy the code

The data code is as follows:

data() {
    return {
        isCheckedAll: false, // Whether to select state isIndeterminate:false, // Whether the semi-selected state isMultipleDownload:true, // Whether to disable isDownloadFile:true, // Whether all selected files can be downloaded isDownloadFileBtn:true, // newTreeArray: [], // filter to keep the selected new array totalNum: 0, // Count the total number of files selectTotalNum: 0, // Select the total number of files props: {// Configure option children:"children",
          label: "name",
          isLeaf: "leaf"}, treeData: [// Initializes the directory tree list data {directoryId: 1, directoryType: 2, // 1: directory 2: file downloadType: 1, secretType: 0, size: 12367, name:"Top end factory interview guide. PDF",
          gmtUpdate: 1630825270483,
          gmtUpload: 1630825248029,
          children: [],
        },
        {
          directoryId: 2,
          directoryType: 2,
          downloadType: 1,
          secretType: 1,
          size: 5236700,
          name: "Front End senior engineer inside secret. Docx",
          gmtUpdate: 1630825270483,
          gmtUpload: 1630825248029,
          children: [],
        },
        {
          directoryId: 3,
          directoryType: 2,
          downloadType: 0,
          secretType: 1,
          size: 2267,
          name: "Front-end learning roadmap. PNG",
          gmtUpdate: 1630834889072,
          gmtUpload: 1630825248029,
          children: [],
        },
        {
          directoryId: 4,
          directoryType: 1,
          downloadType: 1,
          secretType: 0,
          name: "Front-end Open Source Project Summary",
          gmtUpdate: 1630825270483,
          gmtUpload: 1630825248029,
          children: [
            {
              directoryId: 41,
              directoryType: 2,
              downloadType: 1,
              secretType: 0,
              size: 13200,
              name: "Small program personality resume source.zip",
              gmtUpdate: 1630825270483,
              gmtUpload: 1630825248029,
              children: [],
            },
            {
              directoryId: 42,
              directoryType: 1,
              downloadType: 1,
              secretType: 0,
              name: "E-commerce Website Project",
              gmtUpdate: 1630825270483,
              gmtUpload: 1630825248029,
              children: [
                {
                  directoryId: 421,
                  directoryType: 2,
                  downloadType: 1,
                  secretType: 0,
                  size: 132008,
                  name: "Ele. me H5 mobile terminal source.zip",
                  gmtUpdate: 1630825270483,
                  gmtUpload: 1630825248029,
                  children: [],
                },
              ],
            },
          ],
        },
        {
          directoryId: 5,
          directoryType: 1,
          downloadType: 0,
          secretType: 1,
          name: "Front-end Engineering Knowledge System",
          gmtUpdate: 1630834889072,
          gmtUpload: 1630834889072,
          children: [
            {
              directoryId: 51,
              directoryType: 2,
              downloadType: 0,
              secretType: 1,
              size: 13200,
              name: "CI/CD project deploy.doc",
              gmtUpdate: 1630834889072,
              gmtUpload: 1630834889072,
              children: [],
            },
            {
              directoryId: 52,
              directoryType: 2,
              downloadType: 0,
              secretType: 1,
              size: 335200,
              name: "Front End Development Specification Secret. XLSX",
              gmtUpdate: 1630834889072,
              gmtUpload: 1630834889072,
              children: [],
            },
          ],
        },
      ]
    }
}
Copy the code

Script code is as follows:

methods: {
    // Whether to select all
    async handleCheckAllChange(val) {
      let tree = this.treeData;
      this.isIndeterminate = false;
      if (val) {
        / / all
        this.isMultipleDownload = tree[0].downloadType === 0;
        this.isDownloadFileBtn = this.isMultipleDownload;
        this.$refs.tree.setCheckedNodes(tree);
      } else {
        // Deselect all
        this.$refs.tree.setCheckedNodes([]);
        this.isMultipleDownload = true;
        this.isDownloadFile = true;
      }
      this.selectTotalNum = 0;
      await this.getRecursion(tree);
      this.newTreeArray = await this.getFilterFile(tree);
    },
    // Triggered when the checkbox is clicked
    async handleCheckChange(data, node) {
      let tree = this.treeData;
      this.selectTotalNum = 0;
      await this.getRecursion(tree);
      this.isDownloadFileBtn = data.downloadType === 0 && data.isChecked;
      this.newTreeArray = await this.getFilterFile(tree);
      this.isCheckedAll = this.newTreeArray.length === tree.length;
      this.isIndeterminate =
        this.newTreeArray.length > 0 && this.newTreeArray.length < tree.length;

      if (node.checkedNodes.length > 0) {
        this.isMultipleShare = false;
        this.isMultipleDownload = node.checkedNodes[0].downloadType === 0;
      } else {
        this.isCheckedAll = false;
        this.isIndeterminate = false;
        this.isMultipleDownload = true;
        this.isDownloadFile = true;
        this.newTreeArray = []; }},// The node selects the callback when the state changes
    handleCurChange(data, checked, indeterminate) {
      let isChecked = checked;
      let arr = [];
      arr.push(data);
      this.getCheckedChild(arr, [], isChecked, indeterminate);
    },
    // Recursively set all subsets to check state isChecked
    async getCheckedChild(data, arr, flag, isParent) {
      return data.map(async (item) => {
        if (flag) {
          item.isChecked = true;
        } else {
          item.isChecked = false;
        }
        if (isParent && item.directoryType === 1) {
          item.isChecked = true;
        }
        if (item.children) {
          await this.getCheckedChild(item.children, arr, flag, isParent);
        }
        return item;
      });
    }
    // Count the total number of selected files
    async getRecursion(tree) {
      this.$nextTick(async () => {
        tree.map(async (item) => {
          if (item.directoryType === 2 && item.isChecked) {
            this.selectTotalNum += 1;
          }
          if (item.children) {
            await this.getRecursion(item.children); }}); }); },// Recursive filtering preserves the selected directory tree array to the back end
    getFilterFile(tree) {
      return tree
        .filter((item) = > item.isChecked === true)
        .map((item) = > {
          item = Object.assign({}, item);
          if (item.children) {
            item.children = this.getFilterFile(item.children);
          }
          returnitem; }); }},Copy the code

El – table component

The el-Table component in ElementUI is used for secondary development to realize the directory tree list structure.

Features: Simple table list style, support paging search function, display multiple similar structure of data, data sorting, screening, comparison or other customized operations. With the tree-props property configuration option, the row-key is bound to the directoryId variable, which is the unique value of the data. For example, if a parent node is selected, all child nodes are selected. The only drawback is that el-table can’t make the parent nodes select the child nodes together, because there are too many nested parent nodes. At that time, I have been racking my brains, but I can’t come up with an effective solution, if you know, please don’t be afraid to comment.

The template code looks like this:

<div class="tree-header">
  <div class="tree-btn">
    <el-button
      type="primary"
      size="small"
      plain
      :disabled="multiple"
      @click="handleDownload(null, 1)"</el-button > <el-buttontype="primary"
      size="small"
      plain
      :disabled="multiple"
      @click="handleShare(null, 1)"</el-button > </div> <div class="total-num"</div> <div class= {{totalNum}} </div> </div> <div class="tree-box">
  <el-table
    ref="table"
    v-loading="loading"
    :data="tableData"
    class="w100"
    row-key="directoryId"
    default-expand-all
    :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
    @select="selectRow"
    @select-all="selectAll"
    @selection-change="handleSelectionChange"
  >
    <el-table-column
      type="selection"
      :selectable="selectable"
      width="45"
      align="center"
      label="Future generations"
    ></el-table-column>
    <el-table-column label="Name" show-overflow-tooltip>
      <template slot-scope="scope">
        <el-tooltip effect="dark" placement="left">
          <div slot="content">
            {{ scope.row.name }}
          </div>
          <i
            v-if="scope.row.directoryType === 1"
            class="file-icon icon-folder"
          ></i>
          <i v-else :class="scope.row.name | getIcon"></i>
        </el-tooltip>
        {{ scope.row.name }}
      </template>
    </el-table-column>
    <el-table-column
      prop="size"
      label="Size"
      align="center"
      width="100"
      show-overflow-tooltip
    >
      <template slot-scope="scope" v-if="scope.row.directoryType === 2">
        <span> {{ scope.row.size | renderSize }} </span>
      </template>
    </el-table-column>
    <el-table-column
      prop="gmtUpdate"
      label="Modification time"
      align="center"
      width="200"
      show-overflow-tooltip
    >
      <template slot-scope="scope" v-if="scope.row.directoryType === 2">
        <span>{{
          scope.row.gmtUpdate
            ? $DayTime(scope.row.gmtUpdate).format("YYYY-MM-DD HH:mm")
            : null
        }}</span>
      </template>
    </el-table-column>
    <el-table-column
      label="Upload time"
      align="center"
      width="200"
      show-overflow-tooltip
    >
      <template slot-scope="scope" v-if="scope.row.directoryType === 2">
        <span>{{
          $DayTime(scope.row.gmtUpload).format("YYYY-MM-DD HH:mm")
        }}</span>
      </template>
    </el-table-column>
    <el-table-column
      prop="secretType"
      label="Encryption level"
      align="center"
      width="100"
    >
      <template slot-scope="scope" v-if="scope.row.directoryType === 2">
        {{ scope.row.secretType | secretType }}
      </template>
    </el-table-column>
    <el-table-column
      prop="downloadType"
      label="Download Level"
      align="center"
      width="100"
    >
      <template slot-scope="scope" v-if="scope.row.directoryType === 2">
        {{ scope.row.downloadType | downloadStatus }}
      </template>
    </el-table-column>
    <el-table-column label="Operation" align="center" width="200">
      <template slot-scope="scope">
        <el-button
          v-if=" scope.row.downloadType === 1 && scope.row.directoryType === 2 "
          type="text"
          size="small"
          @click="() => handleDownload(scope.row, 2)"</el-button > <el-button v-if="scope.row.directoryType === 2"
          type="text"
          size="small"
          @click="() => handleShare(scope.row, 2)"> share < / el - button > < / template > < / el - table - column > < / el - table > < / div >Copy the code

The data code is as follows:

data() {
    returnSelectTotalNum: 0, // Select ids: [], // select single:true// Disable multiple:true, // disable downloadTypeArr: [], tableData: [], // Directory tree structure is the same as el-tree}}Copy the code

Script code is as follows:

methods: {
    // Select all/cancel the select operation
    selectAll() {
      console.log("All = =".this.tableData);
      this.selectTotalNum = 0;
      this.getRecursion(this.tableData);
      let data = this.tableData;
      this.isAllSelect = !this.isAllSelect;
      this.toggleSelect(data, this.isAllSelect, "all");
    },
    // Select a row
    selectRow(selection, row) {
      console.log("Select a row ===", row);
      this.selectTotalNum = 0;
      this.getRecursion(this.tableData);
      this.$set(row, "isChecked", !row.isChecked);
      this.$nextTick(() = > {
        this.isAllSelect = row.isChecked;
        this.toggleSelect(selection, row.isChecked, "tr");
        this.toggleSelect(row, row.isChecked, "tr");
      });
    },
    // Change the selection
    toggleSelection(row, flag) {
      this.$set(row, "isChecked", flag);
      this.$nextTick(() = > {
        if (flag) {
          this.$refs.table.toggleRowSelection(row, flag);
        } else {
          this.$refs.table.clearSelection(); }}); },// Recursive sublevel
    toggleSelect(data, flag, type) {
      if (type === "all") {
        if (data.length > 0) {
          data.forEach((item) = > {
            this.toggleSelection(item, flag);
            if (item.children && item.children.length > 0) {
              this.toggleSelect(item.children, flag, type); }}); }}else {
        if (data.children && data.children.length > 0) {
          data.children.forEach((item) = >{ item.isChecked = ! item.isChecked;this.$refs.table.toggleRowSelection(item, flag);
            this.toggleSelect(item, flag, type); }); }}},// Count the total number of files in the list (can also directly return the total number of files)
    getTotalNum(tree) {
      for (let item of tree) {
        if (item.directoryType === 2) {
          this.totalNum += 1;
        }
        if (item.children) {
          this.getTotalNum(item.children); }}},// Count the number of selected files
    getRecursion(tree) {
      this.$nextTick(async () => {
        tree.map(async (item) => {
          if (item.directoryType === 2 && item.isChecked) {
            this.selectTotalNum += 1;
          }
          if (item.children) {
            this.getRecursion(item.children); }}); }); },// This event is triggered when the selection changes
    handleSelectionChange(selection) {
      this.ids = selection.map((item) = > item.directoryId);
      this.downloadTypeArr = selection.map((item) = > item.downloadType);
      this.single = selection.length ! = =1;
      this.multiple = ! selection.length; }},Copy the code

conclusion

Above are some practical notes I summarized after two all-nighters of practice. I feel that I have gained the most from this project. I have learned multiple uses of recursion and the tree-like structure of pit digging and pit filling, which has improved my ability to solve various complex business needs. In order to pursue perfection (the premise does not change the demand does not cut demand), I wrote this article on hydrology, hoping to gain more valuable opinions or suggestions from leaders. Because my skill is still shallow, but also need to be closed cultivation.

❤️ Thanks for support

If this article is of any help to you, please give me a thumbs up. Your “thumbs up” is the motivation for my writing.

Pay attention to my public number [lazy code farmers], get more project actual combat experience and a variety of source resources. If you also love technology and are fascinated by it, please add my wechat [LazyCode520], and we will invite you to join our front-end practice learning group to fix problems and program for happiness ~ 🦄