天天看點

vue - 使用vue實作自定義多選與單選的答題功能

4月底立得flag,五月底插上小旗,結果拖到六月底七月初才來執行。說什麼工作忙都是借口,就是睡的比豬早,起的比豬晚。

本來實作多選單選這個功能,vue元件中在表單方面提供了一個v-model指令,非常的善解“猿”意,

能把我們的多選單選功能很完美且很強大得雙向綁定起來,實作多選、單選、任意選...根本不在話下。

但是,凡事都有一個但是!

但是奈何這個項目設計稿的緣故,使用原生的表單元件是不可能使用了,請看ui圖:  

vue - 使用vue實作自定義多選與單選的答題功能

可悲的是,這個項目兩個月後,我才來做項目複盤,

話說也就在此時,我才發現有一種更簡單的方式來實作并且應用上v-model,

為什麼要為了樣式放棄功能然後自己吭哧吭哧傻滴呼呼的用js來實作了類似雙向綁定的感覺!!!

flag:今天先專注把我費勁巴拉手動搬得磚總結一下,明天(07-05)我再把所謂的最簡單的方法做出來貼這裡~

這個需求的難點在于以下幾點:

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對上眼以後,就如正确的鑰匙對上了合适的鎖,類名綁定就成了。

也就是以上難題中的第一個難題的前半部分:單選點選後選中狀态。

vue - 使用vue實作自定義多選與單選的答題功能
vue - 使用vue實作自定義多選與單選的答題功能
vue - 使用vue實作自定義多選與單選的答題功能

費這麼半天勁,才解決一個點啊!我不服!别急,接下來還有好戲。

但其實這個思路還是挺重要的,靠這一點“死皮賴臉”拉關系的勁頭,這個法子以後還倒是可以有很多用武之地。

好戲在下一個屬性,沒錯就是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 = '';// 選中再取消的情況

}           

複制

好了,第二個多選樣式搞定。

vue - 使用vue實作自定義多選與單選的答題功能
vue - 使用vue實作自定義多選與單選的答題功能
vue - 使用vue實作自定義多選與單選的答題功能

那麼接下來,選擇的結果呢,能不能來一次“趁火打劫”,趁點選的時候偷偷記錄下使用者的選擇?答案當然是可以的啦!

首先說多選功能的趁火打劫吧,就着上邊增删類名的熱乎勁,緊接着在每次點選時我們記錄下目前點選的是誰

// 擷取選中結果

        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去掉吧,不好看,那我就傻逼了。

事實上,本來人家設計稿裡選項處就沒有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;}           

複制

vue - 使用vue實作自定義多選與單選的答題功能

然後是選擇選項後可送出。

那這不好說嘛!我隻要點選事件一觸發,就把可點選狀态放開不就好了嘛!

那好,我是使用者,我在如圖第15題選擇a、c解鎖送出按鈕,然後我再點選a、c抹掉我的記錄。。。開不開心我的神操作?

但這時我的送出按鈕已打開,我可以在他毫無防備的情況下趁虛而入(中華文化真博大,這是第三個同意義的成語了!哈哈哈)。

這當然不可以了,直接點選事件就放開下一題按鈕,在單選場景下是通的。但是多選的時候我們還要再防禦一層。

那就是:

// 置灰送出按鈕與否

        if(this.chooseNumStr.length > 0){
        //多選的時候,因為再次點選會把記錄抹除,是以chooseNumStr會是動态改變的,
        //如果一個也沒選擇,多選也好單選也罷,這個字元串肯定是空的,故而判斷長度小于0就不讓他送出!

          this.unclickable = false;

        }else{

          // 沒有選東西,就置灰按鈕

          this.unclickable = true;

        }               

複制

vue - 使用vue實作自定義多選與單選的答題功能

耶!第六點多選功能與下一題按鈕高亮可跳轉功能的結合也完成啦 

至此,關于按鈕的樣式和邏輯就完畢了,每次點選下一題下一題的功能就跑通了。

但是,一直跑到第十四題點選下一題,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{

    //該幹嘛幹嘛去!

}           

複制

是以,到底應該幹嗎?!

終于說到最後,我好困,如果不是自娛自樂我可能坐着睜眼就睡着了,不,我已經進入夢鄉了......

說到拿響應結果,,這無非就是根據響應結果彈層而已,我不想說什麼了。

睡了。晚安世界~

