iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0
Modern Web

LV的全端開發體驗系列 第 28

Day28 增加不同的答題模式-來認真玩一下Vue

  • 分享至 

  • xImage
  •  

這趟全端體驗幾乎有一大半時間都在處理後端的東西,前端似乎只有顯示的功能,有點對不起vue,今天來提升一下vue的存在感一下。

先前我們在做測驗列表時,都是一頁全部顯示出來的方式,這在後面40題以上時會有點又臭又長,網頁用這種傳統紙本的方式來呈現實在不太理想,所以我們打算提供使用者另一種模式的選擇,一頁一題的方式,也是在模擬國家技術士學科的測驗模式:

先在首頁增加一個選擇答題模式的選項:

<script setup>
.....略

//增加一個模式的選擇
const testSelect = reactive({ quizbank: props.banks[0].id, 
                              type: "test" ,
                              amount:20,
                              mode:'page'});
.....略
</script>
<template>
.....略

<div class="m-4">
    <div>選擇答題模式:</div>
    <input type="radio" name="mode" value="page" v-model="testSelect.mode" />
        傳統-全顯示
    <input type="radio" name="mode" value="singal" v-model="testSelect.mode" />
        一頁一題
</div>
.....略
</template>

我們先建立一個獨立的頁面檔案來處理這個功能,等做完後再來觀察有沒有可能兩個模式放在同一個頁面中做切換,或是利用組件化的方式來處理:
複製 resources\js\Pages\StartTest.vue 成為 resources\js\Pages\StartTestSingal.vue

<script setup>
.....略
</script>
<template>
<FrontLayout>
  <h3 class="p-4 border w-full bg-lime-100 shadow text-xl text-center">模擬測驗</h3>
  <div>測驗及練習</div>
  <div>題庫:{{ bank.name }}{{bank.levelC}}級</div>
  <div>類型:{{ type }}</div>
  <div>
    <div v-for="subject , idx in subjects" :key="subject.id">
      {{ idx+1 }}.
      <SingalSubject :subject="subject" :form="form[idx+1]" />
    </div>
        <QuizButton @click="submit" color="teal-dark" size="xl">
            送出
        </QuizButton>
  </div>
</FrontLayout>
</template>

先在測驗的Controler中做個簡單的判斷,原則上,頁面的顯示方式應該讓前端來處理,但是我們現在用Controller 來切換不同的頁面檔案只是權宜之計,如果類似的功能都是Controller在做,那Controller會太累:
app\Http\Controllers\TestController.php

function startTest(Request $request,$bank_id,$type){
    //把要回傳的資料先放在獨立的陣列中
    $page=[
        'canLogin' => Route::has('login'),
        'canRegister' => Route::has('register'),
        'username'=>Auth::user()->name??'訪客',
        'role'=>Auth::user()->role??'guest',
        'bank'=>$this->bank->find($bank_id),
        'subjects'=>$this->bank->subjects($bank_id,$request->amount),
        'type'=>$type
    ];

    //利用mode值來回傳不同的頁面
    switch($request->mode){
        case 'page':
            return Inertia::render('StartTest',$page );
        break;
        case 'singal':
            return Inertia::render('StartTestSingal',$page );
        break;
    }
}

一頁一題的顯示方式當然不能像傳統的做法真的做出20頁40頁的檔案在那傳來傳去的,最基本是利用網址的 query string 來傳參數,但這次因為整個題目組都在前端了,還要一次一次重新 reload 有點多餘,所以我們採用的方式是由 一頁一題 改成 一次顯示一題 ,因為描述的方式不同,所以會造成做法的不同,但最後在視覺上都是一樣的。

一次顯示一題的基本做法就是在頁面上先畫出全部的題目,這和原生的做法是一樣的,然後我們把全部的dom都存入到一個 ref 陣列,接下來就是操作這個陣列。

所以我們建立一個go()函式,根據帶入的參數來決定要顯示那一題,因為要提供使用者可以在題目間自由來去,又希望未來容易維護擴充,所以我們要去判斷參數的資料型態是文字還是數字,再讓語意化的文字參數轉為題數。

最後就是操作陣列中的dom來決定誰要顯示,誰要隱藏,我們使用原生語法去檢查每一個題目中的Class列表是否有 block 這個class 名,有的話就移除並改為 hidden;最後把指定要顯示的dom加上 block 這個class就完成了

<script setup>
import FrontLayout from '@/Layouts/FrontLayout.vue';
import { useForm } from "@inertiajs/inertia-vue3";
import SingalSubject from '@/QuizComponents/SingalSubject.vue';
import QuizButton from '@/QuizComponents/QuizButton.vue';
import Swal from "sweetalert2";
import { onMounted, ref } from 'vue';
const props = defineProps({
  bank: Object,
  subjects:Array,
  type: String,
});

const data={}
props.subjects.forEach(function(subject,idx){
  data[idx+1]={seq:0,subjectId:subject.id,optionId:0}
})
data['bankId']=props.bank.id;
data['type']=props.type;

const form=useForm(data);

const submit=()=>{
  form.post(route('test.result'))
}

//宣告一個ref now 來紀錄目前顯示的題目
const now=ref(0)

