國家技術士的學科考試,有些職類或場地會提供上機作答,全國檢定則大多還是紙本,而線上作答的系統中有一個倒數計時的功能,我也想在自己的系統中加上這個,同時要依照使用者的狀態,除了去紀錄作答使用的時間外,也紀錄提前結束還是取消作答;
所以我們的測驗資料表中需要增加兩個欄位,一個用來記錄測驗用掉多少時間,一個是用來紀錄測驗的狀態(提前完成,準時完成,取消作答)
先來處理前端的東西,簡單的加上個計時器並且提供方法可以啟動及結束:
<script setup>
//.....略
import { onMounted, ref ,reactive,computed} from 'vue';
const props = defineProps({
bank: Object,
subjects:Array,
type: String,
seconds:{ type:Number, default:1200 }
});
//把後端傳來的秒帶入倒數杪變數countdown
const countdown=ref(props.seconds)
//使用computed來計算時分秒,監控對像為倒數秒countdown
const timer=reactive({hour:computed(() => Math.floor(countdown.value/3600)),
minute:computed(() => Math.floor((countdown.value%3600)/60)),
sec:computed(()=>Math.floor(countdown.value%60))})
//建立一個計時器的參考變數
const t=ref(null)
const startCountdown=()=>{
t.value=setInterval(()=>{
//當倒數秒大於0時就減一秒
if(countdown.value>0){
countdown.value--;
}else{
//當倒數秒結束時停止計時器
stopCountdown();
}}
,1000)
}
const stopCountdown=()=>{
clearInterval(t.value)
}
//.....略
</script>
<template>
//.....略
<!--獨立一個區塊當功能控制區-->
<div class="flex justify-between py-4">
<div>題庫:{{ bank.name }}{{bank.levelC}}級</div>
<div>
<QuizButton @click="startCountdown">開始</QuizButton>
<!--秒為一直存在的最小單位,時分結束後就消失-->
<span v-if="timer.hour">{{timer.hour}}時</span>
<span v-if="timer.minute">{{timer.minute}}分</span>
<span>{{timer.sec}}秒</span>
<QuizButton @click="stopCountdown">停止</QuizButton>
</div>
<div>
<QuizButton>提前結束</QuizButton>
<QuizButton>取消作答</QuizButton>
</div>
</div>
//.....略
</template>
接著我們來處理使用者測驗結束後要把時間和作答狀況寫入資料表,因為我們先前沒有設計這兩個欄位,所以現在來增加:
php artisan make:migration add_column_to_test --table=tests
database\migrations\xxxxxxxxx_add_column_to_tests.php
public function up()
{
Schema::table('tests', function (Blueprint $table) {
$table->unsignedInteger('seconds')->default(0);
$table->string('status');
});
}
public function down()
{
Schema::table('tests', function (Blueprint $table) {
$table->dropColumn('seconds');
$table->dropColumn('status');
});
}
然後我們在前端把各種狀態及資訊寫入到要傳送給後端的資料包中,這些狀態或非題目的資訊,通常都是觸發事件後發生的比始按下提前結束時,會觸發停止計時,然後把時間和'提前結束'這個狀態記入表單資料中,所以要在各項事件中來做紀錄;
其中,取消作答和提前結束不一樣的地方是取消作答會清空先前所做的所有選擇,等於是放棄這次的測驗:
resources\js\Pages\StartTestSingal.vue
<script setup>
//.....略
//建立兩個方法分便傳入不同的參數
const earlyClosure=()=>{
submit('earlyClosure')
}
const giveUp=()=>{
submit('giveup')
}
//修改送出的方法,依照帶入的參數來決定
const submit=(action)=>{
stopCountdown();
switch(action){
case 'earlyClosure':
form.status='提前結束';
form.seconds=(props.subjects.length*60)-countdown.value
break;
case 'giveUp':
form.status='取消作答';
form.seconds=(props.subjects.length*60)-countdown.value
//遍歷所有的題目並取消所有的作答內容
Object.keys(form).forEach((idx)=>{
if(Number.isInteger(idx*1)){
form[idx].seq=0;
form[idx].optionId=0
}
})
break;
default:
if(countdown.value>0){
form.status='提前結束';
form.seconds=(props.subjects.length*60)-countdown.value
}else{
form.status='完成';
form.seconds=props.subjects.length*60
}
}
form.post(route('test.result'))
}
</script>
<template>
//.....略
<!--建立按鈕提供使用者使用-->
<QuizButton @click="earlyClosure">提前結束</QuizButton>
<QuizButton @click="giveUp">取消作答</QuizButton>
//.....略
<!--調整一下原本的送出方法,帶入參數-->
<QuizButton @click="submit('finish')" color="teal-dark" size="xl">
送出
</QuizButton>
</template>
接著是處理後端,我們要先在測驗結果頁增加測驗用時及狀態這兩個屬性:
app\Http\Controllers\TestController.php
function testResult(Request $request){
$subjects=$this->test->check($request->input());
$result=$this->test->resultAnalyze($subjects);
if($request->type=='test'){
$result=$this->test->score($result);
}
return Inertia::render('TestResult', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'username'=>Auth::user()->name??'訪客',
'role'=>Auth::user()->role??'guest',
'bank'=>$this->bank->find($request->bankId),
'subjects'=>$subjects,
'result'=>$result,
'type'=>$request->type,
'seconds'=>$request->seconds, //增加測驗用時
'status'=>$request->status //增加測驗狀態
]);
}
因為增加了表單內容,所以service端的處理也要調整一下,但這裏衍生了一個問題,如果之後我還要再加加減減些什麼欄位或功能,是不是就一直要來這邊修改,這樣就失去了一開始希望的單純及拆分清楚的目標了,所以這邊我就會在開發日誌上記錄一下,這個對答案的方式不應該這樣做,但改這個功能會需要前端表單也一併調整,先hold住:
app\Services\TestService.php
function check($form)
{
$bankId=$form['bankId'];
$type=$form['type'];
//移除表單中不屬於題目的欄位屬性
unset($form['bankId'], $form['type'], $form['seconds'], $form['status']);
$form=collect($form);
.....略
return $subjects;
}
後端有回傳資料,前端就可以使用:
resources\js\Pages\TestResult.vue
<script setup>
//.....略
const props = defineProps({
bank: Object,
subjects:Array,
type: String,
result:Object,
seconds:Number,
status:String
//.....略
});
</script>
<template>
//.....略
<div v-if="seconds">用時:{{ seconds }}</div>
<div v-if="status">狀態:{{ status }}</div>
//.....略
</template>
同樣的結果頁面,我們在匯出和儲存時一樣的可以送出新增的欄位資料,這邊先以儲存來當例子:
resources\js\Pages\TestResult.vue
<tempalte>
//.....略
<QuizButton>
<Link v-if="$page.props.auth.user"
:href="route('test.save',$page.props.auth.user.id)"
method="post"
:data="{subjects:props.subjects,
type:props.type,
result:props.result,
seconds:props.seconds,
status:props.status}"
as="button"
type="button"
color="green-dark" size="lg">
儲存結果
</Link>
</QuizButton>
</template>
資料儲存增加這兩個新的欄位:
app\Http\Controllers\TestController.php
function testSave(Request $request ,$userId)
{
$this->test
->resultSave(
['user_id'=>$userId,
'subjects'=>json_encode($this->test->zipSubjects($request->subjects)),
'result'=>json_encode($request->result),
'type'=>$request->type,
'score'=>$request->result['score']??0,
'seconds'=>$request->seconds,
'status'=>$request->status
]);
return redirect("/");
}
然後我們調整一下使用者後台提供資料的方法,增加兩個欄位回傳
app\Services\TestService.php
function testsByUserId($user_id)
{
return $this->test
->testsByUserId($user_id)
->map(function($test){
$t=[];
$t['id']=$test->id;
$t['time']=date("Y-m-d H:i:s",strtotime($test->created_at));
$t['result']=json_decode($test->result);
$t['type']=$test->type;
$t['seconds']=$test->seconds;
$t['status']=$test->status;
return $t;
});
}
回傳資料有新增,前端也調整一下:
resources\js\Pages\UserHome\UserHome.vue
<script setup>
//.....略
</script>
<template>
//.....略
<ul>
<li class="py-2 px-4 border rounded flex justify-between text-center bg-lime-600 text-lime-50">
<div class="w-1/4">測驗時間</div>
<div class="w-1/12">題數</div>
<div class="w-2/12">正確/錯誤</div>
<div class="w-1/12">類型</div>
<div class="w-1/12">用時</div>
<div class="w-2/12">狀態</div>
<div class="w-1/12">成績</div>
</li>
<Link v-for="test in tests" :key="test.id"
:href="route('test.record',{user_id:$page.props.auth.user.id,id:test.id})"
class="py-2 px-4 border rounded flex justify-between text-center -mt-[1px] hover:bg-slate-200 cursor-pointer">
<div class="w-1/4">{{ test.time }}</div>
<div class="w-1/12">{{ test.result.total }}</div>
<div class="w-2/12">
<span class="text-green-600">{{ test.result.correct }}</span>
/
<span class="text-red-600">{{ test.result.wrong }}</span>
</div>
<div class="w-1/12">{{ test.type }}</div>
<div class="w-1/12">{{ test.seconds }}秒</div>
<div class="w-2/12">{{ test.status }}</div>
<div class="w-1/12">{{ test.result.score }}</div>
</Link>
</ul>
//.....略
</template>
這邊其實還有很多細節要調整,但礙於時間和篇幅,今天先做到這邊好,測試用的開始計時按鈕會移除,改成在這個頁面 onMounted
的時候就啟動計時,或是中間插個 SweetAlert
的提示,SweetAlert
之後才開始計時,而停止計時則在使用者點擊提前結束、取消作答或送出測驗時執行。
後續的功能新增和調整就是一直前後端前後端這樣跳來跳去的。