Files
skt-vuetify-templates/src/services

元件 (Component) ↓ 呼叫 Store (Pinia) ← 管理狀態、快取 ↓ 呼叫 API Service ← 封裝業務邏輯 ↓ 呼叫 HTTP Client ← Axios 實例、攔截器

目前的資料流(以登入為例)

  1. views/Login.vuePlayground 頁面)只負責表單/驗證碼/導頁等 UI 行為
  2. stores/auth.ts 統一負責登入狀態(user/token/loading/error
  3. services/modules/user.ts 封裝 login/getProfile/... 端點
  4. services/client.ts 建立 axios instance
  5. services/interceptors.ts 統一注入 token 與處理 HTTP 錯誤

Menu API 與資料結構

選單系統採用 API 驅動設計:

API 端點

  • GET /service/api/menu:取得完整選單樹
  • GET /service/api/menu/favorite:取得使用者收藏選單

資料結構

interface MenuNode {
  mdl_id: string // 模組 ID
  mdl_name: string // 模組名稱
  unt_id?: string // 單位 ID
  unt_name?: string // 單位名稱
  fnc_id?: string // 功能 ID
  fnc_name?: string // 功能名稱
  children?: MenuNode[]
}

階層關係

  • 第一層:模組(mdl
  • 第二層:單位(unt
  • 第三層:功能(fnc),作為葉節點使用 fnc_id 作為路由路徑

Store 持久化

stores/menu.ts 提供:

  • 自動 localStorage 持久化選單與收藏
  • 初始化時自動還原資料
  • 登出時清除快取

API 前綴:/api

目前 Playground 已將 api 資料夾更名為 services,避免與 API 前綴 /api 衝突。

在開發模式下:

  • 前端呼叫一律使用 /service/api/*
  • Vite dev server 透過 proxy 將 /service/* 轉送到後端(目前指向 http://192.168.89.54:9002

HTTP Client 設定

  • baseURL:優先使用 VITE_API_BASE_URL,否則預設 /service/api(搭配 Vite proxy
  • Content-Type:預設 application/json

Token Service(單一來源)

為避免「Pinia token」與「localStorage token」不同步的問題,這裡採用單一來源:

  • services/token.ts 使用 ref 保存 token,並同步 localStorage
  • Store 與 Interceptor 都只透過 tokenService 讀寫 token
  • 401 時清除 token,可立即同步到 UI

Token 注入策略(Interceptor

Interceptor 會從 tokenService 讀取 token 並注入 Authorization: Bearer <token>

這樣做的原因是避免循環依賴:

store(auth) -> services(userApi) -> httpClient -> interceptors -> store(auth)

Store 仍然是「唯一負責更新 token 的地方」,Interceptor 只負責「讀取 token 並附加到 request」。

錯誤正規化(normalizeError

為了讓 UI 不需要理解 AxiosError,這裡將錯誤統一成 ApiRequestError

  • services/error.ts 提供 normalizeError()ApiRequestError
  • Interceptor 在 response error 時呼叫 normalizeError()
  • Store 只需要處理 error.message / error.code / error.status

最低限度的映射規則:

  • response.data.message 優先使用
  • 其次使用 AxiosError.message
  • 都沒有則顯示 請求失敗

請求取消(AbortController

取消策略採「同類型請求互斥」,目前示範在 login

  • Store 建立 AbortController,每次登入前先取消前一次
  • Service 只接收 signal,不管理 controller 狀態
  • normalizeError 會將取消行為轉為 CanceledRequestError
  • UI 不顯示取消造成的錯誤訊息
DECISION WHY WHY NOT
Store 呼叫 API,不是元件直接呼叫 狀態集中管理、可快取、易測試 元件直接呼叫會導致狀態散落
API 模組化(userApi、orderApi 關注點分離、好維護 全塞一個檔案會變超大
Interceptor 獨立檔案 單一職責、好測試 寫在 client.ts 會雜亂
泛型 ApiResponse 型別安全、IDE 提示 用 any 會失去 TS 優勢
API 前綴使用 /api 使用習慣一致、搭配 Vite proxy 容易理解 若前端資料夾也叫 api 容易造成路徑衝突,因此此專案改名為 services