refactor: update icon usage to use mdi imports for consistency
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
元件 (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 <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<T>| 型別安全、IDE 提示 | 用 any 會失去 TS 優勢 |
|
||||
| API 前綴使用 `/api` | 使用習慣一致、搭配 Vite proxy 容易理解 | 若前端資料夾也叫 `api` 容易造成路徑衝突,因此此專案改名為 `services` |
|
||||
Reference in New Issue
Block a user