feat(stores): add Pinia domain stores and update docs
Implement concrete Pinia stores for app UI and domain data instead of placeholder re-exports, including seeded student records and snackbar state. Refresh README guidance for components, plugins, and services to document the current project structure, data flow, and usage conventions.feat(stores): add Pinia domain stores and update docs Implement concrete Pinia stores for app UI and domain data instead of placeholder re-exports, including seeded student records and snackbar state. Refresh README guidance for components, plugins, and services to document the current project structure, data flow, and usage conventions.
This commit is contained in:
+41
-92
@@ -1,115 +1,64 @@
|
||||
元件 (Component)
|
||||
↓ 呼叫
|
||||
Store (Pinia) ← 管理狀態、快取
|
||||
↓ 呼叫
|
||||
API Service ← 封裝業務邏輯
|
||||
↓ 呼叫
|
||||
HTTP Client ← Axios 實例、攔截器
|
||||
# Services
|
||||
|
||||
## 目前的資料流(以登入為例)
|
||||
`src/services` 是資料存取與 HTTP 邊界,負責封裝 axios client、interceptor、token/session、錯誤處理與 API 模組。
|
||||
|
||||
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[]
|
||||
}
|
||||
```txt
|
||||
component/view -> store/composable -> service module -> httpClient -> interceptor
|
||||
```
|
||||
|
||||
### 階層關係
|
||||
原則:
|
||||
|
||||
- **第一層**:模組(mdl)
|
||||
- **第二層**:單位(unt)
|
||||
- **第三層**:功能(fnc),作為葉節點使用 `fnc_id` 作為路由路徑
|
||||
- component 不直接處理底層 HTTP client、token、interceptor 或錯誤正規化。
|
||||
- store 或 composable 負責協調 UI 狀態與呼叫 service。
|
||||
- service 回傳資料,不持有 UI 狀態。
|
||||
- service 不 import component、view 或 store。
|
||||
|
||||
### Store 持久化
|
||||
## 目前檔案
|
||||
|
||||
`stores/menu.ts` 提供:
|
||||
- `client.ts`:建立單一 axios instance,設定 `baseURL`、timeout、credentials 與 interceptor。
|
||||
- `interceptors.ts`:集中處理 request token 注入與 response 錯誤。
|
||||
- `error.ts`:提供 `normalizeError()` 與統一錯誤型別。
|
||||
- `http-error.ts`:提供全域 HTTP 錯誤事件。
|
||||
- `http-toast.ts`:提供 HTTP 錯誤提示相關流程。
|
||||
- `token.ts`:提供 token 單一來源,並同步 localStorage。
|
||||
- `session.ts`:提供 session 相關流程。
|
||||
- `modules/auth.ts`:封裝登入與驗證碼 API。
|
||||
- `modules/menu.ts`:封裝選單與收藏選單 API。
|
||||
|
||||
- 自動 localStorage 持久化選單與收藏
|
||||
- 初始化時自動還原資料
|
||||
- 登出時清除快取
|
||||
## API 模組規則
|
||||
|
||||
## API 前綴:`/api`
|
||||
新增 API 時,優先放在 `src/services/modules/<domain>.ts`。
|
||||
|
||||
目前 Playground 已將 `api` 資料夾更名為 `services`,避免與 API 前綴 `/api` 衝突。
|
||||
API module 應:
|
||||
|
||||
在開發模式下:
|
||||
|
||||
- 前端呼叫一律使用 `/service/api/*`
|
||||
- Vite dev server 透過 proxy 將 `/service/*` 轉送到後端(目前指向 `http://192.168.89.54:9002`)
|
||||
- 使用 `httpClient` 發 request。
|
||||
- 匯出清楚命名的 API 物件,例如 `authApi`、`menuApi`。
|
||||
- 定義與該 module 相關的 request/response 型別。
|
||||
- 接收 `AbortSignal` 等 request option,但不管理頁面 loading 或 controller 狀態。
|
||||
|
||||
## HTTP Client 設定
|
||||
|
||||
- `baseURL`:優先使用 `VITE_API_BASE_URL`,否則預設 `/service/api`(搭配 Vite proxy)
|
||||
- `Content-Type`:預設 `application/json`
|
||||
`client.ts` 的 `baseURL` 優先使用 `VITE_API_BASE_URL`,否則使用 `/service/api`。開發模式下,Vite proxy 會將 `/service/*` 轉送到後端。
|
||||
|
||||
## Token Service(單一來源)
|
||||
目前 API 呼叫範例:
|
||||
|
||||
為避免「Pinia token」與「localStorage token」不同步的問題,這裡採用單一來源:
|
||||
- `authApi.getCaptcha()` -> `/Auth/get-captcha`
|
||||
- `authApi.login()` -> `/Auth/login`
|
||||
- `menuApi.getMenu()` -> `/Menu/GetMenu`
|
||||
- `menuApi.getFavorite()` -> `/Menu/GetFavorite`
|
||||
|
||||
- `services/token.ts` 使用 `ref` 保存 token,並同步 localStorage
|
||||
- Store 與 Interceptor 都只透過 `tokenService` 讀寫 token
|
||||
- 401 時清除 token,可立即同步到 UI
|
||||
## Token 與錯誤處理
|
||||
|
||||
## Token 注入策略(Interceptor)
|
||||
token 由 `tokenService` 作為單一來源:
|
||||
|
||||
Interceptor 會從 `tokenService` 讀取 `token` 並注入 `Authorization: Bearer <token>`。
|
||||
- store 負責登入成功後寫入 token,以及登出時清除 token。
|
||||
- interceptor 只讀取 token 並附加到 request。
|
||||
- 401 或 HTTP 錯誤由 interceptor 與錯誤事件流程集中處理。
|
||||
|
||||
這樣做的原因是避免循環依賴:
|
||||
錯誤透過 `normalizeError()` 轉成 UI 可理解的格式。UI 或 store 不需要直接理解 AxiosError。
|
||||
|
||||
`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` |
|
||||
需要取消請求時,由 store 或 composable 建立 `AbortController`,service module 只接收 `signal`。不要讓 service module 持有 controller 或 UI 狀態。
|
||||
|
||||
Reference in New Issue
Block a user