iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
Vue.js

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

Day 3:[Vueの呼吸・貳之型] 資料流設計 - 初探銷售系統的狀態規劃

  • 分享至 

  • xImage
  •  

什麼是「狀態 (State)」?

在開始實作之前,我們需要先釐清一個核心問題:

「當使用者在我們的系統中操作時,有哪些資訊是必須被記錄下來的?」

簡單來說,這些需要被記錄、會隨著時間或操作而改變的資訊,就是我們應用程式的 「狀態 (State)」

另外,狀態又可以再區分成兩大類:

  • 前端狀態 (Client-side State):存在於使用者當下操作的環境中,例如表單輸入值、UI 畫面的選擇。
  • 伺服器狀態 (Server-side State):由伺服器或資料庫維護,前端需要透過 API 取得或更新,例如商品清單、會員資訊。

假設你的登入畫面有一個「選擇門市」的下拉選單:

<select>
  <option value="">請選擇門市</option>
  <option value="store001">台北信義店</option>
  <option value="store002">台中逢甲店</option>
  <option value="store003">高雄夢時代店</option>
</select>

在這個情境中:

屬於「前端狀態」的部分為使用者目前選中的門市 (selectedStore: "store001")

→ 系統需要記住使用者的選擇,登入後才能顯示對應資料。

屬於「伺服器狀態」的部分為門市列表本身 (台北信義店、台中逢甲店...)

→ 這是後端提供的參考資料,通常從 API 取得,並不會因為單一使用者操作而改變。

參考:前端開發初學者必讀:了解各種「狀態」的分類與來源

👉 本篇文章將聚焦在 前端狀態 (Client-side State) 的管理。


為什麼要記錄狀態?

如果不記錄,這些資訊就會在頁面切換或元件銷毀時消失,導致系統「忘記」使用者的操作,進而破壞使用體驗。


狀態的生命週期

每個狀態都有它的「生命週期」:

  • 有些狀態只在某個元件顯示時需要,元件關掉就不重要了 → 區域狀態 (Local State)
  • 有些狀態則必須跨頁面、跨元件一直保存,直到整個流程結束 → 全域狀態 (Global State)

理解「狀態的生命週期」,能幫助我們決定該把狀態放在元件內部,還是全域。


盤點我們系統中的「狀態」

在我們目前的 Prototype 中,這些「狀態」非常零散。
有些存在 HTML 的 value 裡,有些是全域的 JS 變數,導致資料流向混亂,難以追蹤和管理。

因此,第一步就是把所有狀態都明確地「盤點」出來。


那麼,該怎麼知道哪些資料要被「盤點」成狀態?

我們可以這樣思考:

  1. 是否需要被記住?

    • 資料若在操作間需要被保留,就應視為狀態。(例如:搜尋條件、登入狀態)
  2. 是否會隨操作改變?

    • 靜態的資料(例如產品目錄中「A產品」的名稱)通常不需要放進狀態,可以直接從 API 拿。
    • 動態的資料(例如「目前選中的客戶」)就需要被追蹤。
  3. 誰會使用它?

    • 如果只有一個元件需要它 → 區域狀態。
    • 如果多個元件或頁面都會用到 → 全域狀態。

基於這個思維,我們檢視 Prototype,大概整理出三大類必須管理的狀態:

  1. 使用者與身份驗證 (Auth)

    • isLoggedIn: 使用者是否已登入 (Boolean)
    • currentUser: 當前登入的使用者資訊 (包含姓名、角色等)
    • selectedStore: 在登入頁選擇所屬的門市
  2. 訂單管理 (Order Management)

    • orderFilters: 搜尋表單中的所有篩選條件
    • orderList: 從後端 API 獲取到的訂單列表
    • isLoadingOrders: 是否正在讀取訂單
  3. 報價單流程 (Quotation Flow)

    • currentQuotation: 正在建立中的報價單資訊 (包含客戶資料、商品列表等)

規劃狀態的存放位置:Local vs. Global

盤點完狀態後,最重要的決定來了:
這些狀態應該放在哪裡?

在 Vue 中,我們主要有兩種選擇:

1. 區域狀態 (Local State)

  • 定義:只在單一元件內部使用,與其他元件無關的狀態。
  • 管理方式:直接寫在該元件的 <script setup> 中,使用 ref()reactive()

📌 範例:訂單列表的檢視方式

在「訂單管理」頁面中,使用者可以選擇用「卡片」或「表格」兩種不同的方式來瀏覽訂單列表。

卡片視圖
卡片視圖

表格視圖
表格視圖

這個功能需要一個狀態來記錄「使用者當前選擇的是哪種視圖」。
我們可以稱這個狀態為 activeView,它的值可能是 cardtable

  • 誰關心它?
    只有「訂單列表」(OrderResultsPanel.vue) 需要知道 activeView 的值,
    因為它要根據這個值來決定顯示 <OrderCardGrid>(卡片) 還是 <OrderTable>(表格)

  • 誰不關心它?

    • 左邊的「搜尋面板」(OrderSearchPanel.vue) 完全不關心結果的呈現方式。
    • 最外層的 OrderManagement.vue 頁面本身也不關心,它只負責傳遞訂單資料給訂單列表。
    • 其他頁面(如 Dashboard、報價單)更是與此無關。

