哈囉,各位好!今天我們要來挑戰實用又有趣的頁面——製作一個互動式的聯絡表單。不管你是剛入門的新手,還是想更深入了解 Nuxt 3 的開發者,這個教學都能讓你學到很多實用的技巧。我們會用到 Nuxt 3、Pinia 和一些基本的表單處理技巧。準備好了嗎?讓我們開始吧!
房東不給養鸚鵡 - 那就用 Nuxt + Express + MongoDB 30 天寫一個全端鸚鵡專案。
挑戰人生第一次鐵人賽,就來個佛心分享 side project,從專案發想、規劃、設計、資料庫、開發到部署,技術使用 Nuxt 3、Tailwind CSS、Pinia、Axios、Express、MongoDB,製作一個前後端分離的鸚鵡專案,功能主要介紹食物計算機和聯絡我們,希望可以讓更多人瞭解專案的完整流程 ✨
IT 全文章連結
1. 設定 API Store
在 stores
資料夾中修改 api.ts
檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
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
方法,用來將使用者的回饋發送到伺服器。
2. 實作聯絡表單頁面
接下來,我們要在 pages
資料夾中修改 connect.vue
檔案。這個檔案會包含我們的表單和相關邏輯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <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>
|
在這個檔案中,我們:
- 引入了必要的函式和 store。
- 設定了表單所需的響應式變數。
- 定義了一個來源選項的陣列。
3. 定義資料結構
在開始實作表單邏輯之前,我們需要先定義清晰的資料結構。這是程式設計中非常重要的一步,它能幫助我們更好地組織和管理數據。
1 2 3 4 5 6 7
| interface FeedbackData { contactPerson: string phone: string email: string feedback: string source?: string }
|
讓我們深入了解這個 interface:
contactPerson
、phone
、email
和 feedback
都被定義為 string
類型,這意味著它們必須是文字資料。
source
後面的 ?
表示這是一個可選欄位。使用者可以填寫,也可以不填寫。
使用 interface 的好處:
- 型別安全:TypeScript 會在編譯時檢查我們的程式碼,確保我們使用了正確的資料類型。
- 程式碼提示:當我們使用這個 interface 時,IDE 會提供相應的程式碼提示。
- 文件化:interface 本身就是一種文件,清楚地描述了我們期望的資料結構。
- 可重用性:我們可以在多個地方重複使用這個 interface。
4. 實作表單邏輯
現在,讓我們深入了解表單的核心邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| async function sendContact() { try { showLoading()
let data: FeedbackData = { contactPerson: contactPerson.value, phone: phone.value, email: email.value, feedback: feedback.value }
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() } }
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
函式的作用:
- 使用正則表達式來驗證 email 格式。
- 如果格式不正確,顯示警告訊息並清空輸入欄位。
- 這個函式通常綁定在 email 輸入框的
blur
事件上,當使用者離開輸入框時觸發。
5. 製作表單介面
這個 Vue 模板實現了一個功能完整的聯絡表單頁面。讓我們來看看它的三個主要區塊:
5-1. 簡介區
1 2 3 4 5 6 7 8 9 10
| <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>
|
說明:
- 這個區塊包含了兩個版本的簡介文字,分別針對手機和桌面版面。
- 使用 Tailwind 的響應式類別(
lg:hidden
和 hidden lg:block
)來控制不同螢幕尺寸下的顯示。
- 簡介內容鼓勵用戶提供回饋,並邀請他們參與專案。
5-2. 表單區
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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"
綁定了提交事件。
5-3. 聯絡資訊區
1 2 3 4 5 6 7 8 9 10 11
| <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>
|
說明:
- 這個區塊提供了其他聯繫方式,增加了用戶與網站互動的選擇。
- 包含兩個連結:一個指向個人資料頁面,另一個是郵件聯繫。
- 使用 Icon 組件來添加視覺效果。
- 應用了 hover 效果,提升了用戶體驗。
整體設計考量:
- 響應式設計:使用 Tailwind 的響應式類別,確保在不同設備上都有良好的顯示效果。
- 用戶體驗:提供清晰的指引和多種聯繫方式,方便用戶反饋。
- 視覺一致性:使用自定義的 CSS 類別(如
cus-border
、cus-intro
等)來保持整體風格的統一。
- 功能性:結合了 Vue 的各種功能,如數據綁定、條件渲染和事件處理,實現了豐富的交互功能。
這個模板不僅在視覺上吸引人,還具有良好的功能性和用戶體驗,是一個優秀的聯絡表單設計範例。
今日實作頁面成果錄影
點擊圖片可以打開 YouTube 影片歐 ~

今日範例完整程式碼
pages/connect.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
| <!-- 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| // 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 的世界中探險吧!加油!
希望這篇文章有幫助到你,謝謝你的觀看,若有想看的系列也歡迎告訴我 😉