栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > Web开发 > Vue.js

vue中实现自定义多选与单选的答题功能

Vue.js 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

vue中实现自定义多选与单选的答题功能

本来实现多选单选这个功能,vue组件中在表单方面提供了一个v-model指令,非常的善解“猿”意,能把我们的多选单选功能很完美且很强大得双向绑定起来,实现多选、单选、任意选...根本不在话下。(所以完美版之v-model实现自定义样式の多选与单选我会在下篇晒出。)


但是,凡事都有一个但是!但是奈何这个项目设计稿的缘故,当时愚蠢的我看了设计稿后觉得,使用原生的表单组件是不可能使用了,请看ui图:  

于是我就吭哧吭哧的傻滴呼呼的用原生js配合vue的属性来实现了类似双向绑定的感觉。虽然是一次把自己埋过头顶的填坑记,但是我的实现思路还是可以记录一下。万一日后在别的问题上可以提供点灵感呢也说不准!


这个需求的难点在于以下几点:

1.单选点击后选中状态,需满足如下:

  a) 每次点击只能选中其中一个

  b) 当选中时再次点击其他选项需要切换选择对应点击项

  c) 选中时点击自身无显示上的反应(同样的逻辑再做一遍也无妨,即再加一遍类名也看不出来)

2.多选样式展示,需满足如下:

  a) 同时可以选中多个

  b) 多选已选中状态再次点击取消选中

3.多选选中项的记录,需满足如下:

  a) 选择几个记录几个

  b) 选中再取消时需要将本条记录的数据通时消除(依据点击事件,事件点击触发判断哪个被选中了)

4.单选选中项的记录,方便提交数据

5.未点击选项不可提交,并给提示

6.可提交状态,需满足如下:

  a) 单选选中任意一个,即可提交。再次修改对提交没有影响

  b) 多选至少选中一个可提交,再次修改需判断是不是没选东西

7.第十四题点下一题切换提交按钮

8.快速点击下一题,多次提交

9.点击下一题提交数据后,拿响应结果调取弹层提示用户选择是否正确


=============接下来一 一解决====================

首先先说结构

看似十道题,其实是一道题不停的换数据,所以我的外部结构就是一个form加一个空的div

别问我为什么多余一个空的,我也很无措。

form.question(v-if="state.ExamInfo") 
    div

然后题目标题很傻瓜式得使用了h3

h3.qus-title(:data-id="state.ExamInfo.QuestionID") {{state.ExamInfo.ExamQuestionNo}} {{state.ExamInfo.Description}}

选项上,我使用ul>li的形式描述了多个选项

ul.qus-list
    li(v-for="(item,index) in state.ExamInfo.QuestionAnswerCode" @click="choosed(index)" v-bind:class="{'li-focus' : chooseNum==index}" ref="liId") {{item.Code}}、{{item.Description}}

大致几个属性

  • v-for是为了遍历题中的每一个选项,

  • click绑定了点击当前li时的事件,v-bind同步click绑定了动态的类名,用于展示选中状态。

  • 这里值得注意的一个点也是当时抓虾的一个点是,v-on:click和v-bind:class结合,

    • click的时候,每次把当前点击的li的index值传出去,

    • 然后定义一个变量chooseNum,点击函数中,将参数index赋给他

this.chooseNum = index;

靠这种间接拿到点击索引值的曲线救国方式,在v-bind的监视下,每次点击获得的索引chooseNum和这几个li中自己的index对上眼以后,就如正确的钥匙对上了合适的锁,类名绑定就成了。

也就是以上难题中的第一个难题的前半部分:单选点击后选中状态

费这么半天劲,才解决一个点啊!我不服!别急,接下来还有好戏。

但其实这个思路还是挺重要的,靠这一点“死皮赖脸”拉关系的劲头,这个法子以后还倒是可以有很多用武之地。

好戏在下一个属性,没错就是ref,这也是我步入万丈深渊一去不复返的梯子啊!


ref

  要知道人家可是vue里边的特殊特性,

  要知道人家可是很有能力的,

  要知道我老是连着打不出妖之道这三个字!

  好了不皮了。

官网记载:ref这个特殊特性,被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。

如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

我的理解大白话来说,他就是给dom元素或者组件实例一个身份证号,身份证号有的特性他也就有,那就是唯一不重复。

如果配合上v-for,就能获取这一批带有ref特性所组成的数组。

