参考vue文件地址:递归组件

实现遍历效果
在这里插入图片描述

递归组件

实现思路:

  1. 遍历数组元素,并判断子元素的子集数量是否大于0。
  2. 如果子集的集合数量大于0,则需要需要再一次调用该组件。
  3. 如果子集的集合数量等于0,则直接显示内容。

创建组件

	<!-- subordinate:判断是否为子集递归; activeNames:打开折叠面板的集合。 -->
    <van-collapse v-if="!subordinate" v-model="activeNames">
      <!-- 遍历数组 -->
      <van-collapse-item v-for="(item, index) in list" :key="index" :name="item.id" :border="false">
        <!-- 折叠面板的title数据 -->
        <template slot="title">
          <div class="checkBox">
            <span class="iconfont active" v-if="item.checked" @click.stop.prevent="clickBoxFn(item, item.id)">&#xe66a;</span>
            <span class="noactive" v-else @click.stop.prevent="clickBoxFn(item, item.id)"></span>
            <div @click.stop.prevent="openForm(item)" class="checktypeName first">{{item.title}}</div>
          </div>
        </template>
        <!-- 遍历当前元素的子集元素 -->
        <div class="cheackBoxList" v-for="c of item.children" :key="c.id">
          <!-- 判断子集元素的子集集合长度是否大于0,如果大于0则需要重新调用当前组件 -->
          <div v-if="c.children.length > 0">

            <!--  editfn: 编辑事件,点击title文字,会打开编辑框,编辑当前数据
                  changebox: 复选框选中的change事件,复选框的切换会调用该事件。 -->
            <category-item
              :list="c.children"
              :subordinate="true"
              :parent="c"
              :activeNameCopy="activeNames"
              @editfn="openForm"
              @changebox="clickBox"
            ></category-item>
          </div>

          <!-- 不需要再次递归的情况,在这里展示内容 -->
          <div class="checkBox" v-else>
            <van-checkbox v-model="c.checked" shape="square" @change="clickBox('', c.pid)"></van-checkbox>
            <div @click="openForm(c)" class="checktypeName">{{c.title}}</div>
          </div>
        </div>
      </van-collapse-item>
    </van-collapse>

    <!-- 子组件需要再次递归当前组件 -->
    <van-collapse v-else v-model="activeChildrenNames">

      <!-- 显示当前父级的内容,否则父级内容会直接跳过 -->
      <van-collapse-item  :name="parent.id" :border="false">
        <template slot="title">
          <div class="checkBox">
            <span class="iconfont active" v-if="parent.checked" @click.stop.prevent="clickBoxFn(parent, parent.id)">&#xe66a;</span>
            <span class="noactive" @click.stop.prevent="clickBoxFn(parent, parent.id)" v-else></span>
            <div @click.stop.prevent="openForm(parent)" class="checktypeName">{{parent.title}}</div>
          </div>
        </template>
        <!-- 遍历当前元素的子集元素 -->
        <div class="cheackBoxList" v-for="c of list" :key="c.id">
        <!-- 判断子集元素的子集集合长度是否大于0,如果大于0且count小于5则需要重新调用当前组件 -->
          <div v-if="c.children.length > 0 && c.count < 5">
            <category-item
              :list="c.children"
              :subordinate="true"
              :parent="c"
              :activeNameCopy="activeChildrenNames"
              @editfn="openForm"
              @changebox="clickBox"
            ></category-item>
          </div>
          <!-- 不需要再次递归的情况,在这里展示内容 -->
          <div class="checkBox" v-else>
            <van-checkbox v-model="c.checked" shape="square" @change="clickBox('', c.pid)"></van-checkbox>
            <div @click="openForm(c)" class="checktypeName">{{c.title}}</div>
            <van-icon
              v-if="c.children.length > 0 && c.count === 5"
              class="tip_arrow"
              name="arrow-down"
              size="0.32rem"
              @click="alertTip"
            />
          </div>
        </div>
      </van-collapse-item>
    </van-collapse>


	export default {
	  // 当前组件名称
	  name: 'category-item',
	
	  /**
	   * list: 当前组件接收的数据源
	   * parent: 父级对象数据
	   * subordinate: 是否子集调用
	   * activeNameCopy: 打开的折叠面板
	   */
	  props: ['list', 'parent', 'subordinate', 'activeNameCopy'],
	  
	  data () {
	    return {
	    }
	  },
	  methods: {
	  }
	}

