元件 (Component) ↓ 呼叫 Store (Pinia) ← 管理狀態、快取 ↓ 呼叫 API Service ← 封裝業務邏輯 ↓ 呼叫 HTTP Client ← Axios 實例、攔截器 ## 目前的資料流(以登入為例) 1. `views/Login.vue`(Playground 頁面)只負責表單/驗證碼/導頁等 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`:取得使用者收藏選單 ### 資料結構 ```ts 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 `。 這樣做的原因是避免循環依賴: `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` |