2fish 的部落格

歡迎來到我的部落格!這裡是我記錄開發點滴的地方,希望你會喜歡😌

0%

[ 2024 IT 鐵人賽 DAY28 ] Nuxt 3 專案實戰:打造互動式聯絡表單

哈囉,各位好!今天我們要來挑戰實用又有趣的頁面——製作一個互動式的聯絡表單。不管你是剛入門的新手,還是想更深入了解 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
// 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 方法,用來將使用者的回饋發送到伺服器。

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
<!-- 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>

在這個檔案中,我們:

  1. 引入了必要的函式和 store。
  2. 設定了表單所需的響應式變數。
  3. 定義了一個來源選項的陣列。

3. 定義資料結構

在開始實作表單邏輯之前,我們需要先定義清晰的資料結構。這是程式設計中非常重要的一步,它能幫助我們更好地組織和管理數據。

1
2
3
4
5
6
7
interface FeedbackData {
contactPerson: string
phone: string
email: string
feedback: string
source?: string
}

讓我們深入了解這個 interface:

  • contactPersonphoneemailfeedback 都被定義為 string 類型,這意味著它們必須是文字資料。
  • source 後面的 ? 表示這是一個可選欄位。使用者可以填寫,也可以不填寫。

使用 interface 的好處:

  1. 型別安全:TypeScript 會在編譯時檢查我們的程式碼,確保我們使用了正確的資料類型。
  2. 程式碼提示:當我們使用這個 interface 時,IDE 會提供相應的程式碼提示。
  3. 文件化:interface 本身就是一種文件,清楚地描述了我們期望的資料結構。
  4. 可重用性:我們可以在多個地方重複使用這個 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
}

// 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 函式的詳細說明:

  1. 使用 try-catch 結構來處理可能發生的錯誤。
  2. showLoading() 顯示載入畫面,提升用戶體驗。
  3. 創建 data 物件,使用我們之前定義的 FeedbackData interface。
  4. 使用條件判斷來處理可選的 source 欄位。
  5. 使用 apiStore.createFeedback() 發送資料到後端。
  6. 根據回應來顯示成功或失敗的訊息。
  7. 如果成功,清空所有表單欄位。
  8. 最後,不管成功與否,都會呼叫 hideLoading() 來隱藏載入畫面。

checkEmail 函式的作用:

  1. 使用正則表達式來驗證 email 格式。
  2. 如果格式不正確,顯示警告訊息並清空輸入欄位。
  3. 這個函式通常綁定在 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:hiddenhidden 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 效果,提升了用戶體驗。

整體設計考量:

  1. 響應式設計:使用 Tailwind 的響應式類別,確保在不同設備上都有良好的顯示效果。
  2. 用戶體驗:提供清晰的指引和多種聯繫方式,方便用戶反饋。
  3. 視覺一致性:使用自定義的 CSS 類別(如 cus-bordercus-intro 等)來保持整體風格的統一。
  4. 功能性:結合了 Vue 的各種功能,如數據綁定、條件渲染和事件處理,實現了豐富的交互功能。
    這個模板不僅在視覺上吸引人,還具有良好的功能性和用戶體驗,是一個優秀的聯絡表單設計範例。

今日實作頁面成果錄影

點擊圖片可以打開 YouTube 影片歐 ~
Yes

今日範例完整程式碼

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 的世界中探險吧!加油!




希望這篇文章有幫助到你,謝謝你的觀看,若有想看的系列也歡迎告訴我 😉