说明:首次递归和后面的递归的不同

  • 首次递归没有父级元素,直接渲染当前元素的title ,判断子元素是否需要再次递归当前组件即可。但是递归组件,在递归遍历儿子组件的时候不仅仅要渲染孙子元素,也要将当前儿子元素的内容展示出来,然后将孙子元素递归的结果作为儿子元素的内容。
  • 设置了 subordinate和 parent,subordinate是用来判断是否为子元素递归组件,从而采用不同的渲染方式;parent用于接收父元素的对象,如果是子集递归需要用到。

递归方法

遍历数组,如果子集的子集集合数量大于0,则继续调用当前函数

    /**
     * 数据递归转换 : 接收参数list, 循环遍历,如果有子集的长度大于0则重复调用
     * @param {*} list 闭包轮询数据源
     * @param {*} callback 回调函数
     * @param {*} parentObj 父级的对象信息
     * @param {*} count 层级,默认从1开始
     */
    changeListData (list, callback, parentObj = {}, count = 1) {
      let arr = list.map(item => {
        let children = item.children
        let parent = { id: item.id, title: item.title, checked: item.checked }
        if (children.length > 0) {
          // 重复调用
          children = this.changeListData(children, callback, parent, count + 1)
        }
        if (callback) callback(item.id)
        return { ...item, children, checked: this.checkedAll, pid: parentObj.id, count }
      })
      return arr
    },

父组件和子组件的源码

父组件

<template>
  <div class="content">
    <!-- <div class="search">
      <van-cell-group>
        <van-field
          clearable
          @click-left-icon="search"
          left-icon="search"
          placeholder="搜索关键词"
        />
      </van-cell-group>
    </div> -->
    <div class="category_items">
      <van-pull-refresh v-model="isLoading" @refresh="onRefresh">
        <category-item
          :list="list"
          ref="categoryitem"
          @editfn="openForm"
          @changebox="clickBox"
        ></category-item>
      </van-pull-refresh>
    </div>
    <div class="addBox" >
      <div @click="openForm(null)" class="add">
        <van-icon name="plus" />
      </div>
    </div>
    <div class="actionBar">
      <div class="actionBarList">
        <van-checkbox
          v-model="checkedAll"
          @change="checkAllFn"
          shape="square">
          全选
        </van-checkbox>
      </div>
      <div class="actionBarList detele">
        <span v-if="delIds.length > 0" @click="showDelDialog = true">删除</span>
      </div>
      <div class="actionBarList" @click="showOptionFn" :class="{color1:active}">
        <div>更多</div>
        <div class="iconfont">{{active?'&#xe600;':'&#xe601;'}}</div>
      </div>
      <van-overlay :show="showOption" class-name="overlay_customer" @click="showOptionFn">
        <transition name="van-fade">
          <div class="options" v-show="active">
            <span class="option" @click="onSelect('isRelation', true)">父子关联</span>
            <span class="option" @click="onSelect('isRelation', false)">取消关联</span>
            <span class="option" @click="onSelect('openAll', true)">展开所有</span>
            <span class="option" @click="onSelect('openAll', false)">合并所有</span>
          </div>
        </transition>
      </van-overlay>
    </div>
    <commodity-category-form
      ref="form"
      :dialogTitle="itemName"
      :is-edit="isEdit"
      :activeNames="activeNames"
      @save="addData"
    />

    <van-dialog
      class="dialog_del"
      v-model="showDelDialog"
      show-cancel-button
      @cancel="showDelDialog = false"
      @confirm="delItems"
      >
      <div class="tip">
        确定删除所选中的{{delIds.length}}条数据吗?
      </div>
    </van-dialog>

  </div>
</template>

