iT邦幫忙

2024 iThome 鐵人賽

DAY 28
1

哈囉,各位好!今天我們要來挑戰實用又有趣的頁面——製作一個互動式的聯絡表單。不管你是剛入門的新手,還是想更深入了解 Nuxt 3 的開發者,這個教學都能讓你學到很多實用的技巧。我們會用到 Nuxt 3、Pinia 和一些基本的表單處理技巧。準備好了嗎?讓我們開始吧!

1. 設定 API Store

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 方法,用來將使用者的回饋發送到伺服器。

2. 實作聯絡表單頁面

接下來,我們要在 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>

在這個檔案中,我們:

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

3. 定義資料結構

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

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. 實作表單邏輯

現在,讓我們深入了解表單的核心邏輯:

// 送出聯絡我們
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. 簡介區

<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. 表單區

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. 聯絡資訊區

<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 的各種功能,如數據綁定、條件渲染和事件處理,實現了豐富的交互功能。
    這個模板不僅在視覺上吸引人,還具有良好的功能性和用戶體驗,是一個優秀的聯絡表單設計範例。

今日實作頁面成果錄影

Yes

今日範例完整程式碼

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

(對了,如果你覺得今天的內容對你有幫助,別忘了給個讚支持一下喔!這會是我繼續努力的動力呢~)


上一篇
[ DAY27 ] Nuxt 3 專案實戰:打造互動式鮮食計算機
下一篇
[ DAY29 ] Nuxt 3 專案最終章:Vercel 部署全攻略
系列文
房東不給養鸚鵡 - 那就用 Nuxt + Express + MongoDB 30 天寫一個全端鸚鵡專案30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言