iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Vue.js

打造銷售系統30天修練 - 全集中・Vue之呼吸系列 第 14

Day 14:[Componentの呼吸・柒之型] Form組合 - 基礎登入表單實作

  • 分享至 

  • xImage
  •  

在前幾天的修煉中,我們已經實作出了幾個我們需要的基礎元件(Component),每一個都蘊含著「單一職責」的概念。

今天,我們的目標就是將先前打造的元件們集結起來,實作出一個功能完整且具備現代設計感的登入頁面——LoginView.vue。我們將會看到,這些獨立的元件如何像拼圖一樣,一塊塊地組合起來,最終構成一個能與使用者互動的完整畫面。

準備好了嗎?讓我們開始組合屬於我們的第一個表單吧!

檔案結構

我們需要先建立 src/view/LoginView.vue。這是一個「視圖(View)」層級的元件,它的職責是整合多個基礎元件,構成一個完整的頁面。

/src
├── App.vue
├── main.js
├── assets/
├── components/      # 我們的基礎元件們
│   ├── BaseButton.vue
│   ├── BaseSelect.vue
│   ├── ErrorMessage.vue
│   ├── GoogleIcon.vue
│   └── LoadingSpinner.vue
├── router/
│   └── index.js
├── stores/
└── view/            # 頁面級元件
    ├── LoginView.vue  <-- 今日主角

LoginView.vue 元件分析與組合

LoginView.vue 是我們銷售系統的入口。它由一個背景、一個置中的登入表單構成。

讓我們一步步拆解它的結構。

1. <template> 的結構

首先,我們看到 <template> 中定義了整個畫面的骨架。

<template>
  <div class="login-container">
    <form @submit.prevent="handleLogin" class="login-form">
      <!-- Logo -->
      <div>
        <img class="login-logo" src="@/assets/Images/mrl_LOGO_340x40px_1.png" alt="居家先生" />
      </div>
      <h1 class="login-title">銷售系統</h1>

      <!-- 門市選擇 -->
      <div class="input-group">
        <BaseSelect label="門市選擇" v-model="storeSelect" :options="storeOptions" />
        <ErrorMessage v-if="storeError">{{ storeError }}</ErrorMessage>
      </div>

      <!-- 登入按鈕 -->
      <BaseButton type="button" variant="white" :class="['btn-login']" @click="googleLogin" :disabled="isLoading">
        <LoadingSpinner v-if="isLoading" size="medium" />
        <GoogleIcon v-else :size="28" />
        {{ isLoading ? '登入中...' : '使用 Google 登入' }}
      </BaseButton>

      <!-- 頁腳 -->
      <p class="login-footer">
        本系統僅供授權業務人員使用<br />
        如有問題請聯繫系統管理員
      </p>
    </form>
  </div>
</template>

這裡體現了元件化開發的核心思想:宣告式 UI。我們不是用程式碼去命令「畫一個選擇器、再畫一個按鈕」,而是像組合積木一樣,直接宣告「這裡需要一個 BaseSelect、那裡需要一個 BaseButton」。

  • BaseSelect:我們用它來做「門市選擇」。透過 v-model="storeSelect" 將元件的內部狀態與 LoginViewstoreSelect 變數雙向綁定。options 則由 :options="storeOptions" 傳入,定義了下拉選單的所有選項。

  • ErrorMessage:在 BaseSelect 之後,它透過 v-if="storeError" 進行條件渲染。只有當 storeError 有值(即發生錯誤)時,這個元件才會被渲染出來,向使用者顯示錯誤訊息。

  • BaseButton:它不僅是一個按鈕,更是一個動態容器

    • 內部透過 v-if="isLoading" 判斷式,來決定要顯示 LoadingSpinner 還是 GoogleIcon
    • 按鈕的文字也透過 {{ isLoading ? '登入中...' : '使用 Google 登入' }} 這個三元運算子實現了動態變化。
    • :disabled="isLoading" 屬性綁定,讓按鈕在載入中時自動變為不可點擊狀態,防止使用者重複觸發。

一個小小的按鈕,就整合了狀態判斷、條件渲染和屬性綁定,展現了 Vue 的響應式能力。

2.<script setup> 的邏輯