<script>
import * as dd from 'dingtalk-jsapi'
import { Toast, Dialog } from 'vant'
import { getMaterialCategoryTree, materialCategoryAdd, updateItem, deleteBatch } from '@/api/commodityCategory'
import CommodityCategoryForm from './commodityCategoryForm.vue'
import CategoryItem from './categoryItem.vue'
export default {
  components: { CommodityCategoryForm, CategoryItem },
  data () {
    return {
      checkedAll: false, // 全选
      openAll: true, // 展开所有
      isRelation: true, // 是否父子关联
      isEdit: false, // 是否为编辑
      delIds: [], // 删除的数据的ids
      selectBoxs: [], // 当前选中的数据

      active: false, // 是否打开更多
      isLoading: false, // 下拉加载loading
      itemName: '', // 编辑的类别名称
      activeNames: [], // 当前选中的打开的面板集合
      list: [], // 数据源

      showDelDialog: false, // 打开提示框

      showOption: false // 打开操作框
    }
  },
  mounted () {
    this.global.isShowTabr = false
    this.getData()
  },
  computed: {
    isDingDing () {
      return dd.env.platform === 'ios' || dd.env.platform === 'android'
    }
  },
  methods: {
    /** 获取树级数据 */
    getData () {
      getMaterialCategoryTree({id: ''}).then(res => {
        if (res && res.length > 0) {
          this.list = this.changeListData(res)
          this.openOrMergeAll()
        }
      })
    },

    showOptionFn () {
      this.active = !this.active
      this.showOption = !this.showOption
    },

    /** 全选和反选 */
    checkAllFn () {
      // const flag = this.checkedAll
      this.list = this.changeListData(this.list)
    },
    /** 展开/合并所有子集菜单 */
    openOrMergeAll () {
      const flag = this.openAll
      this.activeNames = []
      if (flag) {
        this.changeListData(this.list, (id) => {
          this.activeNames.push(id)
        })
      } else this.activeNames = []
      this.$refs['categoryitem'].activeNames = this.activeNames
      this.$refs['categoryitem'].activeChildrenNames = this.activeNames
      this.$refs['categoryitem'].$forceUpdate()
    },

    /** 点击复选框 */
    clickBox (id, pid) {
      if (this.isRelation) {
        // 处理复选框点击后选中的结果
        this.list = this.setListCheck(this.list, id, pid)

        // 如果存在即是子集又是父级的元素点击,那么这个方法就是处理选中后各个复选框的选择结果
        this.list = this.setAllParentCheck(this.list)
      }

      // 获取所有复选框中选中的id
      this.delIds = this.getAllCheck(this.list)
    },

    // 查询所有数据
    // 1. 判断当前是否父子关联
    // 2. 只改子集,传入父级的id,查询该父级下面的所有chilren,然后将其所有子集状态修改为和父级状态相同
    // 3. 只改父级,传入pid,根据pid查询当前父级的数据,然后判断其所有子元素是否全部选中,如果全部选中,则该父元素的状态选中,否则状态为false.
    /**
     * 修改数据
     * @param {*} list 数据源
     * @param {*} id 父级的id(父级传入的id)
     * @param {*} pid 父级id(子集的pid)
     */
    setListCheck (list, id, pid) {
      const arr = list.map(item => {
        let children = item.children
        let checked = item.checked
        // 按照父级的id查询修改子集数据
        if (id && item.id === id) {
          children = children.map(c => {
            // eslint-disable-next-line camelcase
            let c_children = c.children
            // eslint-disable-next-line camelcase
            if (c_children && c_children.length > 0) {
              // eslint-disable-next-line camelcase
              c_children = this.setAllItemCheck(c_children, checked)
            }
            return { ...c, children: c_children, checked }
          })
        } else if (id && item.children && item.children.length > 0) {
          // 轮询根据父级的id查询并修改子集数据
          children = this.setListCheck(item.children, id, pid)
        } else if (pid && item.id === pid) {
          // 根据子集传入的pid,找到该父级及其所有的子集,判断所有子集是否被选中
          const flag = children.every(c => c.checked === true)
          // 修改其父级的状态
          checked = flag
        } else if (pid && item.children && item.children.length > 0) {
          // 轮询根据子集的传入的pid查询并修改父级的状态
          children = this.setListCheck(item.children, id, pid)
        } else {
          console.log('边界情况没查到')
        }
        return { ...item, children, checked }
      })
      return arr
    },

    /**
     * 设置所有子集的状态一致,主要用于,一级菜单被选中,所有子集及其子集的子集也需要被选中
     * @param {*} list 数据源
     * @param {*} flag 状态值
     */
    setAllItemCheck (list, flag) {
      const arr = list.map(item => {
        let children = item.children
        if (children && children.length > 0) {
          children = this.setAllItemCheck(children, flag)
        }
        return { ...item, checked: flag, children }
      })
      return arr
    },

    // 4. 查询所有数据,从最后一级开始判断,如果所有子集的状态都为true,则父级的状态为true,否则为false
    setAllParentCheck (list) {
      const arr = list.map(item => {
        let children = item.children
        let checked = item.checked
        if (children && children.length > 0) {
          children = this.setAllParentCheck(children)
          checked = children.every(c => c.checked === true)
        }
        return { ...item, checked, children }
      })
      return arr
    },

    /**
     * 数据递归转换 : 接收参数list, 循环遍历,如果有子集的长度大于0则重复调用
     * @param {*} list 闭包轮询数据源
     * @param {*} callback 回调函数
     * @param {*} parentObj 父级的对象信息
     * @param {*} count 层级,默认从1开始
     */
    changeListData (list, callback, parentObj = {}, count = 1) {
      let arr = list.map(item => {
        let children = item.children
        let parent = { id: item.id, title: item.title, checked: item.checked }
        if (children.length > 0) {
          // 重复调用
          children = this.changeListData(children, callback, parent, count + 1)
        }
        if (callback) callback(item.id)
        return { ...item, children, checked: this.checkedAll, pid: parentObj.id, count }
      })
      return arr
    },

    /**
     * 获取所有被选中的id
     * @param {*} list 数据源
     */
    getAllCheck (list) {
      let arr = []
      let childrenArr = []
      list.map(item => {
        if (item.checked) arr.push(item.id)
        if (item.children && item.children.length > 0) {
          const newArr = this.getAllCheck(item.children)
          childrenArr = [...childrenArr, ...newArr]
        }
      })
      return [...arr, ...childrenArr]
    },

    /** 添加数据 */
    addData (form) {
      if (this.isEdit) {
        updateItem(form).then(res => {
          const { code } = res
          if (code === 200) {
            Toast.success('修改成功!')
            this.isClose = false
            this.getData()
            this.$refs['form'].form = {}
            this.$refs['form'].categoryName = ''
          }
        }).catch(e => {
          Toast.fail(JSON.stringify(e))
        })
      } else {
        materialCategoryAdd(form).then(res => {
          const { code } = res
          if (code === 200) {
            Toast.success('添加成功!')
            this.isClose = false
            this.getData()
            this.$refs['form'].form = {}
            this.$refs['form'].categoryName = ''
          }
        }).catch(e => {
          Toast.fail(JSON.stringify(e))
        })
      }
    },
    /** 批量删除 */
    delItems () {
      if (this.delIds.length === 0) return
      deleteBatch({ ids: this.delIds.join(',') + ',' }).then(res => {
        const { code, data } = res
        if (code === 200) {
          Toast.success('删除成功!')
          this.getData()
          this.delIds = []
        } else {
          Dialog.alert({
            title: '删除失败',
            message: data.message || data,
            confirmButtonColor: '#1890FF'
          }).then(() => {
            // on close
          })
        }
      })
    },
    /** 打开form表单 */
    openForm (item = {}) {
      if (item && item.id) {
        this.isEdit = true
        this.$refs['form'].getItem(item.id)
      } else this.isEdit = false
      this.$refs['form'].Editdialog = true
      // eslint-disable-next-line no-mixed-operators
      this.$refs['form'].getMaterialCategoryTree(item && item.id || '')
    },
    /**
     * 操作
     * @param {*} code 改变的变量
     * @param {*} flag 变量的布尔值
     */
    onSelect (code, flag) {
      this[code] = flag
      if (code === 'openAll') {
        this.openOrMergeAll()
      }
    },

    onRefresh () {
      setTimeout(() => {
        this.getData()
        this.isLoading = false
      }, 1000)
    }
  }
}
</script>