通过数组下标索引出来的个体,也就是对应的dom元素本身或者组件实例本人无疑了。

就好像拿着身份证号去公安局查人一样,快速不说,还很高效有没有,一查一个准!

但需要说明的是,在created钩子中,这个特性拿不到东西,

生命周期钩子里只有在mounted里可以用(可能还有后边的钩子里也可以使用,我没用过不准确),

毕竟你想啊,身份证号虽说一出生就有了,但是只有挂载到网上你才能查得到的嘛!

所以,我究竟用它做了什么呢?那就是多选功能啊!

 

还是先回到上边说的,绑定了一个事件,并且会传递一个当前点击li的索引号,

并且前边也提到过,ref返回的是数组,有数组有索引号,简直是万事俱备啊。于是乎让我们来呼唤东风(东风别看了,就是说你呢)!

在choose点击函数中就有了这么一段:

if(this.$refs.liId[index].className.length <= 0){    
    //首先先判断当前li有没有被选中,因为我这里li除了选中状态的有类名,其他没有类名,所以我就这么判断了。    
    //这么看有时候舍弃一小丢丢规范的东西反而省力。     
    this.$refs.liId[index].className = 'li-focus';// 添加类 
}else{    
    //当前li已经被选中,那么在多选的逻辑里,是允许人们选中后再取消选中的,所以前端展示层面上把样式去掉。     
    this.$refs.liId[index].className = '';// 选中再取消的情况 
}

好了,第二个多选样式搞定。

那么接下来,选择的结果呢,能不能来一次“趁火打劫”,趁点击的时候偷偷记录下用户的选择?答案当然是可以的啦!

首先说多选功能的趁火打劫吧,就着上边增删类名的热乎劲,紧接着在每次点击时我们记录下当前点击的是谁

// 获取选中结果
for (let i = 0; i < this.$refs.liId.length; i++) {
    if (this.$refs.liId[i].className.length > 0) {
        this.chooseNumStr += this.$refs.liId[i].innerText.substring(0, 1);
    }
}

这一段再次利用了ref的特性,找到当前点击的dom,截取人家选项里的第一个字,那就是ABC or D;

拼接到事先准备好的字符串chooseNumStr中(要发给数据用的),因为这里和后端提前约定的就是将选择结果以字符串的形式提交。

if判断那里,条件再次利用了li谁有类名就是选了谁的不讲理原则。第三个多选记录选项功能问题搞定。

 

第四个问题是,既然多选记录搞定了,那么单选呢,也应该在每次点击的时候搞定他吧?那是自然!

这里我刚刚突然又想到了一个解决方法,于是这里我将呈现俩个:

1.那就是我当时脑残的解决方法,不过这种方法唯一的好处可能是,

产品大大过来说,那sei,你把选项中的ABCD去掉吧,不好看,那我就傻逼了。

//索引0-3对应答案A-B,依次类推
// 注意,这里看看最多的选项是多少个,进行下配置,当前只是配置到了F
switch (index) {
case 0:
this.chooseNumStr = 'A';
break;
case 1:
this.chooseNumStr = 'B';
break;
case 2:
this.chooseNumStr = 'C';
break;
case 3:
this.chooseNumStr = 'D';
break;
case 4:
this.chooseNumStr = 'E';
break;
case 5:
this.chooseNumStr = 'F';
break;
}

没错,还是在choose方法中,我判断是单选后,用switch来判断index的值,进而匹配到chooseNumStr的结果。

虽然这种方法很笨拙,而且有超出设置范围的选项的危险,但是,我傻啊!那有什么方法!

当初就是觉得这么干很不妥,可是直到今天我再看自己的代码才想到更好的解决方案的啊!那他是啥啊?!

那就是:

 

2. 就还是强大的ref登场,规则和选择多选一样,只不过不用for循环。你是不是已经想到了啊哈!

对的,每次单选点的是哪个就li,截取当前li内部文本的第一个字符,也就是ABC or D啊

this.$refs.liId[i].innerText.substring(0,1);

好了,第四个问题单选的答案记录问题解决。简直soeasy,soshengshi!

 

然后,我们接着趁热打铁(才发现他和趁火打劫好像是兄弟啊!),解决下边点击按钮的问题。

需求是没选是灰色,选择选项后可提交:

首先是两个按钮的结构,为了避免后期下一题和提交按钮的交班时我还得判断点击事件是他俩按钮谁和谁的,

