大致上需要的功能和模組都處理的差不多了,前後端之間的關係也算熟悉了,接下來就是把其它功能一個一個的補上去,不過目前為止都還只是在驗證我想做的功能在這個全端解決方案下是可行的,距離完成還有段不小的距離呢。
今天要來做的是計分模式,就是把原本的練習模式加上算分數的方法,把原本寫死的一些數值參數化。
首先,我們在首頁增加選題數的功能,當然,未來可以做成選單或輸入的方式,各位可以自由發揮。
resources\js\Pages\Home.vue
<script setup>
.....略
const testSelect = reactive({ quizbank: props.banks[0].id,
type: "test" ,
amount:20});
</script>
<template>
.....略
<div class="m-4">
<div>選擇數量:</div>
<input type="radio" name="amount" value="20" v-model="testSelect.amount" />20
<input type="radio" name="amount" value="40" v-model="testSelect.amount" />40
<input type="radio" name="amount" value="60" v-model="testSelect.amount" />60
<input type="radio" name="amount" value="80" v-model="testSelect.amount" />80
</div>
.....略
</template>
產生題目的部份只需要小改,先前我們是寫死數量,現在把表單傳來的數量帶進去就可以了:
app\Http\Controllers\TestController.php
function startTest(Request $request,$bank_id,$type){
return Inertia::render('StartTest', [
'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
]);
}
這樣,前端測驗功能就改完了;
接著我們來處理算分數的問題,先前我們已經做完對答案的功能了(複選題還沒處理),但是統計結果我們是寫在要儲存時才去做的,現在我們要把本原本的儲存功能拆成統計結果,壓縮題目(減少存進資料表的容量),及儲存結果三個函式:
app\Services\TestService.php
//原本的儲存函式名稱不變,但功能只負責把資料傳去Repository做新增資料的動作
function resultSave($result)
{
$this->test->save($result);
}
//用來精簡題目內容,簡省儲存空間
function zipSubjects($subjects){
$subjects=collect($subjects);
return $subjects->map(function($subject,$idx){
$tmp=['id'=>$subject['id'],
'seq'=>$idx+1,
'select'=>$subject['selectSeq'],
'options'=>collect($subject['options'])
->map(function($option,$i){
unset($option['option'],$option['subject_id']);
$option['seq']=$i+1;
return $option;
}),
'result'=>$subject['result'],
];
return $tmp;
});
}
//用來分析題目內容的題數及對錯數量
function resultAnalyze($subjects){
$result=[];
$result['total']=count($subjects);
$result['correct']=collect($subjects)
->where('result',true)
->count();
$result['wrong']=collect($subjects)
->where('result',false)
->count();
return $result;
}
接著我們根據測驗模式來計算成績,因為我們已經把功能拆得很散了,所以現在只要根據統計結果來算分數就可以了,而回傳的資料是原本的資料多一個成績的欄位而已:
app\Services\TestService.php
function score($result){
$perScore=round(100/$result['total'],1);
$result['score']=($perScore*$result['correct']>100)?100:$perScore*$result['correct'];
return $result;
}
回傳給結果頁面的 Controller 調整一下,原本的測驗結果頁面只有題目的對答結果,現在我們加上了統計的結果;
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
]);
}
即然結果頁的資料中多了成績或統計結果,那就可以顯示在畫面上了:
resources\js\Pages\TestResult.vue
<script setup>
.....略
const props = defineProps({
bank: Object,
subjects:Array,
type: String,
result:Object //增加接收一個屬性進來
});
.....略
</script>
<template>
<FrontLayout>
<div>測驗結果</div>
<div>題庫:{{ bank.name }}{{bank.levelC}}級</div>
<div>類型:{{ type }}</div>
<div>題目數:{{ result.total }}</div>
<div>正確:{{ result.correct }}</div>
<div>錯誤:{{ result.wrong }}</div>
<!--如果資料項中有score才顯示成績-->
<div v-if="result.score">成績:{{ result.score }}</div>
.....略
</FrontLayout>
</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]);
return redirect("/");
}
最後我們要讓使用者可以在會員中心方便的區分自己有那些紀錄是測驗,那些紀錄是練習,但是之前我們在取得測試資料時有些資料是沒有放在回傳資料集中的,要記得去補一下:
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; //增加測驗類別
return $t;
});
}
resources\js\Pages\UserHome\UserHome.vue
.....略
<ul>
<li class="py-2 px-4 border rounded flex justify-between text-center bg-lime-600 text-lime-50">
<div class="w-1/3">測驗時間</div>
<div class="w-2/12">題數</div>
<div class="w-2/12">正確/錯誤</div>
<div class="w-2/12">類型</div>
<div class="w-2/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/3">{{ test.time }}</div>
<div class="w-2/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-2/12">{{ test.type }}</div>
<div class="w-2/12">{{ test.result.score }}</div>
</Link>
</ul>
.....略
最後,我們在每筆測驗紀錄結果的顯示中一樣加上計分或統計結果
app\Services\TestService.php
function getRecordById($id){
$test=$this->test->find($id);
$subjects=collect(json_decode($test->subjects))
->map(function($subject){
$sub=$this->subject->find($subject->id);
$options=$sub->options;
$sub->selectOption=$options[$subject->select-1]->id;
$sub->selectSeq=$subject->select;
$seq=collect($subject->options)->where('ans',1)->first()->seq;
$sub->result=($seq==$subject->select)?true:false;
return $sub;
});
return ['subjects'=>$subjects,
'type'=>$test->type,
'result'=>json_decode($test->result)]; //回傳的資料增加統計結果
}
app\Http\Controllers\TestController.php
function testRecordByUser($user_id,$id){
$record=$this->test->getRecordById($id);
$bank=$this->bank->find($record['subjects'][0]->bank_id);
return Inertia::render('UserHome/TestRecord', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'username'=>Auth::user()->name??'訪客',
'role'=>Auth::user()->role??'guest',
'bank'=>$bank,
'subjects'=>$record['subjects'],
'type'=>$record['type'],
'result'=>$record['result'] //增加統計結果回傳
]);
}
resources\js\Pages\UserHome\TestRecord.vue
<script setup>
.....略
const props = defineProps({
bank: Object,
subjects:Array,
type: String,
resutl:Object
});
.....略
</script>
<template>
.....略
<!--右側內容區-->
<div class="w-[calc(100%_-_12rem)] p-4 ">
<div>
<div>測驗結果</div>
<div>題庫:{{ bank.name }}{{bank.levelC}}級</div>
<div>類型:{{ type }}</div>
<div>題目數:{{ result.total }}</div>
<div>正確:{{ result.correct }}</div>
<div>錯誤:{{ result.wrong }}</div>
<div v-if="result.score">成績:{{ result.score }}</div>
</div>
<div v-for="subject , idx in subjects" :key="subject.id">
{{ idx+1 }}.
<SingalSubject :subject="subject" />
</div>
</div>
.....略
</template>
先前我們做了把使用者的後台路由改成 name 時,把所有數字的路由都導到使用者後台的首頁去了,但並不是所有的路由都要到使用者首頁去,比如看成績結果的路由也有數字的 user_id
照 middleware
的邏輯,會改成名字,然後往 userhome
導向;
我們只想要替換路由中的 user_id 然後導向他原本的路由去,所以我們要先取出路由的名字或路徑,然後取代其中的 user_id 然後再導向:
app\Http\Middleware\RedirectByAuthcatedRole.php
if(Auth::user()->role==='user' && is_numeric($request->route('user_id'))){
$routeName=$request->route()->getName();
$parameters=$request->route()->parameters();
$parameters['user_id']=Auth::user()->name;
return redirect()->route($routeName,$parameters);
}
雖然還有一些細節要調整,尤其是視覺的呈現上;
同時做著做著,和一開始一樣,愈來愈會發現有不少的程式是可以再精簡或合併的,這是我喜歡寫程式這件事的原因之一,不管是多完整的事前計畫,真正在做時,總是能在實做過程中有些新的發現或體會。
不過我們做到這,也可以感受到一開始的辛苦把架構分層所帶來的好處,在後面要加功能或變動時,要去那邊改都清清楚楚的,不需要把程式碼重讀一次才會知道要改那裏,所以往往做到最後都是在重組這些拆分得乾淨清楚的方法或類別,真正寫程式碼的時間愈來愈少。