子组件

<template>
  <div>
    <!-- subordinate:判断是否为子集递归; activeNames:打开折叠面板的集合。 -->
    <van-collapse v-if="!subordinate" v-model="activeNames">
      <!-- 遍历数组 -->
      <van-collapse-item v-for="(item, index) in list" :key="index" :name="item.id" :border="false">
        <!-- 折叠面板的title数据 -->
        <template slot="title">
          <div class="checkBox">
            <span class="iconfont active" v-if="item.checked" @click.stop.prevent="clickBoxFn(item, item.id)">&#xe66a;</span>
            <span class="noactive" v-else @click.stop.prevent="clickBoxFn(item, item.id)"></span>
            <div @click.stop.prevent="openForm(item)" class="checktypeName first">{{item.title}}</div>
          </div>
        </template>
        <!-- 遍历当前元素的子集元素 -->
        <div class="cheackBoxList" v-for="c of item.children" :key="c.id">
          <!-- 判断子集元素的子集集合长度是否大于0,如果大于0则需要重新调用当前组件 -->
          <div v-if="c.children.length > 0">

            <!--  editfn: 编辑事件,点击title文字,会打开编辑框,编辑当前数据
                  changebox: 复选框选中的change事件,复选框的切换会调用该事件。 -->
            <category-item
              :list="c.children"
              :subordinate="true"
              :parent="c"
              :activeNameCopy="activeNames"
              @editfn="openForm"
              @changebox="clickBox"
            ></category-item>
          </div>

          <!-- 不需要再次递归的情况,在这里展示内容 -->
          <div class="checkBox" v-else>
            <van-checkbox v-model="c.checked" shape="square" @change="clickBox('', c.pid)"></van-checkbox>
            <div @click="openForm(c)" class="checktypeName">{{c.title}}</div>
          </div>
        </div>
      </van-collapse-item>
    </van-collapse>

    <!-- 子组件需要再次递归当前组件 -->
    <van-collapse v-else v-model="activeChildrenNames">

      <!-- 显示当前父级的内容,否则父级内容会直接跳过 -->
      <van-collapse-item  :name="parent.id" :border="false">
        <template slot="title">
          <div class="checkBox">
            <span class="iconfont active" v-if="parent.checked" @click.stop.prevent="clickBoxFn(parent, parent.id)">&#xe66a;</span>
            <span class="noactive" @click.stop.prevent="clickBoxFn(parent, parent.id)" v-else></span>
            <div @click.stop.prevent="openForm(parent)" class="checktypeName">{{parent.title}}</div>
          </div>
        </template>
        <!-- 遍历当前元素的子集元素 -->
        <div class="cheackBoxList" v-for="c of list" :key="c.id">
        <!-- 判断子集元素的子集集合长度是否大于0,如果大于0且count小于5则需要重新调用当前组件 -->
          <div v-if="c.children.length > 0 && c.count < 5">
            <category-item
              :list="c.children"
              :subordinate="true"
              :parent="c"
              :activeNameCopy="activeChildrenNames"
              @editfn="openForm"
              @changebox="clickBox"
            ></category-item>
          </div>
          <!-- 不需要再次递归的情况,在这里展示内容 -->
          <div class="checkBox" v-else>
            <van-checkbox v-model="c.checked" shape="square" @change="clickBox('', c.pid)"></van-checkbox>
            <div @click="openForm(c)" class="checktypeName">{{c.title}}</div>
            <van-icon
              v-if="c.children.length > 0 && c.count === 5"
              class="tip_arrow"
              name="arrow-down"
              size="0.32rem"
              @click="alertTip"
            />
          </div>
        </div>
      </van-collapse-item>
    </van-collapse>
  </div>