👉 結論:activeView 的影響範圍,完全沒有超出「訂單列表」這個元件,因此屬於 區域狀態


2. 全域狀態 (Global State)

  • 定義:需要被多個元件共享、或在跨頁面時需要持續存在的狀態。
  • 管理方式:使用全域狀態管理工具。Vue 3 官方推薦的選擇是 Pinia

📌 範例:建立報價單流程

從我們的檔案:

  • quotation-create-step1.html → 步驟一:選擇客戶
  • quotation-create-step2.html → 步驟二:加入商品
  • quotation-create-step3.html → 步驟三:預覽與確認

可以看出這是一個分步驟的操作。
問題來了:

當使用者在步驟一選擇了客戶,進入步驟二時,應用程式要如何「記得」剛剛選的是哪個客戶?


如果我們把「選擇的客戶」當作 步驟一的區域狀態

  • 當切換到步驟二,步驟一元件被銷毀
  • 客戶資料就會 消失

👉 這顯然不行。

我們需要一個獨立於任何單一元件之外的、公共的、全域的地方來存放「正在建立中的報價單」資料。
這個地方,就是 全域狀態儲存庫 (Global State Store),也就是我們將用 Pinia 建立的 quotationStore


📌 流程示意

  • 步驟一
    使用者選擇了客戶 A
    → 呼叫 quotationStore.setCustomer(客戶A)
    → 存到 全域 Store

  • 步驟二
    元件建立時
    → 從 quotationStore 取出客戶 A 的資料顯示
    → 新增商品後呼叫 quotationStore.addProduct(商品B)

  • 步驟三
    quotationStore 取出客戶 A + 商品 B 的資料
    → 顯示成完整的預覽報價單

👉 quotationStore 就像一個 公共購物車,跨步驟共享資料,不會因為頁面切換而消失。


為什麼需要 Pinia?

當我們把狀態分成 區域狀態 (Local State)全域狀態 (Global State) 之後,下一個問題就來了:

全域狀態要存在哪裡?又該怎麼存?


在 Vue 裡:

  • 區域狀態 → 用 ref()reactive(),存放在元件內部即可。
  • 全域狀態 → 如果散落在各個元件,資料會變得難以共享與維護。

👉 所以我們需要一個 集中存放全域狀態的工具,這就是 狀態管理 (State Management)


Pinia 的特點

  • 輕量化:API 比 Vuex 更精簡,不再需要繁瑣的 mutations
  • TypeScript 友好:天然支援型別推斷,寫起來更安全
  • 官方推薦:Vue 團隊已將 Pinia 視為長期維護的主流方案
  • Composition API 整合:和 Vue 3 的 ref()reactive() 完美搭配

簡單來說:

如果把「元件的區域狀態」想像成小抽屜,那麼 Pinia Store 就是一個全域的倉庫,讓不同頁面或元件都能隨時存取同一份狀態。


設計我們的 Pinia Stores

基於前面盤點的分類,我們可以大概規劃出以下三個 Pinia Store 來管理全域狀態:

1. userStore.js

  • 職責:管理使用者身份驗證相關狀態
  • 包含
    • isLoggedIn : 是否已登入
    • currentUser : 使用者資訊
    • selectedStore : 當前選擇的門市
  • 操作 (Actions)
    • login(userInfo) : 登入並更新狀態
    • logout() : 登出並清空狀態

2. orderStore.js

  • 職責:管理訂單相關資料
  • 包含
    • orderFilters : 當前的查詢條件
    • orderList : 訂單清單
    • isLoadingOrders : 是否正在載入
  • 操作 (Actions)
    • fetchOrders() : 從 API 抓取訂單
    • setFilters() : 設定查詢條件

3. quotationStore.js

  • 職責:管理跨步驟的報價單建立流程
  • 包含
    • currentQuotation : 當前編輯中的報價單
  • 操作 (Actions)
    • setCustomer(customer) : 設定客戶資訊
    • addProduct() : 新增商品
    • removeProduct() : 移除商品

總結

今天,我們完成了從「產品化」角度最重要的思維轉變:
我們不再將資料視為散落在各處的變數,而是將它們系統性地規劃為 區域狀態全域狀態

明日,我們將繼續深入 Vueの呼吸・參之型,探討在 Vue 中的路由架構設計 —— 心を燃やせ!!


上一篇
Day 2:[Vueの呼吸・壹之型] 架構設計 - 從Prototype到Component架構
下一篇
Day 4:[Vueの呼吸・參之型] 初探路由架構 - 多角色的Navigation設計
系列文
打造銷售系統30天修練 - 全集中・Vue之呼吸5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言