哈囉,各位好!今天我們要來挑戰實用又有趣的頁面——製作一個互動式的聯絡表單。不管你是剛入門的新手,還是想更深入了解 Nuxt 3 的開發者,這個教學都能讓你學到很多實用的技巧。我們會用到 Nuxt 3、Pinia 和一些基本的表單處理技巧。準備好了嗎?讓我們開始吧!
在 stores
資料夾中修改 api.ts
檔案:
// stores/api.ts
import { defineStore } from 'pinia'
import axios from 'axios'
const baseURL = '<https://two024it-test-app.onrender.com>'
const api = axios.create({
baseURL
})
export const useApiStore = defineStore('api-store', {
actions: {
// 新增回饋
async createFeedback(data: any) {
const response = await api.post('/feedbacks', data)
return response
}
}
})
新增 createFeedback
方法,用來將使用者的回饋發送到伺服器。
接下來,我們要在 pages
資料夾中修改 connect.vue
檔案。這個檔案會包含我們的表單和相關邏輯。
<!-- pages/connect.vue -->
<script setup lang="ts">
import { useApiStore } from '~/stores/api'
import { showLoading, hideLoading } from '~/stores/eventBus'
const apiStore = useApiStore()
const contactPerson = ref('')
const phone = ref('')
const email = ref('')
const feedback = ref('')
const source = ref('')
const sourceOption = ['網路搜尋', '社群媒體', '親友介紹', '其他']
// ... 其他程式碼會在後續步驟中添加
</script>
<template>
<!-- 模板內容會在後續步驟中添加 -->
</template>
在這個檔案中,我們:
在開始實作表單邏輯之前,我們需要先定義清晰的資料結構。這是程式設計中非常重要的一步,它能幫助我們更好地組織和管理數據。
interface FeedbackData {
contactPerson: string
phone: string
email: string
feedback: string
source?: string
}
讓我們深入了解這個 interface:
contactPerson
、phone
、email
和 feedback
都被定義為 string
類型,這意味著它們必須是文字資料。source
後面的 ?
表示這是一個可選欄位。使用者可以填寫,也可以不填寫。使用 interface 的好處:
現在,讓我們深入了解表單的核心邏輯:
// 送出聯絡我們
async function sendContact() {
try {
showLoading()
let data: FeedbackData = {
contactPerson: contactPerson.value,
phone: phone.value,
email: email.value,
feedback: feedback.value
}
// source 有值才加入
if (source.value) {
data = { ...data, source: source.value }
}
const res = await apiStore.createFeedback(data as any)
const result = res.data
if (result && result.status === 'success') {
alert('感謝您的回饋,我們會盡快處理!')
// 清空表單
contactPerson.value = ''
phone.value = ''
email.value = ''
feedback.value = ''
source.value = ''
} else {
alert('發生錯誤,請稍後再試!')
}
} catch (error) {
alert('發生錯誤,請稍後再試!')
} finally {
hideLoading()
}
}
// 檢查 email 格式 (blur 事件觸發)
function checkEmail() {
if (email.value) {
const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
if (!emailReg.test(email.value)) {
alert('請輸入正確的 email 格式!')
email.value = ''
}
}
}
sendContact
函式的詳細說明:
try-catch
結構來處理可能發生的錯誤。showLoading()
顯示載入畫面,提升用戶體驗。data
物件,使用我們之前定義的 FeedbackData
interface。source
欄位。apiStore.createFeedback()
發送資料到後端。hideLoading()
來隱藏載入畫面。checkEmail
函式的作用:
blur
事件上,當使用者離開輸入框時觸發。這個 Vue 模板實現了一個功能完整的聯絡表單頁面。讓我們來看看它的三個主要區塊:
<div class="cus-intro lg:hidden">
使用上遇到困難?<br />希望有更好用的功能?<br />覺得網站很實用?<br />
把想法都告訴我們吧,<br />我們可以把你的想法化為現實。<br />
非常歡迎擁有專業知識的夥伴加入我們的 side project ✨
</div>
<div class="cus-intro hidden lg:block">
使用上遇到困難?希望有更好用的功能?覺得網站很實用?<br />
把想法都告訴我們吧,我們可以把你的想法化為現實。<br />
非常歡迎擁有專業知識的夥伴加入我們的 side project ✨
</div>
說明:
lg:hidden
和 hidden lg:block
)來控制不同螢幕尺寸下的顯示。html
Copy
<div class="cus-block-padding">
<h2 class="cus-page-title">填寫表單幫助我們變得更好</h2>
<div class="cus-col-3">
<!-- 名稱、電話、信箱、內容輸入框 --><!-- 來源選擇(單選按鈕) -->
</div>
<button
class="cus-btn-primary mt-5"
:disabled="!contactPerson || !phone || !email || !feedback"
@click="sendContact"
>
送出表單
</button>
</div>
說明:
v-model
指令實現數據的雙向綁定。:disabled
綁定來控制是否可點擊,確保必填欄位都有值。@click="sendContact"
綁定了提交事件。<div class="cus-block-padding">
<h2 class="cus-page-title">或是你也可以用其他方式聯繫我們</h2>
<a href="https://profile.2fishs.com/" target="_blank" class="mb-2 flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3">
<Icon name="ph:link" size="20" />
<p>profile_web</p>
</a>
<a href="mailto:yu13142013@gmail.com" target="_blank" class="flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3">
<Icon name="ph:envelope-simple-light" size="20" />
<p>yu13142013@gmail.com</p>
</a>
</div>
說明:
cus-border
、cus-intro
等)來保持整體風格的統一。
pages/connect.vue
<!-- pages / connect.vue --> <script setup lang="ts"> import { useApiStore } from '~/stores/api' const apiStore = useApiStore() import { showLoading, hideLoading } from '~/stores/eventBus' const contactPerson = ref('') const phone = ref('') const email = ref('') const feedback = ref('') const source = ref('') // "網路搜尋", "社群媒體", "親友介紹", "其他" const sourceOption = ['網路搜尋', '社群媒體', '親友介紹', '其他'] interface feedbackData { contactPerson: string phone: string email: string feedback: string source?: string // 使 source 屬性成為可選的 } // 送出聯絡我們 async function sendContact() { try { showLoading() let data: feedbackData = { contactPerson: contactPerson.value, phone: phone.value, email: email.value, feedback: feedback.value } // source 有值才加入 if (source.value) { data = { ...data, source: source.value } } const res = await apiStore.createFeedback(data as any) // console.log(res) const result = res.data if (result && result.status === 'success') { alert('感謝您的回饋,我們會盡快處理!') contactPerson.value = '' phone.value = '' email.value = '' feedback.value = '' source.value = '' } else { alert('發生錯誤,請稍後再試!') } } catch (error) { alert('發生錯誤,請稍後再試!') } finally { hideLoading() } } // 檢查 email 格式 (blur 事件觸發) function checkEmail() { if (email.value) { const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ if (!emailReg.test(email.value)) { alert('請輸入正確的 email 格式!') email.value = '' } } } </script> <template> <div class="w-full"> <!-- * content --> <div class="cus-border"> <!-- * introduction --> <div class="cus-intro lg:hidden"> 使用上遇到困難?<br />希望有更好用的功能?<br />覺得網站很實用?<br /> 把想法都告訴我們吧,<br />我們可以把你的想法化為現實。<br /> 非常歡迎擁有專業知識的夥伴加入我們的 side project ✨ </div> <div class="cus-intro hidden lg:block"> 使用上遇到困難?希望有更好用的功能?覺得網站很實用?<br /> 把想法都告訴我們吧,我們可以把你的想法化為現實。<br /> 非常歡迎擁有專業知識的夥伴加入我們的 side project ✨ </div> <hr class="cus-line-row" /> <!-- * feedback info --> <div class="cus-block-padding"> <h2 class="cus-page-title">填寫表單幫助我們變得更好</h2> <div class="cus-col-3"> <div class="cus-col-1"> <label for="contactPerson" class="cus-label" >名稱 <span class="text-red2">*</span></label > <input type="text" class="cus-input" id="contactPerson" v-model="contactPerson" placeholder="請輸入名稱" /> </div> <div class="cus-col-1"> <label for="phone" class="cus-label">電話 <span class="text-red2">*</span></label> <input type="tel" class="cus-input" id="phone" v-model="phone" placeholder="請輸入電話" /> </div> <div class="cus-col-1"> <label for="email" class="cus-label">信箱 <span class="text-red2">*</span></label> <input type="email" class="cus-input" id="email" v-model="email" placeholder="請輸入信箱" @blur="checkEmail" /> </div> <div class="cus-col-1"> <label for="feedback" class="cus-label">內容 <span class="text-red2">*</span></label> <input type="text" class="cus-input" id="feedback" v-model="feedback" placeholder="請輸入內容" /> </div> <div class="cus-col-1"> <label for="source" class="cus-label">從哪裡得知此網站</label> <div class="cus-radio-row"> <label class="cus-label-radio" for="網路搜尋"> <input type="radio" name="source" class="" id="網路搜尋" v-model="source" value="網路搜尋" /> <span></span> 網路搜尋 </label> <label class="cus-label-radio" for="社群媒體"> <input type="radio" name="source" class="" id="社群媒體" v-model="source" value="社群媒體" /> <span></span> 社群媒體 </label> <label for="親友介紹" class="cus-label-radio"> <input type="radio" name="source" class="" id="親友介紹" v-model="source" value="親友介紹" /> <span></span>親友介紹 </label> <label for="其他" class="cus-label-radio"> <input type="radio" name="source" class="" id="其他" v-model="source" value="其他" /> <span></span>其他 </label> </div> </div> </div> <button class="cus-btn-primary mt-5" :disabled="!contactPerson || !phone || !email || !feedback" @click="sendContact" > 送出表單 </button> </div> <hr class="cus-line-row" /> <!-- * contact --> <div class="cus-block-padding"> <h2 class="cus-page-title">或是你也可以用其他方式聯繫我們</h2> <a href="https://profile.2fishs.com/" target="_blank" class="mb-2 flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3" > <Icon name="ph:link" size="20" /> <p>profile_web</p> </a> <a href="mailto:yu13142013@gmail.com" target="_blank" class="flex transform items-end gap-2 text-blue4 duration-300 hover:text-blue3" > <Icon name="ph:envelope-simple-light" size="20" /> <p>yu13142013@gmail.com</p> </a> </div> </div> </div> </template> <style scoped></style>
stores/api.ts
// stores/api.ts import { defineStore } from 'pinia' import axios from 'axios' const baseURL = 'https://two024it-test-app.onrender.com' const api = axios.create({ baseURL }) export const useApiStore = defineStore('api-store', { actions: { // 取得食物列表 async fetchFoodList() { const response = await api.get('/freshfoods/') return response }, // 新增鮮食計算 async calculateFood(data: any) { const response = await api.post('/foods/calculatefood', data) return response }, // 新增回饋 async createFeedback(data: any) { const response = await api.post('/feedbacks', data) return response } } })
我們已經深入探討了如何使用 Nuxt 3 和 Vue 3 來創建一個功能完整的聯絡表單。從資料結構的定義,到表單邏輯的實現,再到用戶界面的設計,每一步都是前端開發中重要的環節。這個過程不僅讓我們學習了技術細節,更讓我們理解了如何從使用者的角度來思考和設計。
雖然由於時間限制,我們只能實作兩個頁面,但這已經足以讓我們掌握 Nuxt 3 專案的基本架構和開發流程。記住,實踐是學習的最好方式。即使只有兩個頁面,也要盡可能地將所學付諸實踐,這樣才能真正理解和掌握這些概念。
明天,我們將邁出最後一步 —— 將專案部署到雲端!這將是一個將我們的作品展示給全世界的機會。在此之前,別忘了每天都要將你的更新推送到 GitHub。這不僅是一個好習慣,也是確保你的程式碼安全的重要步驟。
最後,記住程式開發是一個持續學習和改進的過程。今天的每一小步,都是邁向成為優秀開發者的重要一步。保持好奇心,勇於嘗試,相信自己的能力。我們在雲端見!
大家有沒有想要更詳細補充的部分呢?歡迎在下方留言分享喔!讓我們一起在 Nuxt3 的世界中探險吧!加油!
(對了,如果你覺得今天的內容對你有幫助,別忘了給個讚支持一下喔!這會是我繼續努力的動力呢~)