如果說 <template> 是骨架,那 <script setup> 就是驅動這副骨架活動的大腦和神經。

import { ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import BaseButton from '@/components/BaseButton.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import LoadingSpinner from '@/components/LoadingSpinner.vue';
import GoogleIcon from '@/components/GoogleIcon.vue';
import ErrorMessage from '@/components/ErrorMessage.vue';

// 1. 引入所有需要的元件和 Vue API

const router = useRouter();

// 2. 定義元件狀態 (State)
const storeOptions = [
  { value: '', text: '請選擇您的門市...', disabled: true },
  // ...其他門市
];
const storeSelect = ref('');
const storeError = ref('');
const isLoading = ref(false);

// 3. 定義互動邏輯 (Actions)
function validateStore() {
  // ...驗證邏輯
}

async function googleLogin() {
  if (!validateStore()) return;
  isLoading.value = true;
  // ...Google 登入的非同步處理
}

// 4. 狀態監控
watch(storeSelect, () => {
  if (storeError.value) {
    storeError.value = '';
  }
});

這裡的邏輯非常清晰:

  1. 匯入元件:首先,我們像召喚夥伴一樣,import 所有需要用到的基礎元件。
  2. 定義狀態:使用 ref 創建了幾個響應式變數,它們是整個元件的「狀態」。
    • storeSelect:儲存使用者選擇的門市。
    • storeError:儲存驗證失敗時的錯誤訊息。
    • isLoading:控制是否顯示載入動畫。
      這些狀態的任何改變,都會被 Vue 捕捉到,並自動更新到 <template> 中對應的畫面。
  3. 定義行為googleLogin 函式定義了點擊登入按鈕後的所有行為,包括驗證、設定載入狀態、呼叫 Google API 等。這是一個處理使用者互動的核心函式。
  4. watch 監聽:我們設定了一個 watch 來監控 storeSelect 的變化。一旦使用者開始選擇門市,之前顯示的錯誤訊息 storeError 就會被立刻清空。

3. <style scoped> 的外觀

最後,<style scoped> 賦予了這個登入頁面現代化的外觀。

.login-container {
  background: url('@/assets/Images/darlicat_family.jpg') center/cover no-repeat;
}

.login-container::before {
  /* ... */
  background-color: rgba(0, 0, 0, 0.2);
}
.login-form {
  background: rgba(255, 255, 255);
  backdrop-filter: blur(20px); 
  /* ... */
  animation: slideIn 0.8s ease-out; 
}

這裡有幾個亮點:

  • Scoped CSSscoped 屬性確保了這些樣式只會作用於 LoginView.vue 元件內部,不會「污染」到其他元件。
  • 動畫效果:透過 @keyframes 定義了 slideInfadeInUp 等動畫,讓表單和其中的元素以動態的方式進場,而不是生硬地出現。

完整畫面

LoginView
LoginView - isLoading

總結

今天,我們成功地將之前打造的各個基礎元件組合在一起,完成了一個功能齊全、外觀精美的登入頁面。我們學到了:

  1. 視圖(View)的角色:View 層級的元件是組合和調度基礎元件的舞台。
  2. 元件間的通訊:透過 props(如 :options)和 v-model 實現父子元件的數據流動。
  3. 狀態驅動畫面:利用 ref 創建響應式狀態,並透過這些狀態的變化來動態控制畫面的顯示內容(如 v-if 和三元運算子)。
  4. 提升使用者體驗:透過 watch:disabled 等細節處理,讓表單互動更流暢、更人性化。

這就是元件化開發的魅力所在。在 View 中,將每一個元件一塊一塊的組合成我們想要的樣子。

經過今天的修煉,我們已經掌握了「柒之型 - Form組合」。接下來,我們要處理 Google SSO 的登入功能!

明日,Day 15:[Authの呼吸・壹之型] SSO整合 - Google OAuth登入實作。心を燃やせ 🔥!


上一篇
Day 13:[Componentの呼吸・陸之型] 測試Card - 驗證卡片元件功能
下一篇
Day 15:[Authの呼吸・壹之型] SSO整合 - Google OAuth登入實作(上)
系列文
打造銷售系統30天修練 - 全集中・Vue之呼吸15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言