vue中如何使用递归组件,多层不同题目,如何使用递归重组树形数据结构

vue官方文档给出,递归组件介绍


image.png

但是如何使用,以及实际应用的案例网上很少出现,今天介绍一下工作中遇到的问题,以及使用方法

1,应用递归组件遍历树形题目


image.png

需求:有多种类型的题目,有多级题目。分别根据类型显示不同题目
2, 后端给的结构

{
    "data":{
        "cause_id":1,
        "id":3,
        "name":"相识结婚",
        "node_id":1,
        "points":[
            {
                "id":19,
                "level":1,
                "module_id":3,
                "must":false,
                "name":"二人相识方式",
                "reasons":[
                    {
                        "has_sub_reason":true,
                        "id":19,
                        "level":1,
                        "name":"经人介绍",
                        "point_id":19,
                        "status":1,
                        "type_op":1
                    },
                    {
                        "has_sub_reason":false,
                        "id":20,
                        "level":1,
                        "name":"自由恋爱",
                        "point_id":19,
                        "status":1,
                        "type_op":1
                    },
                    {
                        "has_sub_reason":false,
                        "id":21,
                        "level":1,
                        "name":"婚恋网站",
                        "point_id":19,
                        "status":1,
                        "type_op":1
                    }
                ],
                "status":1,
                "type":1,
                "type_op":0
            },
            {
                "id":20,
                "level":1,
                "module_id":3,
                "must":false,
                "name":"请问您是否存在受胁迫结婚的情形?",
                "reasons":[
                    {
                        "has_sub_reason":true,
                        "id":22,
                        "level":1,
                        "name":"是",
                        "point_id":20,
                        "status":1,
                        "type_op":1
                    },
                    {
                        "has_sub_reason":false,
                        "id":23,
                        "level":1,
                        "name":"否",
                        "point_id":20,
                        "status":1,
                        "type_op":1
                    }
                ],
                "status":1,
                "type":1,
                "type_op":0
            },
            {
                "id":21,
                "level":1,
                "module_id":3,
                "must":false,
                "name":"双方之间是否进行了结婚登记?",
                "reasons":[
                    {
                        "child_node":[
                            {
                                "id":1,
                                "level":2,
                                "must":false,
                                "name":"双方是再婚吗?",
                                "parent_reasons_id":24,
                                "reasons":[
                                    {
                                        "has_sub_reason":true,
                                        "id":1,
                                        "level":2,
                                        "name":"原告再婚",
                                        "point_id":1,
                                        "status":1,
                                        "type_op":1
                                    },
                                    {
                                        "has_sub_reason":true,
                                        "id":2,
                                        "level":2,
                                        "name":"被告再婚",
                                        "point_id":1,
                                        "status":1,
                                        "type_op":1
                                    },
                                    {
                                        "has_sub_reason":false,
                                        "id":3,
                                        "level":2,
                                        "name":"双方系再婚",
                                        "point_id":1,
                                        "status":1,
                                        "type_op":1
                                    }
                                ],
                                "status":1,
                                "type":1,
                                "type_op":0
                            },
                            {
                                "id":2,
                                "level":2,
                                "must":false,
                                "name":"结婚登记日期为?",
                                "parent_reasons_id":24,
                                "reasons":[
                                    {
                                        "description":"请选择结婚登记日期为?",
                                        "has_sub_reason":false,
                                        "id":4,
                                        "level":2,
                                        "name":"结婚登记日期为?",
                                        "point_id":2,
                                        "status":1,
                                        "type_op":1
                                    }
                                ],
                                "status":1,
                                "type":3,
                                "type_op":0
                            }
                        ],
                        "description":"",
                        "has_sub_reason":true,
                        "id":24,
                        "level":1,
                        "name":"是",
                        "point_id":21,
                        "status":1,
                        "type_op":1
                    },
                    {
                        "has_sub_reason":false,
                        "id":25,
                        "level":1,
                        "name":"否",
                        "point_id":21,
                        "status":1,
                        "type_op":1
                    }
                ],
                "status":1,
                "type":1,
                "type_op":0
            }
        ],
        "status":1
    },
    "message":"",
    "status":200,
    "success":true
}

3, 前端代码展示


image.png

首先解释一下,RadioComponent,CheckComponent等组件设置成全局组件,form_item.type===1 为题目类型,为1 时为单选按钮组件显示。其他如同。
这里组件用的时iview-design :prop="points.${form_index}.reasons"为设置是否必填,根据must判断的。
此页面所有代码为

<style lang="less" scoped>
   .writeComplaint {
       flex: 1;
       display: flex;
       padding-bottom: 30px;

       .writeComplaintContent {
           display: flex;
           flex: 1;

           .leftMenu {
               width: 200px;
               display: flex;
           }

           .writeComplaintCard {
               margin-left: 20px;
               flex: 1;

               .submit {
                   /deep/ .ivu-form-item-content {
                       display: flex;
                       justify-content: center;
                   }
               }
           }

           .reference {
               width: 300px;
           }

           .wordContent {
               flex: 1;
               margin-left: 20px;
           }
       }
   }