所以我用了两个按钮,绑了两个事件,把不同功能的事件分开绑定了。

.public-btn(v-if="!isLast" @click="nextItem" v-bind:class="{'public-btn-gray': unclickable}") 下一题 
.public-btn(v-else @click="submitItem" v-bind:class="{'public-btn-gray': unclickable}") 提交

可以看到,除了事件我还绑定了class,那个public-btn-gray的生存与否取决于unclickable。


先说没选是灰色的处理:

这个思路上就是,页面初始化时按钮默认肯定就是灰色的,也就是有着public-btn-gray类名的。

这里有一个用于描述按钮是不可点击状态的变量unclickable,专门管理按钮是否是可点击的。

初始化时是true不可点击的。这样,按钮的gray类名public-btn-gray就加了。

逻辑上,点击按钮的时候先判断这个值,如果为true就提示用户要先选择答案:

if(this.unclickable){
    alert('您还没有选择答案哦!');
}else{// do someting you wanted;}


然后是选择选项后可提交

那这不好说嘛!我只要点击事件一触发,就把可点击状态放开不就好了嘛!

那好,我是用户,我在如图第15题选择a、c解锁提交按钮,然后我再点击a、c抹掉我的记录。。。开不开心我的神操作?

但这时我的提交按钮已打开,我可以在他毫无防备的情况下趁虚而入(中华文化真博大,这是第三个同意义的成语了!哈哈哈)。

这当然不可以了,直接点击事件就放开下一题按钮,在单选场景下是通的。但是多选的时候我们还要再防御一层。

那就是:

// 置灰提交按钮与否
if(this.chooseNumStr.length > 0){
//多选的时候,因为再次点击会把记录抹除,所以chooseNumStr会是动态改变的,
//如果一个也没选择,多选也好单选也罢,这个字符串肯定是空的,故而判断长度小于0就不让他提交!
  this.unclickable = false;
}else{
  // 没有选东西,就置灰按钮
  this.unclickable = true;
}

耶!第六点多选功能与下一题按钮高亮可跳转功能的结合也完成啦 

 

至此,关于按钮的样式和逻辑就完毕了,每次点击下一题下一题的功能就跑通了。

但是,一直跑到第十四题点击下一题,15题内按钮文案还是下一题,可是这是最后一题了啊,讲点理吧!

好,那就讲理点,让他改成提交,这时下一题和提交按钮换岗。

换岗的时机我是在数据响应回来后判断本题目的题号/id,如果是14题,那么下一题就是最后一题,点击下一题就让提交按钮上岗,下一题退休。

说了这么多,说的最多的是点击下一题。所以在下一题按钮绑定的事件中,就有一个角落是来干这个事的:

// 下一题
 
if(_this.state.ExamInfo.QuestionID == 14){
    //点击下一题,数据响应回来后,新数据替换前,判断如果当前是第14题就改变按钮。
 
    //判断切换下一题和提交按钮
 
     _this.isLast = true;
 
}

然后,提交和下一题俩按钮的样式就靠这个状态值控制,只要在条件成立的时候改变状态值让他俩交岗即可。

(仔细总结会发现,都是这么一个套路,数据改变某个状态值,状态值绑定在结构上,被改变后影响视图的不同展示)

 

后来,还发现一个隐藏的问题:

点击下一题后,因为是单页应用,页面结构和数据都没有刷新,上一道题用户选择的结果绑在li上边的样式还需要清空,

所以每次点击下一题甚至提交后,都需要在重新填新题目数据时,把li的样式选中都清空,也就是把类名都清空。

// 样式清空
 
for (let i = 0; i < _this.$refs.liId.length; i++) {
 
     _this.$refs.liId[i].className = '';
 
}

也需要把上一题的用户的选择数据变量清空,也就是

chooseNumStr字符串=’’;

且如果用户翻到下边,离开第一屏时点击提交选项,此时再替换下一题数据,虽然用户看着像换了页面,但其实还在这一页。为了把假象做的更逼真点,需要页面定位到顶部:

// 点击下一题,新页面应该定位到顶头题干位置

document.body.scrollTop = 0;

正当我看着这个天衣无缝的假功能玩的开心的时候,测试大大跑过来说:

~我快速点击多次提交就提交了好多次。。

~exm??!你没事一直点提交干嘛?

~我是测试 :-)

~好,大大,你别说了,我这就改嘎。

~恩,辛苦啦辛苦啦 

~~~

