做到現在,都在忙功能,操作體驗不是太好,因為接下來的功能都是差不多的套路,所以是時候來提升一下操作體驗了。
modal是很常見的應用,以前我都是自己手刻一套來自己用,但這會讓我開發的準備期比較久,就像後台明明有不少套件可以用,但我不自覺得就自己做了。
這次前端的modal想來使用國人開發的一個開源專案 vue-final-modal
有滿完整的 中文官方文件可以參考,應該不會太難上手吧?
npm install vue-final-modal@next
resources\js\app.js
import { vfmPlugin } from 'vue-final-modal';
const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
const pinia = createPinia()
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, app, props, plugin }) {
return createApp({ render: () => h(app, props) })
.use(plugin)
.use(ZiggyVue, Ziggy)
.use(pinia)
.use(vfmPlugin) //註冊vue-final-modal
.mount(el);
},
});
resources\js\Pages\Home.vue
.....略
import { VueFinalModal } from "vue-final-modal";
const show=ref(false)
.....略
//在按鈕上註冊click事件來改變顯示狀態
<button class="px-4 py-2 border rounded-lg bg-sky-300 text-blue-900"
@click="show=true">modal</button>
//利用組件提供的屬性對內部的各區塊進行class設定,像是置中或大小設定等等
<vue-final-modal v-model="show" classes="flex justify-center items-center">
<div class="bg-white h-32 w-32 border rounded-lg text-center p-6">
modal test
</div>
</vue-final-modal>
</FrontLayout>
可想而知,如果要直接在每個頁面都寫這麼一大串modal的內容,那日後的閱讀和維護又會是一個痛點,所以我會把需要使用的modal再包成一個組件來使用。
這邊還有另一個問題是 Inertia
在 SPA
的支援上,對於 Modal
的應用並不是很友善,所以如果不會使用到Modal的話,那透過 Inertia
的確是可以不用寫Api,但如果會有 Modal
的應用的話,那透過 Api
及 axios
之類的 ajax 套件反而比較方便。
我們來嘗試把後台的增改刪都加上 Modal 的應用,讓後台的換頁次數可以少一點。
由於modal 會有子父元件互相傳值的問題,所以我們再次出動 pinia
來處理,建立一個給 modal
專用的 Store
:
resources\js\Stores\ModalStore.js
import { defineStore } from "pinia";
export const modalStore=defineStore('modal',{
state:()=>({
show:false,
}),
actions:{
modal(action){
switch(action){
case 'show':
this.show=true;
break;
case 'hide':
this.show=false;
break;
}
},
},
})
把原本的 BankForm
頁面,去掉外部的 Layout
,做成一個元件,並引入 Store
,以前用 emit 的方式來傳遞關閉的事件到父層去,現在改成是由 pinia
來通知 show
變數改變,接著父組件會被通知到 show
改變了,然後關閉modal:
resources\js\QuizComponents\BankForm.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link, useForm } from "@inertiajs/inertia-vue3";
import { modalStore } from "@/Stores/ModalStore.js";
import { storeToRefs } from "pinia";
const props = defineProps({
header: { type: String, default: "新增題庫" },
button: { type: String, default: "新增" },
bank: {
type: Object,
default: {
code: "",
name: "",
level: "A",
},
},
});
//以下為Store的引入
const store = modalStore();
const { show } = storeToRefs(store);
const { modal } = store;
const form = useForm(props.bank);
//原本的表單動作為完成後回到頁面(redirect())
//現在改成關閉modal
const submit = () => {
if (typeof form.id !== "undefined") {
form.put(route("bank.update", form.id),{
onFinish:()=>{modal('hide')}
});
} else {
form.post(route("bank.store"),{
onFinish:()=>{ modal('hide') }
});
}
};
</script>
<template>
<!--CSS的部份做了一些調整來適應modal-->
<div class="p-4 bg-white rounded-lg shadow-xl">
<h2 class="text-xl font-bold">{{ header }}</h2>
<div>
<div>
<label>題庫代碼:</label>
<input type="text" name="code" id="code" v-model="form.code" />
</div>
<div>
<label>題庫名稱:</label>
<input type="text" name="name" id="name" v-model="form.name" />
</div>
<div>
<label>題庫級別:</label>
<select name="level" id="level" v-model="form.level">
<option value="A">甲級</option>
<option value="B">乙級</option>
<option value="C">丙級</option>
<option value="D">單一級</option>
</select>
</div>
<button @click="submit" class="my-2 border px-6 py-2 rounded-xl shadow">
{{ button }}
</button>
</div>
</div>
</template>
原本的題庫主頁一樣引入Store及Modal:
resources\js\Pages\Backstage\Banks.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link } from "@inertiajs/inertia-vue3";
import { VueFinalModal } from 'vue-final-modal';
import QuizBankForm from "@/QuizComponents/BankForm.vue";
import { ref } from "vue";
import { modalStore } from "@/Stores/ModalStore.js";
import { storeToRefs } from "pinia";
const props = defineProps({ banks: Array, count: Number ,errors:Object});
//引入Store來使用
const store = modalStore();
const { show } = storeToRefs(store);
const { modal } = store;
</script>
<template>
<Head title="管理中心" />
<AuthenticatedLayout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">管理中心</h2>
</template>
<div class="w-[calc(100%_-_12rem)] p-4">
<!--原本的Inertia Link 改成一般的Button,並綁定Store中的modal action-->
<button class="inline-block py-2 px-3 border rounded-xl bg-blue-700 text-blue-100 my-4"
@click="modal('show')">
新增題庫
</button>
<div class="w-full px-4 py-2">題庫總數:{{ count }}</div>
<div v-if="errors.bank"
class="text-red-100 px-3 py-1.5 rounded-lg bg-red-500 my-2">
{{ errors.bank }}
</div>
<div v-for="bank in banks"
:key="bank.id"
class="w-full border rounded-xl flex p-4 bg-green-400 justify-between">
<Link :href="route('bank.subjects', bank.id)">
{{ bank.code }}.{{ bank.name }}{{ bank.levelC }}級
</Link>
<div>
<Link :href="route('bank.edit', bank.id)">編輯</Link>
/
<Link :href="route('bank.destroy', bank.id)"
method="delete"
as="button">刪除</Link>
</div>
</div>
</div>
</AuthenticatedLayout>
<!--把Modal放在頁面外部,並綁定Store的show 狀態-->
<vue-final-modal v-model="show"
classes="flex justify-center items-center">
<QuizBankForm />
</vue-final-modal>
</template>
因為新增題庫的功能不再需要向後端請求頁面,而是前端直接呼叫出來,所以原本的 bank.create
路由可以刪除
routes\web.php
//可以刪除
Route::get('/bank/create',[BankController::class,'create'])->name('bank.create');
routes\web.php
//可以刪除
Route::get('/bank/create',[BankController::class,'create'])->name('bank.create');
BankController
中的 create
也可以刪除
新增的功能是沒什麼問題,照原本的機制走就可以了,但是編輯會出問題,因為編輯是要帶入資料的,但是 Inertia
在 Modal
的支援上是有點問題的,作者表示他目前也沒有什麼好的解決方案,所以建議 Modal
的一些操作還是使用原本的 xhr
就可以了。
所以編輯的按鈕我們會由 Inertia
的 Link
改回 button
resources\js\Pages\Backstage\Banks.vue
<Link :href="route('bank.edit', bank.id)">編輯</Link>
改成
<button @click="modal('show',bank.id)">編輯</button>
原本 modalStore
中的 action modal()
只是用來改變 show
的值,現在我們加入一個 id
的變數,用來通知表單題庫的 id
值有變化,所以我們來改造一下Store的內容:
resources\js\Stores\ModalStore.js
import { defineStore } from "pinia";
export const modalStore=defineStore('modal',{
state:()=>({
show:false,
bankId:null, //增加一個bankId的狀態值
}),
actions:{
modal(action,id){
this.bankId=id??null //根據id的有無來改變bankId的值
switch(action){
case 'show':
this.show=true;
break;
case 'hide':
this.show=false;
break;
}
},
},
})
現在我們可以觸發 BankId
的改變,我們可以讓 BankForm
自己去向後端請求新的 bank
資料,而不是透過 Inertia
的頁面請求;
增加一個 api 路由
routes\api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BankController;
Route::get('/bank/edit/{id}',[BankController::class,'edit'])->name('bank.edit');
原本routes\web.php中同名的路由可以刪了
resources\js\QuizComponents\BankForm.vue
<script setup>
import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout.vue";
import { Head, Link, useForm } from "@inertiajs/inertia-vue3";
import { modalStore } from "@/Stores/ModalStore.js";
import { storeToRefs } from "pinia";
import { watch, ref } from "vue"; //引入vue的功能
import axios from "axios";
//原本的props用不到了所以刪除,另外建立組件內的變數
const header=ref("新增題庫")
const button=ref("新增")
const store = modalStore();
const { show,bankId } = storeToRefs(store); //引入Store中的bankId
const { modal } = store;
//建立表單初始內容
const form = useForm({ code: "", name: "", level: "A", });
//建立一個重設表單資料的函式,在編輯或新增結束時,重設表單內容
const resetForm=()=>{
form.code="";
form.name="";
form.level="A";
}
//監視 bankId 的變化
watch(bankId,(newId)=>{
if(newId){ //如果bankId不是null,那表示要進行的是編輯
//使用axios去向後台請求某bankId的資料
axios.get(route('bank.edit',newId))
.then((response)=>{
//根據回傳值資料更新對應的內容
Object.assign(form,response.data.bank)
header.value=response.data.header
button.value=response.data.button
})
}
})
const submit = () => {
if (typeof form.id !== "undefined") {
form.put(route("bank.update", form.id),{
onFinish:()=>{
modal('hide')
resetForm() //重設表單
}
});
} else {
form.post(route("bank.store"),{
onFinish:()=>{
modal('hide')
resetForm() //重設表單
}
});
}
};
</script>
.....略
然後原本 BankController
中的 edit
由回傳 Inertia
頁面改成只回傳資料即可
app\Http\Controllers\BankController.php
function edit($id)
{
return ['header'=>'編輯題庫',
'button'=>'修改',
'bank'=>$this->bank->find($id)];
}
改造完成後,原本的頁面 BankForm
被組件 BankForm
取代了,所以少了一個頁面,但多了一個組件。
改用 modal
的方式有時也不單單只是為了提升使用者體驗,在這個範例中,如果所有的功能都是要一個個的獨立頁面來呈現的話,會增加對頁面管理的複雜度。
而 modal
的使用減少了一些對後端的請求,由整個 inertia
頁面的資料傳遞變成只需要表單的部份資料即可,前端可以更專注在畫面的處理上,後端則是更專注在資料的處理,而改以組件的方式來操作表單,也可以強化組件的獨立性,不再依賴父層元件來傳值,組件自己可以做完的事就自行處理。
當然,沒有什麼方式是絕對的,要看專案的需求如何來決定。