雖然是小案子,但完整度還是要有,為了避免做到後期才發現有問題要打掉重做,所以我會先試著把主要的頁面流程先順過一次,這個過程會有點囉嗦,但只要調整妥當,後面的其它功能都是比照辦理了,接下來會建立一堆檔案來備用。
<script setup>
import { Head, Link } from "@inertiajs/inertia-vue3";
import { reactive } from "vue";
const props = defineProps({
canLogin: Boolean,
canRegister: Boolean,
username: String,
role: String,
});
const userInfo = reactive({
user: { link: route("userhome"), string: "會員中心",},
admin: { link: route("backstage"), string: "管理中心", },
});
</script>
<template>
<!--inertia的HEAD組件可以用來改變瀏灠器分頁上的標籤文字(title)-->
<Head title="學科測驗系統" />
<!--這一段是註冊登入用的,將來可以改成sticky的標題功能列-->
<div class="relative">
<div v-if="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">
{{ username }}
<Link v-if="$page.props.auth.user"
:href="userInfo[role].link"
class="text-sm text-gray-700 dark:text-gray-500 underline">
{{ userInfo[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="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">
<div class="m-4">
<div>選擇測驗項目:</div>
<input type="radio" name="quizbank" value="3" checked />網頁設計丙級
<input type="radio" name="quizbank" value="2" />網頁設計乙級
</div>
<div class="m-4">
<div>選擇測驗類型:</div>
<input type="radio" name="type" value="test" checked />測驗
<input type="radio" name="type" value="practice" />練習
</div>
<button class="px-6 py-2 border rounded-lg bg-blue-700 text-blue-100">
開始
</button>
<div class="m-4">
<div>瀏灠題庫:</div>
<Link class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">
網頁設計丙級
</Link>
<Link class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">
網頁設計乙級
</Link>
</div>
</div>
</div>
</template>
看起來很陽春對吧?視覺我會留到最後面再來調整,或是中間真的看不下去時才會小改一下,先確定主要的功能能實踐,基本的畫面和操作都OK,其它的修補或增減就都是小事了。
從這個首頁出發,後面肯定在測驗及練習的動作上要再區分出訪客和使用者行為的不同,訪客就只能單純的網站上進行測驗,測驗完後看到成績就結束了,而使用者則是可以把結果存起來,下次登入時可以看到結果。
在這個首頁的功能上,會延伸出兩個頁面的需求,一是開始測驗或練習的頁面,二是瀏灠題庫的頁面,就順便把這兩個頁面也先建立起來,內容空白也沒關係:
StartTest.vue
<script setup></script>
<template>
<div>測驗及練習</div>
</template>
BrowserQuizBank
<script setup></script>
<template>
<div>瀏灠題庫</div>
</template>
為了讓我們點下 開始 及 瀏灠題庫 時可以切到對應的頁面去,所以我們回到laravel去設定Route及Controller,額外提一下,因為使用了Inertia.js這個套件,所以前端的路由也是交由Inertia去處理,我們只要像原本在後端一樣在web.php中定義路由就可以了:
先建立兩個 Controller :
php artisan make:controller QuizbankController
php artisan make:controller TestController
在 web.php 中建立 Route :
Route::get('/start-test/{bank}/{type}',[TestController::class,'startTest']);
Route::get('/browser-quiz/{bank}',[BankController::class,'browser']);
由於這些Route會同時帶著相關的參數到Controller去,讓我們知道要回傳什麼資料給前端頁面,所以我們要讓Controller可以接收這些參數.
移動到Controller建立對應的方法及回傳頁面:
TestController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;
class TestController extends Controller
{
function startTest($quizbank,$type){
return Inertia::render('StartTest', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'username'=>Auth::user()->name??'訪客',
'role'=>Auth::user()->role??'guest',
'quizbank'=>$quizbank,
'type'=>$type
]);
}
}
QuizbankController
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;
class QuizbankController extends Controller
{
function browser($quizbank){
return Inertia::render('BrowserQuizBank', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'username'=>Auth::user()->name??'訪客',
'role'=>Auth::user()->role??'guest',
'quizbank'=>$quizbank,
]);
}
}
因為我們會使用到不少套件的功能,所以在撰寫Controller時,別忘了在上方要去引入相關的套件(use)。
可以看到我們回傳頁面時帶了不少參數過去,這表示我們可以在前端把這些參數都讀出來,修改一下 StartTest.vue 和 BrowserQuizBank 的內容:
StartTest.vue
<script setup>
const props = defineProps({
canLogin: Boolean,
canRegister: Boolean,
username: String,
role: String,
quizbank: Number,
type: String,
});
</script>
<template>
<div>測驗及練習</div>
<div>題庫:{{ quizbank }}</div>
<div>類型:{{ type }}</div>
</template>
BrowserQuizBank
<script setup>
const props = defineProps({
canLogin: Boolean,
canRegister: Boolean,
username: String,
role: String,
quizbank: Number,
});
</script>
<template>
<div>瀏灠題庫</div>
<div>題庫:{{ quizbank }}</div>
</template>
原本的首頁中對於 開始 按鈕和 瀏灠題庫 的連結並沒有設定,現在我們建好了控制器和路由,是時候把連結補上了:
routes\web.php 加上路由命名:
Route::get('/start-test/{quizbank}/{type}',[TestController::class,'startTest'])
->name('test.start');
Route::get('/browser-quiz/{quizbank}',[QuizbankController::class,'browser'])
->name('quiz.browser');
Home.vue 建立連結和v-mode
<script setup>
import { Head, Link } from "@inertiajs/inertia-vue3";
import { reactive } from "vue";
const props = defineProps({
canLogin: Boolean,
canRegister: Boolean,
username: String,
role: String,
});
const userInfo = reactive({
user: { link: route("userhome"), string: "會員中心", },
admin: { link: route("backstage"), string: "管理中心", },
});
//用來綁定選擇項目的變數
const testSelect = reactive({ quizbank: 3, type: "test" });
</script>
<template>
<Head title="學科測驗系統" />
<div class="relative">
<div v-if="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">
{{ username }}
<Link v-if="$page.props.auth.user"
:href="userInfo[role].link"
class="text-sm text-gray-700 dark:text-gray-500 underline">
{{ userInfo[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="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">
<div class="m-4">
<div>選擇測驗項目:</div>
<input type="radio" name="quizbank" value="3"
v-model="testSelect.quizbank"/> 網頁設計丙級
<input type="radio" name="quizbank" value="2"
v-model="testSelect.quizbank"/> 網頁設計乙級
</div>
<div class="m-4">
<div>選擇測驗類型:</div>
<input type="radio" name="type" value="test"
v-model="testSelect.type" />測驗
<input type="radio" name="type" value="practice"
v-model="testSelect.type" />練習
</div>
<!--原本的Button改成Inertia內建的Link組件,有a link的功能-->
<Link :href="route('test.start', testSelect)"
class="px-6 py-2 border rounded-lg bg-blue-700 text-blue-100">
開始
</Link>
<div class="m-4">
<div>瀏灠題庫:</div>
<!--在題庫連結加上路由和參數-->
<Link :href="route('quiz.browser', 3)"
class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">
網頁設計丙級
</Link>
<Link :href="route('quiz.browser', 2)"
class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">
網頁設計乙級
</Link>
</div>
</div>
</div>
</template>
點選開始測試後的畫面,路由的參數和選擇的項目是一致的
選擇瀏灠題庫後的畫面
這樣就是一個前後端傳值及回傳頁面的內容,雖然我們什麼資料都還沒開始建,但是畫面有東西可以點,而且相關的回應都是正確的,感覺離成功邁進了一大步呢!!
有些參數目前沒用到,因為我自己是習慣先把畫面的流程大致先做出來,會用到的Controller和Route都先定出來,在這過程中也同時再去檢視先前計畫的內容有沒有遺漏或需要再補強的,然後再開始去建資料表。
依照這個 畫面->控制器->路由->畫面 的循環順序,可以很快的把使用者第一層會用到的前後台畫面流程先定下來;
但是我們做著做著想到一個問題,將來的頁面一定會愈來愈多,如果像現在這樣都放在 Pages 目錄下,日後一定難以管理,所以我們可以仿照 Breeze 做的會員機制,在 Pages 目錄下開設不同類別的資料夾來管理不同屬性的頁面檔案,日後的組件管理也是一樣的道理,同樣的邏輯也會用在 Controllers 中,所以我先預告一下這個小案子接下來會開一堆資料夾來把各類型功能的檔案分開做管理。
在 resources\js\Pages 目錄下建立兩個後台頁面的目錄,並把 Backstage.vue 和 UserHome.vue 搬進去
調整下Controller中的頁面回傳路徑:
app\Http\Controllers\UserController.php
class UserController extends Controller
{
function userCenter(){
return Inertia::render('UserHome/UserHome');
}
}
app\Http\Controllers\BackstageController.php
class BackstageController extends Controller
{
function controlPanel() {
return Inertia::render('Backstage/Backstage');
}
}
最後,我們來調整一下後台的頁面,從原本的提示文字改成有點樣子的後台,需要增加的路由和Controller的方法也都要對應:
使用者中心
resources\js\Pages\UserHome\UserHome.vue
<script setup>
import AuthenticatedUserLayout from "@/Layouts/AuthenticatedUserLayout.vue";
import { Head } from "@inertiajs/inertia-vue3";
</script>
<template>
<Head title="會員中心" />
<AuthenticatedUserLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">會員中心</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200 flex">
<div class="border rounded-xl p-4 w-1/2">
<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 class="border rounded-xl p-4 w-1/2">
<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>
</div>
</div>
</div>
</div>
</AuthenticatedUserLayout>
</template>
管理後台首頁
resources\js\Pages\Backstage\Backstage.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/inertia-vue3";
</script>
<template>
<Head title="管理中心" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">管理中心</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200 flex">
<div class="w-1/6 border rounded-xl p-4">
<Link :href="route('backstage.bank')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理題庫
</Link>
<Link :href="route('backstage.quiz')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理試卷
</Link>
<Link :href="route('backstage.test')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理測驗
</Link>
<Link :href="route('backstage.group')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理群組
</Link>
</div>
<div class="w-5/6 border rounded-xl p-4 flex flex-wrap">
<div class="w-1/2 border rounded-xl flex p-4 h-48 bg-green-400">
題庫:
</div>
<div class="w-1/2 border rounded-xl flex p-4 h-48 bg-sky-400">
試卷:
</div>
<div class="w-1/2 border rounded-xl flex p-4 h-48 bg-yellow-400">
測驗:
</div>
<div class="w-1/2 border rounded-xl flex p-4 h-48 bg-orange-400">
群組:
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
routes\web.php
Route::get('/backstage/banks',[BackstageController::class,'bankList'])
->name('backstage.bank');
Route::get('/backstage/quizzes',[BackstageController::class,'quizList'])
->name('backstage.quiz');
Route::get('/backstage/tests',[BackstageController::class,'testList'])
->name('backstage.test');
Route::get('/backstage/groups',[BackstageController::class,'groupList'])
->name('backstage.group');
後台題庫頁面
resources\js\Pages\Backstage\Banks.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/inertia-vue3";
</script>
<template>
<Head title="管理中心" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">管理中心</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200 flex">
<div class="w-1/6 border rounded-xl p-4">
<Link :href="route('backstage.bank')"
class="block my-2 py-2 px-4 bg-slate-200">
管理題庫
</Link>
<Link :href="route('backstage.quiz')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理試卷</Link>
<Link :href="route('backstage.test')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理測驗
</Link>
<Link :href="route('backstage.group')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理群組
</Link>
</div>
<div class="w-5/6 border rounded-xl p-4">
<button class="py-2 px-3 border rounded-xl bg-blue-700 text-blue-100 my-4">
新增題庫
</button>
<div class="w-full border rounded-xl flex p-4 bg-green-400 justify-between">
<div>題庫一</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-green-400 justify-between">
<div>題庫二</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-green-400 justify-between">
<div>題庫三</div>
<div>編輯 / 刪除</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
後台試卷頁面
resources\js\Pages\Backstage\Quizzes.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/inertia-vue3";
</script>
<template>
<Head title="管理中心" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">管理中心</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200 flex">
<div class="w-1/6 border rounded-xl p-4">
<Link :href="route('backstage.bank')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理題庫
</Link>
<Link :href="route('backstage.quiz')"
class="block my-2 py-2 px-4 bg-slate-200">
管理試卷
</Link>
<Link :href="route('backstage.test')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理測驗
</Link>
<Link :href="route('backstage.group')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理群組
</Link>
</div>
<div class="w-5/6 border rounded-xl p-4">
<button class="py-2 px-3 border rounded-xl bg-blue-700 text-blue-100 my-4">
新增試卷
</button>
<div class="w-full border rounded-xl flex p-4 bg-sky-400 justify-between">
<div>試卷一</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-sky-400 justify-between">
<div>試卷二</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-sky-400 justify-between">
<div>試卷三</div>
<div>編輯 / 刪除</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
後台測驗頁面
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/inertia-vue3";
</script>
<template>
<Head title="管理中心" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">管理中心</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200 flex">
<div class="w-1/6 border rounded-xl p-4">
<Link :href="route('backstage.bank')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理題庫
</Link>
<Link :href="route('backstage.quiz')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理試卷
</Link>
<Link :href="route('backstage.test')"
class="block my-2 py-2 px-4 bg-slate-200">
管理測驗
</Link>
<Link :href="route('backstage.group')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理群組
</Link>
</div>
<div class="w-5/6 border rounded-xl p-4">
<div class="w-full border rounded-xl flex p-4 bg-yellow-400 justify-between">
<div>測驗一</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-yellow-400 justify-between">
<div>測驗二</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-yellow-400 justify-between">
<div>測驗三</div>
<div>編輯 / 刪除</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
後台群組頁面
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/inertia-vue3";
</script>
<template>
<Head title="管理中心" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">管理中心</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 bg-white border-b border-gray-200 flex">
<div class="w-1/6 border rounded-xl p-4">
<Link :href="route('backstage.bank')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理題庫
</Link>
<Link :href="route('backstage.quiz')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理試卷
</Link>
<Link :href="route('backstage.test')"
class="block my-2 py-2 px-4 hover:bg-slate-200">
管理測驗
</Link>
<Link :href="route('backstage.group')"
class="block my-2 py-2 px-4 bg-slate-200">
管理群組
</Link>
</div>
<div class="w-5/6 border rounded-xl p-4">
<button class="py-2 px-3 border rounded-xl bg-blue-700 text-blue-100 my-4">
新增群組
</button>
<div class="w-full border rounded-xl flex p-4 bg-orange-400 justify-between">
<div>群組一</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-orange-400 justify-between">
<div>群組二</div>
<div>編輯 / 刪除</div>
</div>
<div class="w-full border rounded-xl flex p-4 bg-orange-400 justify-between" >
<div>群組三</div>
<div>編輯 / 刪除</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
我們現在有一個沒有實際功能,但前後台畫面看起來有那麼一回事的東西了,而且它也不完全是靜態頁面,有透過後端來通知前端要顯示什麼;
目前的畫面和程式碼都是暫定的,只是用來確認最低限度需要的畫面和路由有那些,並且把需要處理的方法都先建立出來,萬事起頭難,我們已經克服最難的階段了。
現在看後台的頁面程式碼會發現有大量的重覆,這部份等我們回頭再來做前端時,會利用組件化來把這些重覆的程式碼都做精簡,也方便管理。