花了不少時間在處理後台的架構及一些套件的使用,接著,我們要來把測驗結果做儲存,僅限註冊會員可以儲存,非會員則是使用先前的 excel 匯出來自行留存。
測驗結果的資料表只需要儲存整份測驗的json格式就可以了,因此不需要設計太複雜的欄位,測驗結果也是會簡化成json的格式直接存入欄位即可,不過這樣的簡化對於之後的數據分析會是一個挑戰,但到時再說了,沒有解決不了的問題。
先建立一個測驗資料表
php artisan make:model Test -m
database\migrations\xxxxxx_create_tests_table.php 資料表欄位設定
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('tests', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id');
$table->text('subjects');
$table->text('result')->default(null);
$table->string('type');
$table->unsignedTinyInteger('score')->default(0);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('tests');
}
};
Model關聯設定
app\Models\Test.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Test extends Model
{
use HasFactory;
protected $guarded=[];
function user()
{
return $this->belongsTo(User::class);
}
}
app\Models\User.php
function tests()
{
return $this->hasMany(Test::class);
}
在測驗結果頁增加一個按鈕,只有登入的會員才能看到,點擊後儲存測驗結果:
.....略
<button v-if="$page.props.auth.user"
class="px-4 py-2 bg-lime-100 mx-3 text-green-900 border rounded-lg"
@click="testSave">
儲存結果
</button>
增加一個儲存測試結果用的路由
routes\web.php
Route::post('/test/save/{userId}',[TestController::class,'testSave'])
->name('test.save');
在Controller傳遞資料,這裏其實應該要對傳來的資料做基本的驗證,不過我們先略過。
app\Http\Controllers\TestController.php
function testSave(Request $request ,$userId)
{
$this->test->resultSave($request->input(),$userId);
return redirect("/");
}
在Service中處理測驗的資料,這邊主要是把題目的內容簡化,像是文字的部份就不存了,讓資料表中的資料最小化,之後取出時再去撈關聯的資料就可以了,而result欄位則是用來統計測試的結果,減少日後每次重新讀取測驗再計算的工夫。
app\Services\TestService.php
//之前是用臨時的數字,現在有資料表了,可以做正式的資料回傳
function infos()
{
return ['count'=>$this->test->count()];
}
function resultSave($data,$userId)
{
$subjects=[];
$result=[];
collect($data['subjects'])
->each(function($subject,$idx)use(&$subjects){
$subjects[]=['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'],
];
});
$result['total']=count($data['subjects']);
$result['correct']=collect($data['subjects'])
->where('result',true)
->count();
$result['wrong']=collect($data['subjects'])
->where('result',false)
->count();
$this->test->save(['user_id'=>$userId,
'subjects'=>json_encode($subjects),
'result'=>json_encode($result),
'type'=>$data['type']]);
}
在Repository中做存入資料表的動作
app\Repositories\TestRepository.php
namespace App\Repositories;
use App\Models\Test;
class TestRepository
{
function __construct(protected Test $test){}
function count(){ return $this->test->count(); }
function all(){ return $this->test->all(); }
function save($testResult)
{
//補上測驗存入的時間
$testResult['created_at']=date("Y-m-d H:i:s");
$testResult['updated_at']=date("Y-m-d H:i:s");
$this->test->insert($testResult);
}
}
由於我們可以在管理者後台檢視測驗活動的狀況,所以如果有人完成測驗並存入資料表中,管理者後台可以看到最新的數據:
可以儲存測驗的結果,當然也要讓使用者可以再次拿出來看結果,所以我們來整修一下會員中心,先把會員中心仿照管理後台的版面重建一下,未來會慢慢加上更多的功能上去;
先來建立會員中心的幾個後台路由,並且將會員中心的路由都加上中介層的檢查,而且會員中心應該要綁定會員的ID來做區隔(突然想到後台管理者如果不只一人,是不是也一樣?),這關係到我們前面做的一堆和路由相關的設計,所以和會員中心相關的路由都要再加上id:
routes\web.php
Route::middleware(['auth', 'verified','role:user'])->group(function(){
Route::get('/userhome/{user_id}', [UserController::class,'userCenter'])
->name('userhome');
Route::get('/userhome/{user_id}/tests', [UserController::class,'testList'])
->name('userhome.tests');
Route::get('/userhome/{user_id}/analysis', [UserController::class,'testAnalysis'])
->name('userhome.analysis');
});
登入及中介層的路由也要修改
app\Http\Middleware\RedirectIfAuthenticated.php
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
//根據登入者不同的角色,導向到不同的頁面去
switch(Auth::user()->role){
case 'user':
dd(Auth::user()->id);
return redirect()->route('userhome',Auth::user()->id);
break;
case 'admin':
return redirect(RouteServiceProvider::HOME);
break;
}
}
}
return $next($request);
}
app\Http\Middleware\RedirectByAuthcatedRole.php
public function handle(Request $request, Closure $next, $guard)
{
//根據登入者不同的角色,導向到不同的頁面去
if(Auth::user()->role!==$guard){
switch(Auth::user()->role){
case 'user':
return redirect()->route('userhome',Auth::user()->id);
break;
case 'admin':
return redirect(RouteServiceProvider::HOME);
break;
}
}
return $next($request);
}
登入
app\Http\Controllers\Auth\AuthenticatedSessionController.php
public function store(LoginRequest $request)
{
$request->authenticate();
$request->session()->regenerate();
switch(Auth::user()->role){
case 'user':
return redirect()->intended(route('userhome',Auth::user()->id));
break;
case 'admin':
return redirect()->intended(RouteServiceProvider::HOME);
break;
}
}
註冊
app\Http\Controllers\Auth\RegisteredUserController.php
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect()->route('userhome',Auth::user()->id);
}
調整一下會員中心的頁面
resources\js\Layouts\AuthenticatedUserLayout.vue 和管理者後台幾乎一樣,只是路由連結要加上user_id及增加一個會員中心的選單不一樣
.....略
<!-- Logo -->
<div class="shrink-0 flex items-center">
<Link :href="route('userhome',$page.props.auth.user.id)">
<ApplicationLogo class="block h-9 w-auto" />
</Link>
</div>
.....略
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<NavLink
:href="route('userhome',$page.props.auth.user.id)"
:active="route().current('userhome',$page.props.auth.user.id)">
會員中心
</NavLink>
</div>
.....略
<!-- Responsive Navigation Menu -->
<div class="pt-2 pb-3 space-y-1">
<ResponsiveNavLink :href="route('userhome',$page.props.auth.user.id)"
:active="route().current('userhome',$page.props.auth.user.id)">
會員中心
</ResponsiveNavLink>
</div>
.....略
<!-- Page Content -->
<main class="relative z-10">
<!--後台主內容區-->
<div class="w-full bg-white flex">
<UserhomeLeftMenu />
<slot />
</div>
</main>
resources\js\Layouts\UserhomeLeftMenu.vue也幾乎一樣,只是選單名稱不一樣,及路由加上ID
<script setup>
import { storeToRefs } from "pinia";
import { menuStore } from "@/Stores/MenuStore.js";
import MenuItem from "@/QuizComponents/MenuItem.vue";
const store = menuStore();
const { selected } = storeToRefs(store);
</script>
<template>
<!--左側選單-->
<div class="w-48 max-w-96 bg-black text-white py-8 px-2 shadow-lg h-[calc(100vh_-_138px)]">
<MenuItem :href="route('userhome.tests',$page.props.auth.user.id)"
:onSelected="selected['測驗紀錄']">
測驗紀錄
</MenuItem>
<MenuItem :href="route('userhome.analysis',$page.props.auth.user.id)"
:onSelected="selected['統計分析']">
統計分析
</MenuItem>
</div>
</template>
最後是前台首頁的路由,右上的會員資訊及連結的網址也要帶上user_id
<script setup>
import { Head, Link } from "@inertiajs/inertia-vue3";
import { reactive } from "vue";
const userInfo = reactive({
user: { string: "會員中心", },
admin: { string: "管理中心", },
});
</script>
<template>
<Head title="學科測驗系統" />
<div class="relative">
<div v-if="$page.props.canLogin"
class="hidden sticky top-0 right-0 bg-sky-200 text-sm
text-sky-900 text-right px-6 py-4 sm:block w-full">
{{ $page.props.username }}
<Link v-if="$page.props.auth.user"
:href="route('userhome',$page.props.auth.user.id)"
class="text-sm text-gray-700 dark:text-gray-500 underline">
{{ userInfo[$page.props.role].string }}
</Link>
<template v-else>
<Link :href="route('login')"
class="text-sm text-gray-700 dark:text-gray-500 underline">
登入
</Link>
<Link v-if="$page.props.canRegister"
:href="route('register')"
class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">
註冊
</Link>
</template>
</div>
<!--前台主要內容區域-->
<div class="max-w-7xl m-auto p-4">
<slot />
</div>
</div>
</template>
接著到 app\Http\Controllers\UserController.php 把使用者的相關方法建立起來:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use App\Services\TestService;
class UserController extends Controller
{
function __construct(protected TestService $test){}
function userCenter($user_id){
return Inertia::render('UserHome/UserHome',
['tests'=>$this->test->testsByUserId($user_id)]);
}
function testList($user_id){
}
function testAnalysis($user_id){
}
}
app\Services\TestService.php中調整下回傳測驗資料的方法。
function all(){ return $this->test->all(); }
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);
return $t;
});
}
app\Repositories\TestRepository.php 處理資料表的資料回傳:
function all(){ return $this->test->all(); }
function testsByUserId($user_id)
{
return $this->test->where('user_id',$user_id)->get();
}
最後是 resources\js\Pages\UserHome\UserHome.vue 頁面的呈現
<script setup>
import AuthenticatedUserLayout from "@/Layouts/AuthenticatedUserLayout.vue";
import { Head, Link } from "@inertiajs/inertia-vue3";
const props=defineProps({tests:Array});
</script>
<template>
<Head title="會員中心" />
<AuthenticatedUserLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
會員中心
</h2>
</template>
<!--右側內容區-->
<div class="w-[calc(100%_-_12rem)] p-4 flex flex-wrap">
<div class="w-1/2 border rounded-xl flex p-4 h-full bg-sky-400">
<div class="w-full bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class=" p-4">
<div class="font-bold text-xl my-4">測驗紀錄:{{ tests.length }}</div>
<ul>
<li class="py-2 px-4 border rounded flex justify-between
text-center bg-lime-600 text-lime-50">
<div class="w-1/2">測驗時間</div>
<div class="w-2/12">題數</div>
<div class="w-2/12">正確</div>
<div class="w-2/12">錯誤</div>
</li>
<li v-for="test in tests" :key="test.id"
class="py-2 px-4 border rounded flex justify-between
text-center -mt-[1px]">
<div class="w-1/2">{{ test.time }}</div>
<div class="w-2/12">{{ test.result.total }}</div>
<div class="w-2/12">{{ test.result.correct }}</div>
<div class="w-2/12">{{ test.result.wrong }}</div>
</li>
</ul>
</div>
</div>
</div>
<div class="w-1/2 border rounded-xl flex p-4 h-full bg-green-400">
<div class="w-full bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-4">
<div class="font-bold text-xl my-4">統計分析</div>
<div class="pl-4">最高成績:</div>
<div class="pl-4">最低成績:</div>
<div class="pl-4">平均成績:</div>
<div class="pl-4">測驗次數:</div>
</div>
</div>
</div>
</div>
</AuthenticatedUserLayout>
</template>
不同的使用者會有不同的測驗紀錄
會員中心只會顯示近期的五筆紀錄,統計分析也只會顯示最基本的資料,然後左側選單點擊後會顯示各自更完整的內容。
顯示紀錄的部份我們明天再說,單純的顯示紀錄並不難,但是由於顯示測驗題目這件事在前台,兩個後台都會常用到,如果照原本的流程設計方式,我們會建立三個頁面來顯示,但其實這三個頁面的內容是差不多的,如果將來我想改變題目顯示的外觀或一些互動的話,是不是三個頁面都要改?只要同樣的事可能重覆三次以上,我們就要來想想有沒有可以簡化做法的方式(絕不是為了湊天數)