第⑧个问题:多次点击下一题/提交按钮

好吧,这个问题确实是我没考虑到,以后做这种表单提交的,肯定要防御用户多次点击提交。

有了上面几次的经验,我现在很会利用data里某个变量来充当状态记录了!但是这样定义多个应该很不好吧。。。

定义一个变量isClicked专门用于看管按钮是否被提交过,如果在可点击的状态下点击过,那么抱歉,逻辑中断!

初始化这个isClicked肯定是没有点击状态,为false,然后在下一题/提交按钮的点击事件中判断:

if(!this.isClicked){//没点击过
 
    //该干啥干啥!
 
}else{
 
    //该干嘛干嘛去!
 
}

所以,到底应该干吗?!

 

终于说到最后拿响应结果,,这无非就是根据响应结果弹层而已,我不想说什么了。

完整html结构
//- 题目表单
    form.question(v-if="state.ExamInfo")
      div
        h3.qus-title(:data-id="state.ExamInfo.QuestionID") {{state.ExamInfo.ExamQuestionNo}}、{{state.ExamInfo.Description}}
        ul.qus-list
          li(v-for="(item,index) in state.ExamInfo.QuestionAnswerCode" @click="choosed(index)" v-bind:class="{'li-focus' : chooseNum==index}" ref="liId") {{item.Code}}、{{item.Description}}
    .public-btn(v-if="!isLast" @click="nextItem" v-bind:class="{'public-btn-gray': unclickable}") 下一题
    .public-btn(v-else @click="submitItem" v-bind:class="{'public-btn-gray': unclickable}") 提交