vue - 使用vue實作自定義多選與單選的答題功能
1 //- 題目表單
2     form.question(v-if="state.ExamInfo")
3       div
4         h3.qus-title(:data-id="state.ExamInfo.QuestionID") {{state.ExamInfo.ExamQuestionNo}}、{{state.ExamInfo.Description}}
5         ul.qus-list
6           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}}
7     .public-btn(v-if="!isLast" @click="nextItem" v-bind:class="{'public-btn-gray': unclickable}") 下一題
8     .public-btn(v-else @click="submitItem" v-bind:class="{'public-btn-gray': unclickable}") 送出           

複制

1 export default {
  2   name: 'question',
  3   data () {
  4     return {
  5       state: {
  6         dataUrl: this.$store.state.ownSet.dataUrl,
  7         progress: this.$store.state.init.ActiveProgressEnum,
  8         ExamInfo: this.$store.state.init.ExamInfo,
  9         PersonID: this.$store.state.init.PersonID,
 10         TeamID: this.$store.state.init.TeamID,
 11       },
 12       unclickable: true, // 判斷是否已選擇答案,不選擇不能下一題,并置灰按鈕
 13       showLayer: false, //是否顯示彈層
 14       layerItem: {
 15         isQuestion: false,
 16         isSubmit: false, //是否是最後一道題時觸發“下一題"按鈕,點選了送出
 17         isSuccess: false,
 18         isLoading: false
 19       },
 20       chooseNum: null,
 21       isFocus: false,
 22       isLast: false,
 23       isClicked: false//是否已經點選下一題,防止二次送出
 24     }
 25   },
 26   created(){
 27     // 點選開始答題,新頁面應該定位到頂頭題幹位置
 28     document.body.scrollTop = 0;
 29     if(this.state.progress > 100107 && this.state.progress !== 100112){
 30       alert('您已答題完畢!');
 31     }
 32     if(this.state.ExamInfo.QuestionID == 15){//答到14題退出的情況
 33       //判斷切換下一題和送出按鈕
 34       this.isLast = true;
 35     }
 36   },
 37   methods: {
 38     choosed(index){
 39       this.chooseNumStr = '';//初始化
 40       // 單選or多選
 41       if(this.state.ExamInfo.IsMulti){
 42         // 多選
 43         if(this.$refs.liId[index].className.length <= 0){
 44           // 添加類
 45           this.$refs.liId[index].className = 'li-focus';
 46         }else{
 47           // 選中再取消
 48           this.$refs.liId[index].className = '';
 49         }
 50         // 擷取選中結果
 51         for (let i = 0; i < this.$refs.liId.length; i++) {
 52           if(this.$refs.liId[i].className.length > 0){
 53             this.chooseNumStr += this.$refs.liId[i].innerText.substring(0,1);
 54           }
 55         }
 56         // 置灰送出按鈕與否
 57         if(this.chooseNumStr.length > 0){
 58           this.unclickable = false;
 59         }else{
 60           // 沒有選東西,就置灰按鈕
 61           this.unclickable = true;
 62           // 注意,再添加按鈕的不可點選狀态
 63         }
 64       }else{
 65         // 單選
 66         this.unclickable = false;
 67         this.chooseNum = index;
 68         //索引0-3對應答案A-B
 69         // 注意,這裡看看最多的選項是多少個,進行下配置,目前隻是配置到了F
 70         switch(index){
 71           case 0: this.chooseNumStr = 'A';
 72           break;
 73           case 1: this.chooseNumStr = 'B';
 74           break;
 75           case 2: this.chooseNumStr = 'C';
 76           break;
 77           case 3: this.chooseNumStr = 'D';
 78           break;
 79           case 4: this.chooseNumStr = 'E';
 80           break;
 81           case 5: this.chooseNumStr = 'F';
 82           break;
 83         }
 84       }
 85     },
 86     nextItem(){//下一題
 87       if(this.state.progress > 100107 && this.state.progress != 100112){
 88         alert('您已答題完畢!不能重複答題。');
 89       }else{
 90         if(!this.isClicked){
 91           // 按鈕可以點選-如果送出過一次,不能二次送出,如果送出失敗,可以二次送出
 92           if(this.unclickable){
 93             alert('您還沒有選擇答案哦!');
 94           }else{
 95             this.isClicked = true; // 還沒送出過,可以送出
 96             let postData = `Type=2&PersonID=${this.state.PersonID}&QuestionID=${this.state.ExamInfo.QuestionID}&Result=${this.chooseNumStr}`;//2為下一題
 97             if(this.state.TeamID > 0){
 98               postData+= `&TeamID=${this.state.TeamID}`;
 99             }
100             this.ajaxFun(postData,false)
101             .then((response)=>{
102               // console.log(this.state.ExamInfo.ExamQuestionNo)
103             })
104             .catch((err)=>{
105               this.isClicked = false;
106               console.log(err);
107             });
108           }
109         }
110       }
111     },
112     submitItem(){//送出按鈕
113       if(!this.isClicked){
114         if(this.unclickable){
115           alert('您還沒有選擇答案哦!');
116         }else if(this.state.progress > 100107){
117           alert('您已答題完畢!不能重複答題。');
118         }else{
119           this.showLayer = true;
120           this.layerItem.isSubmit = true;
121         }
122       }
123     },
124     confirmSubmit(data){// 送出彈層 之 确定
125       if(!this.isClicked){
126         this.isClicked = true;
127         // 發送ajax
128         let postData = `Type=3&PersonID=${this.state.PersonID}&QuestionID=${this.state.ExamInfo.QuestionID}&Result=${this.chooseNumStr}`;//3為送出
129         if(this.state.TeamID > 0){
130           postData+= `&TeamID=${this.state.TeamID}`;
131         }
132         this.ajaxFun(postData,true)
133         .then((response)=>{
134           // 關閉送出彈層
135         })
136         .catch((err)=>{
137           this.isClicked = false;
138           console.log(err);
139         });
140       }
141     },
142     changeLayerShow(data){// 送出彈層 之 取消 + 狀态重置
143       this.showLayer = false;
144       this.layerItem.isSubmit = false;
145     },
146     hideLayer(data){
147       this.showLayer = false;
148     },
149     ajaxFun(postData,submitFun){
150       let _this = this;
151       return new Promise(function(resolve,reject){
152         console.log(postData)
153         if(submitFun){
154           // 關閉送出彈層
155           _this.layerItem.isSubmit = false;
156         }
157         _this.layerItem.isQuestion = false;
158         _this.showLayer = true;
159         _this.layerItem.isLoading = true;
160         $axios.get(_this.state.dataUrl+'ExamAnswer?'+postData)
161         .then((response)=>{
162           console.log(response);
163           if(response && response.data && response.data.result === 1){
164             _this.layerItem.isLoading = false;
165             _this.layerItem.isQuestion = true;
166             // 判斷傳回結果
167             if(response.data.RetValue.proResult){
168               _this.layerItem.isSuccess = true;
169             }else{
170               _this.layerItem.isSuccess = false;
171             }
172             resolve(response);
173             setTimeout(()=>{
174               if(submitFun){
175                 // 送出
176                 // resolve(response);
177                 _this.$store.dispatch('setUser',response.data.RetValue);
178                 _this.$router.replace('redpacket');
179               }else{
180                 // 下一題
181                 if(_this.state.ExamInfo.QuestionID == 14){ //ExamQuestionNo
182                 //判斷切換下一題和送出按鈕
183                   _this.isLast = true;
184                 }
185                 // 下一題重新指派
186                 _this.state.ExamInfo = response.data.RetValue;
187                 // 點選下一題,新頁面應該定位到頂頭題幹位置
188                 document.body.scrollTop = 0;
189                 // 樣式清空
190                 for (let i = 0; i < _this.$refs.liId.length; i++) {
191                   _this.$refs.liId[i].className = '';
192                 }
193               } 
194               _this.showLayer = false;
195               _this.layerItem.isQuestion = false;
196               _this.chooseNumStr = '';
197               _this.chooseNum = null;
198               _this.unclickable = true;
199               _this.isClicked = false;
200             }, 2000);
201           }else{
202             _this.showLayer = false;
203             _this.layerItem.isQuestion = false;
204             _this.isClicked = false;
205             reject('資料送出失敗,請重新整理重試!')
206           }
207         })
208         .catch((err)=>{
209           _this.showLayer = false;
210           _this.layerItem.isQuestion = false;
211           _this.isClicked = false;
212           reject(err)
213         });
214       });
215     }
216   }
217 }           

複制

寫完這個發現,我好像總是喜歡繞着彎的踩坑。

雖然問題最終都解決了,但是很能說明其實我基礎還是薄弱的,不會很好利用每一個代碼的特性,簡而言之就是不進階!

2018-07-04  23:18:23 - 2018-07-05  00:00:44好累~

個人學習了解和總結,很多不足還請指正~

下一篇: Scikit-learn