</style>
<template>
   <div class="writeComplaint">
       <div v-if="isCreated" class="writeComplaintContent">
           <Card class="reference"></Card>
           <Card class="wordContent">
               <div v-for="(word_item,word_index) in word" :key="word_index">
                   <div v-if="word_item.title" class="title">{{word_item.title}}:</div>
                   <div class="content">{{word_item.content}}</div>
               </div>
           </Card>
       </div>
       <div v-else class="writeComplaintContent">
           <div class="leftMenu">
               <LeftMenu ref="menus" v-model="activeIndex" :data="modulesData" label="name"></LeftMenu>
           </div>
           <Card dis-hover class="writeComplaintCard">
               <Form :model="formdata" ref="formValidate" label-position="top">

                   <FormItem
                           v-for="(form_item,form_index) in moduleForm.points"
                           :rules="rules(form_item)"
                             :label="form_item.name"
                             :prop="`points.${form_index}.reasons`"
                             :key="form_index">
                       <RadioComponent v-if="form_item.type===1" v-model="formdata.points[form_index].reasons"
                                       :options="form_item.reasons"></RadioComponent>
                       <CheckComponent v-if="form_item.type===2" v-model="formdata.points[form_index].reasons"
                                      :options="form_item.reasons"></CheckComponent>
                       <TimeComponent v-if="form_item.type===3" v-model="formdata.points[form_index].reasons"
                                        :options="form_item.reasons"></TimeComponent>
<!--                        <SelectComponent v-if="form_item.type === 4" v-model="formdata.points[form_index].reasons"-->
<!--                                         :options="form_item.reasons"></SelectComponent>-->
                       <CityComponent v-if="form_item.type === 5" v-model="formdata.points[form_index].reasons"
                                      :options="form_item.reasons"></CityComponent>
                       <InputComponent v-if="form_item.type === 6" v-model="formdata.points[form_index].reasons"
                                       :options="form_item.reasons"></InputComponent>
                       <FormGroComponent v-if="form_item.type === 7" v-model="formdata.points[form_index].reasons"
                                         :options="form_item.reasons"></FormGroComponent>
                       <TextAreaComponent v-if="form_item.type === 8" v-model="formdata.points[form_index].reasons"
                                          :options="form_item.reasons"></TextAreaComponent>
                   </FormItem>

                   <FormItem class="submit">
                       <Button type="primary" @click="prev" v-if="activeIndex">上一步</Button>
                       <Button type="primary" @click="save('formValidate')" style="margin: 0 30px">保存</Button>
                       <Button type="primary" @click="createComplaint('formValidate')"
                               v-if="activeIndex===modulesData.length-1">生成诉状
                       </Button>
                       <Button type="primary" @click="next('formValidate')" v-else>下一步</Button>
                   </FormItem>
               </Form>
           </Card>
       </div>
   </div>
</template>

<script type="text/javascript">
import LeftMenu from '@/components/LeftMenu.vue';
import { created, moduleDetail, modules, save } from '@/api/lawsuit.js';

export default {
 data () {
   var _this = this;
   return {
     formdata: {
       points: []
     },
     moduleForm: {},
     activeIndex: 0,
     modulesData: [],
     isSave: false,
     wordOrder: [],
     isCreated: false,
     word: [],
     points: []
   };
 },
 components: {
   LeftMenu
 },
 watch: {
   activeIndex (newvalue, oldvalue) {
     this.getModuleDetail(this.modulesData[ newvalue ])
   }
 },
 created () {
   this.getModules();
 },
 methods: {
   async getModules () {
     const { data } = await modules({
       node_id: this.$route.query.step,
       cause_id: this.$route.query.cause_id
     })
     this.modulesData = data.data;
     const [ first ] = this.modulesData;
     this.getModuleDetail(first);
   },
   async getModuleDetail (module_data) {
     const { data } = await moduleDetail({
       module_id: module_data.id
     })
     this.moduleForm = data.data;
     this.formdata.points = [];
     this.moduleForm.points.map((module_item) => {
       this.formdata.points.push({
         point_id: module_item.id,
         reasons: []
       })
       // this.points =
     })
   },
   save () {
     this.savaLoad().then((data) => {
       // this.isSave = true
     })
   },
   async createComplaint (name) {
     const invalidate = await this.validate(name)
     if (!invalidate) return
     const data = await this.savaLoad(name);
     const createData = await created({
       document_id: this.$route.query.document_id
     });
     this.isCreated = true
     const wordData = createData.data.data;
     this.wordOrder.map((item) => {
       this.word.push({
         title: wordData[ item.title ],
         content: wordData[ item.content ]
       })
     })
   },
   async next (name) {
     if (this.activeIndex < this.modulesData.length - 1) {
       if (this.isSave) {
         const invalidate = await this.validate(name)
         if (!invalidate) return
         this.activeIndex += 1
       } else {
         this.savaLoad(name).then((data) => {
           if (data) {
             this.activeIndex += 1
           }
         })
       }
     }
   },
   prev () {
     this.activeIndex -= 1
   },
   async savaLoad () {
     const invalidate = await this.validate('formValidate')
     if (!invalidate) return
     const params = Object.assign({}, {
       document_id: this.$route.query.document_id,
       module_id: this.moduleForm.id,
       points: this.formdata.points
     })
     const { data } = await save(params)
     if (data.status === 200) {
       this.$Message.success('保存成功');
       return true
     }
   },
   validate (name) {
     return new Promise((resolve) => {
       this.$refs[ name ].validate((valid) => {
         if (valid) {
           resolve(true)
         } else {
           resolve(false)
         }
       })
     })
   },
   rules (form_item) {
     return [ {
       required: form_item.must,
       message: '不能为空'
     } ]
   }
 }
};
</script>