完整js代码
export default {
  name: 'question',
  data () {
    return {
      state: {
        dataUrl: this.$store.state.ownSet.dataUrl,
        progress: this.$store.state.init.ActiveProgressEnum,
        ExamInfo: this.$store.state.init.ExamInfo,
        PersonID: this.$store.state.init.PersonID,
        TeamID: this.$store.state.init.TeamID,
      },
      unclickable: true, // 判断是否已选择答案,不选择不能下一题,并置灰按钮
      showLayer: false, //是否显示弹层
      layerItem: {
        isQuestion: false,
        isSubmit: false, //是否是最后一道题时触发“下一题"按钮,点击了提交
        isSuccess: false,
        isLoading: false
      },
      chooseNum: null,
      isFocus: false,
      isLast: false,
      isClicked: false//是否已经点击下一题,防止二次提交
    }
  },
  created(){
    // 点击开始答题,新页面应该定位到顶头题干位置
    document.body.scrollTop = 0;
    if(this.state.progress > 100107 && this.state.progress !== 100112){
      alert('您已答题完毕!');
    }
    if(this.state.ExamInfo.QuestionID == 15){//答到14题退出的情况
      //判断切换下一题和提交按钮
      this.isLast = true;
    }
  },
  methods: {
    choosed(index){
      this.chooseNumStr = '';//初始化
      // 单选or多选
      if(this.state.ExamInfo.IsMulti){
        // 多选
        if(this.$refs.liId[index].className.length <= 0){
          // 添加类
          this.$refs.liId[index].className = 'li-focus';
        }else{
          // 选中再取消
          this.$refs.liId[index].className = '';
        }
        // 获取选中结果
        for (let i = 0; i < this.$refs.liId.length; i++) {
          if(this.$refs.liId[i].className.length > 0){
            this.chooseNumStr += this.$refs.liId[i].innerText.substring(0,1);
          }
        }
        // 置灰提交按钮与否
        if(this.chooseNumStr.length > 0){
          this.unclickable = false;
        }else{
          // 没有选东西,就置灰按钮
          this.unclickable = true;
          // 注意,再添加按钮的不可点击状态
        }
      }else{
        // 单选
        this.unclickable = false;
        this.chooseNum = index;
        //索引0-3对应答案A-B
        // 注意,这里看看最多的选项是多少个,进行下配置,当前只是配置到了F
        switch(index){
          case 0: this.chooseNumStr = 'A';
          break;
          case 1: this.chooseNumStr = 'B';
          break;
          case 2: this.chooseNumStr = 'C';
          break;
          case 3: this.chooseNumStr = 'D';
          break;
          case 4: this.chooseNumStr = 'E';
          break;
          case 5: this.chooseNumStr = 'F';
          break;
        }
      }
    },
    nextItem(){//下一题
      if(this.state.progress > 100107 && this.state.progress != 100112){
        alert('您已答题完毕!不能重复答题。');
      }else{
        if(!this.isClicked){
          // 按钮可以点击-如果提交过一次,不能二次提交,如果提交失败,可以二次提交
          if(this.unclickable){
            alert('您还没有选择答案哦!');
          }else{
            this.isClicked = true; // 还没提交过,可以提交
            let postData = `Type=2&PersonID=${this.state.PersonID}&QuestionID=${this.state.ExamInfo.QuestionID}&Result=${this.chooseNumStr}`;//2为下一题
            if(this.state.TeamID > 0){
              postData+= `&TeamID=${this.state.TeamID}`;
            }
            this.ajaxFun(postData,false)
            .then((response)=>{
              // console.log(this.state.ExamInfo.ExamQuestionNo)
            })
            .catch((err)=>{
              this.isClicked = false;
              console.log(err);
            });
          }
        }
      }
    },
    submitItem(){//提交按钮
      if(!this.isClicked){
        if(this.unclickable){
          alert('您还没有选择答案哦!');
        }else if(this.state.progress > 100107){
          alert('您已答题完毕!不能重复答题。');
        }else{
          this.showLayer = true;
          this.layerItem.isSubmit = true;
        }
      }
    },
    confirmSubmit(data){// 提交弹层 之 确定
      if(!this.isClicked){
        this.isClicked = true;
        // 发送ajax
        let postData = `Type=3&PersonID=${this.state.PersonID}&QuestionID=${this.state.ExamInfo.QuestionID}&Result=${this.chooseNumStr}`;//3为提交
        if(this.state.TeamID > 0){
          postData+= `&TeamID=${this.state.TeamID}`;
        }
        this.ajaxFun(postData,true)
        .then((response)=>{
          // 关闭提交弹层
        })
        .catch((err)=>{
          this.isClicked = false;
          console.log(err);
        });
      }
    },
    changeLayerShow(data){// 提交弹层 之 取消 + 状态重置
      this.showLayer = false;
      this.layerItem.isSubmit = false;
    },
    hideLayer(data){
      this.showLayer = false;
    },
    ajaxFun(postData,submitFun){
      let _this = this;
      return new Promise(function(resolve,reject){
        console.log(postData)
        if(submitFun){
          // 关闭提交弹层
          _this.layerItem.isSubmit = false;
        }
        _this.layerItem.isQuestion = false;
        _this.showLayer = true;
        _this.layerItem.isLoading = true;
        $axios.get(_this.state.dataUrl+'ExamAnswer?'+postData)
        .then((response)=>{
          console.log(response);
          if(response && response.data && response.data.result === 1){
            _this.layerItem.isLoading = false;
            _this.layerItem.isQuestion = true;
            // 判断返回结果
            if(response.data.RetValue.proResult){
              _this.layerItem.isSuccess = true;
            }else{
              _this.layerItem.isSuccess = false;
            }
            resolve(response);
            setTimeout(()=>{
              if(submitFun){
                // 提交
                // resolve(response);
                _this.$store.dispatch('setUser',response.data.RetValue);
                _this.$router.replace('redpacket');
              }else{
                // 下一题
                if(_this.state.ExamInfo.QuestionID == 14){ //ExamQuestionNo
                //判断切换下一题和提交按钮
                  _this.isLast = true;
                }
                // 下一题重新赋值
                _this.state.ExamInfo = response.data.RetValue;
                // 点击下一题,新页面应该定位到顶头题干位置
                document.body.scrollTop = 0;
                // 样式清空
                for (let i = 0; i < _this.$refs.liId.length; i++) {
                  _this.$refs.liId[i].className = '';
                }
              } 
              _this.showLayer = false;
              _this.layerItem.isQuestion = false;
              _this.chooseNumStr = '';
              _this.chooseNum = null;
              _this.unclickable = true;
              _this.isClicked = false;
            }, 2000);
          }else{
            _this.showLayer = false;
            _this.layerItem.isQuestion = false;
            _this.isClicked = false;
            reject('数据提交失败,请刷新重试!')
          }
        })
        .catch((err)=>{
          _this.showLayer = false;
          _this.layerItem.isQuestion = false;
          _this.isClicked = false;
          reject(err)
        });
      });
    }
  }
}

光是这一大堆js代码,就足以看出我挖出了可以再造两座尖峰山的土~

最后我只想说,注意安全。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/240573.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号