</template>

<script>
import { Toast } from 'vant'
export default {
  // 当前组件名称
  name: 'category-item',

  /**
   * list: 当前组件接收的数据源
   * parent: 父级对象数据
   * subordinate: 是否子集调用
   * activeNameCopy: 打开的折叠面板
   */
  props: ['list', 'parent', 'subordinate', 'activeNameCopy'],

  data () {
    return {
      activeNames: [],
      activeChildrenNames: this.activeNameCopy
    }
  },
  methods: {
    openForm (item) {
      this.$emit('editfn', item)
    },
    clickBoxFn (item, id, pid) {
      item.checked = !item.checked
      this.clickBox(id, pid)
    },
    /** 弹窗提示 */
    alertTip () {
      // this.$toast()
      Toast({
        message: '抱歉,手机端无法查看5级以上分类,\n请前往PC端查看全部分类!'
      })
    },
    /**
     * 点击复选框操作
     * @param {*} id 父级item的id
     * @param {*} pid 子集的pid
     */
    clickBox (id, pid) {
      if (id) {
        // this.activeNames.push(id)
        // this.activeChildrenNames.push(id)
      }
      this.$emit('changebox', id, pid)
    },
    changeChecked (value) {
      console.log(value, this.list)
      this.$emit('changebox')
    }
  }
}
</script>
Logo

前往低代码交流专区

更多推荐