4,只写其中一个单选的例子,其他类似


image.png
image.png
image.png

radio组件所有代码如下

<template>
   <div class="mock-wrapper flex-sb-top" :style="[{display: 'block'}]">
           <div v-for="(form_item, index) in options"
                :key="index"
           >
               <div>
                   <div v-if="form_item.type_op === 0" class="name-box" @click="getDownOrUp($event,index)">
                       <Icon type="ios-arrow-down" />
                       <span class="question-name">{{form_item.question}},{{form_item.name}}</span>
                   </div>
                   <div v-if="form_item.type_op === 1" class="content-box">
                       <Radio v-model="form_item.checked" :label="form_item.name" @on-change="handleChange($event,form_item)"></Radio>
                   </div>
               </div>
              <RadioComponent v-if="form_item.type === 1" :options="form_item.reasons"></RadioComponent>
              <CheckComponent v-if="form_item.type === 2" :options="form_item.reasons"></CheckComponent>
              <TimeComponent v-if="form_item.type === 3" :options="form_item.reasons"></TimeComponent>
<!--               <SelectComponent v-if="form_item.type === 4" :options="form_item"></SelectComponent>-->
              <CityComponent v-if="form_item.type === 5" :options="form_item.reasons"></CityComponent>
              <InputComponent v-if="form_item.type === 6" :options="form_item.reasons"></InputComponent>
              <FormGroComponent v-if="form_item.type === 7" :options="form_item.reasons"></FormGroComponent>
              <TextAreaComponent v-if="form_item.type === 8" :options="form_item.reasons"></TextAreaComponent>
               <div v-if="form_item.checked && form_item.child_node" class="sub-item-box">
                   <RadioComponent :options="form_item.child_node"></RadioComponent>
               </div>
           </div>
   </div>
</template>
<script>
export default {
 name: 'RadioComponent',
 props: {
   options: {
     type: Array,
     default () {
       return []
     }
   }
 },
 methods: {
   handleChange (checked, option) {
     this.options = this.options.map(val => {
       val.checked = val.id === option.id;
       val.reason_id = val.id
       return val
     });
     this.$emit('input', this.options)
   },
   getDownOrUp (index) {
     let dom = null;
     if (index.toElement.className === 'name-box') {
       dom = index.path[ 1 ].parentNode.children[ 1 ]
     } else {
       dom = index.path[ 1 ].parentNode.parentNode.children[ 1 ]
     }
     const self = index.path[ 0 ].parentNode;
     if (dom.style.display === 'block') {
       dom.style.display = 'none';
       self.getElementsByTagName('i')[ 0 ].classList.replace('ivu-icon-ios-arrow-down', 'ivu-icon-ios-arrow-forward')
     } else {
       dom.style.display = 'block';
       self.getElementsByTagName('i')[ 0 ].classList.replace('ivu-icon-ios-arrow-forward', 'ivu-icon-ios-arrow-down')
     }
   }
 }
}
</script>
<style lang="less">
   @import "index";
</style>

其他组件类似

5,这样就会出现效果,其中有一个三级城市组件给的数据不符合要求,组件需要label 和vaule 现在给的是name, id所以需要转一下,用到递归函数

 handleCity: function (tree) {
      for (let i = 0; i < tree.length; i++) {
        tree[ i ].label = tree[ i ].name;
        tree[ i ].value = tree[ i ].id;
        tree[ i ].reason_id = tree[ i ].id;
        if (tree[ i ].children && tree[ i ].children.length > 0) {
          this.handleCity(tree[ i ].children)
        }
      }
      return tree
    },

6,全篇有个知识点这里有个 子组件用 this.$emit('input', selectedData),
组件调用 使用 <RadioComponent v-if="form_item.type===1" v-model="formdata.points[form_index].reasons"
:options="form_item.reasons"></RadioComponent>
组件调用使用v-model 接受改变过的值
vue中子组件不能直接改变通过props 传过来的值的会报错


image.png

这里没有找到好方法,只能屏蔽警告提示,不影响效果

Vue.config.warnHandler = function (msg) {
  if (!msg.includes('Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.')) { // uniApp bug: https://ask.dcloud.net.cn/question/71966
    return console.warn && console.warn(msg)
  }
}

至此vue递归组件遍历不同类型不同级别的题目问题写好了。
如果喜欢欢迎点赞留言,关注本人维护的公众号---- 程序员蜗牛,有免费学习资料赠送//