const go=(position)=>{
  if(Number.isInteger(position) && 
     parseInt(position)<props.subjects.length){
    now.value=position;
  }else{
    switch(position){
      case 'prev':
        now.value--;
      break;
      case 'next':
        now.value++;
      break;
      case 'first':
        now.value=0;
      break;
      case 'last':
        now.value=props.subjects.lenght-1;
      break;
      default:
        //如果有錯誤的題目參數則跳出警告
        Swal.fire({
          title:"沒有指定的題目",
          icon:"error",
          confirmButtonColor: '#3085d6',
          confirmButtonText: '確定'
        })
    }
  }
    //在陣列中找出目前為block的dom
  subs.value.forEach((dom)=>{
    if(dom.classList.contains('block')){
      dom.classList.remove('block')
      dom.classList.add('hidden')
    }
  })
  subs.value[now.value].classList.remove('hidden')
  subs.value[now.value].classList.add('block')
}

//頁面繪製完成時,先指定第一題顯示出來
onMounted(()=>{
  subs.value[0].classList.remove('hidden')
  subs.value[0].classList.add('block')
})
const subs=ref(new Array())
</script>
<template>
<FrontLayout>
  <h3 class="p-4 border w-full bg-lime-100 shadow text-xl text-center">模擬測驗</h3>
  <div>測驗及練習</div>
  <div>題庫:{{ bank.name }}{{bank.levelC}}級</div>
  <div>類型:{{ type }}</div>
  <QuizButton color="green" @click="go('prev')">上一題</QuizButton>
  <QuizButton color="green" @click="go('next')">下一題</QuizButton>
  <div>
    <div v-for="subject , idx in subjects" 
         :key="subject.id"
         class="hidden"
         ref="subs">
      {{ idx+1 }}.
      <SingalSubject :subject="subject" :form="form[idx+1]" />
    </div>
        <QuizButton @click="submit" color="teal-dark" size="xl">
            送出
        </QuizButton>
  </div>
</FrontLayout>
</template>

我在go的函式中多寫了跳到第一題及最後一題,即然知道這個函式是有用的,那接下來就是把其它的跳題方法寫好並加上按鈕就可以了,這邊要注意一下邊界問題,上一題或下一題的範圍要限制在題目數量之內。

<script setup>
.....略

const jump=ref(0)

const submit=()=>{
  form.post(route('test.result'))
}

//建立一個變數記錄目前顯示的題目
const now=ref(0)

const go=(position)=>{
  if(Number.isInteger(position) && 
     parseInt(position)>=0 && 
     parseInt(position)<props.subjects.length){
    now.value=position;
  }else{
    switch(position){
      case 'prev':
        now.value=now.value>0?now.value-1:0
      break;
      case 'next':
        let length=props.subjects.length-1
        now.value=(now.value<length)?now.value+1:length
      break;
      case 'first':
        now.value=0;
      break;
      case 'last':
        now.value=props.subjects.length-1;
      break;
      default:
        Swal.fire({
          title:"沒有指定的題目",
          icon:"error",
          confirmButtonColor: '#3085d6',
          confirmButtonText: '確定'
        })
    }
  }

  subs.value.forEach((dom)=>{
    if(dom.classList.contains('block')){
      dom.classList.remove('block')
      dom.classList.add('hidden')
    }
  })
  subs.value[now.value].classList.remove('hidden')
  subs.value[now.value].classList.add('block')
}

onMounted(()=>{
  subs.value[0].classList.remove('hidden')
  subs.value[0].classList.add('block')
})
const subs=ref(new Array())
</script>
<template>
<FrontLayout>
  <h3 class="p-4 border w-full bg-lime-100 shadow text-xl text-center">模擬測驗</h3>
  <div>測驗及練習</div>
  <div>題庫:{{ bank.name }}{{bank.levelC}}級</div>
  <div>類型:{{ type }}</div>
  <!---加入第一題及最後一題按鈕-->
  <QuizButton color="green" @click="go('first')">第一題</QuizButton>
  <QuizButton color="green" @click="go('prev')">上一題</QuizButton>

  <!--使用下拉選單來跳題,綁定變數jump,觸發change時,跳到指定題目去-->
  跳到第<select  v-model="jump" @change="go(jump)"
               class="py-1 pl-4 pr-9 mx-2 rounded-lg">
      <option v-for="s,i in subjects.length" :key="i" :value="i">{{ i+1 }}</option>
  </select>題
  <QuizButton color="green" @click="go('next')">下一題</QuizButton>
  <QuizButton color="green" @click="go('last')">最後一題</QuizButton>
  <div>
    <div v-for="subject , idx in subjects" 
         :key="subject.id"
         class="hidden"
         ref="subs">
      {{ idx+1 }}.
      <SingalSubject :subject="subject" :form="form[idx+1]" />
    </div>
        <QuizButton @click="submit" color="teal-dark" size="xl">
            送出
        </QuizButton>
  </div>
</FrontLayout>
</template>

這樣就完成了答題模式的改變了,當然做法不只一種,各位也可以想想有沒有不動到DOM的做法,由資料面去思考也是一種做法,不管那一種做法,我的原則是不增加太多原有機制的負擔,所以原本的答題和結果顯示的機制不會因為我改了答題模式而需要做什麼變動,


上一篇
Day27 計分模式 - 前置規劃做的好,改東改西沒煩惱
下一篇
Day29 測驗狀態紀錄-前端計時器、取消作答、提前結束
系列文
LV的全端開發體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言