Files
skt-vuetify-templates/docs/architecture-strategy.md
T
skytek_xinliang 7b0cfe4448 refactor(login): compose page from focused login components
Split the login page into smaller reusable components for branding,
toolbar, header, form, announcements, and mobile layout behavior. This
keeps the view responsible for orchestration while moving UI sections into
focused components.

Update page creation docs to reflect the simplified flow where views render
sections/items directly and composables coordinate store/service access when
needed.refactor(login): compose page from focused login components

Split the login page into smaller reusable components for branding,
toolbar, header, form, announcements, and mobile layout behavior. This
keeps the view responsible for orchestration while moving UI sections into
focused components.

Update page creation docs to reflect the simplified flow where views render
sections/items directly and composables coordinate store/service access when
needed.
2026-05-27 13:43:43 +08:00

427 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 資料流與元件分層優化策略
## 一、Apple App Store 專案的核心架構特徵
1 單一業務邏輯門面
2 Intent / Action 分離(查詢與命令)
3 Page Model 驅動 UI(資料驅動)
4 Shelf / Item 分層(容器與內容分離)
5 Svelte Context 作為跨層依賴注入
6 命令式外殼 + 聲明式 UI
Read only when needed: [what apple do](./what-apple-do.md)
## 二、我們專案的現況診斷
Read only when needed: [analyse now](./analyse-now.md)
## 三、優化後的資料流策略
### 3.1 核心資料流(單向 + 集中閘道)
```
┌─────────────────────────────────────────────────────────────┐
│ App Shell │
│ (App.vue → Layout → Global Overlays: Snackbar/Dialogs) │
└──────────────────────────┬──────────────────────────────────┘
│ reactive / props
┌─────────────────────────────────────────────────────────────┐
│ View │
│ (views/*.vue — 自含 page model、頁面 UI 與 section 組合) │
└──────────────────────────┬──────────────────────────────────┘
│ section data
┌─────────────────────────────────────────────────────────────┐
│ Section / Shelf │
│ (決定佈局:水平/網格/列表;不關心內部呈現) │
└──────────────────────────┬──────────────────────────────────┘
│ item data
┌─────────────────────────────────────────────────────────────┐
│ Item / Atom │
│ (純粹內容呈現;透過 provide/inject 取得 domain context) │
└─────────────────────────────────────────────────────────────┘
橫向:
composables → 可重用流程(CRUD state machine、form validation、editable grid
stores → 跨頁共享狀態(auth、menu、favorites、messages
services → HTTP 閘道(只封裝 ky,不持有 UI 狀態)
```
### 3.2 Page Model 作為主要資料單位
- **新增 `src/models/page.ts`**:定義各頁面的統一介面。
- View 的職責從「管理資料 + 管理狀態 + 組裝模板」縮減為「呼叫 composable 取得 page model,組裝 section 元件」。
- Page model 可以來自:
- store(已有快取)
- service(直接 API
- composable(組裝多個來源)
範例:
```ts
// views/maint/Example.vue — 簡單頁面直接在 view 組裝 page model
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '單筆資料維護',
records: studentStore.students,
loading: false,
error: null,
}))
```
### 3.3 查詢(Query)與命令(Command)分離
| 類型 | 資料流 | 錯誤處理 | 狀態位置 |
|------|--------|----------|----------|
| **Query** | `usePageDriver``pageModel` → props | `<PageErrorBoundary>` 或 page-level fallback | composable 內部 ref |
| **Command** | `executeCommand()``await service.action()` → 重新載入 query | snackbar / dialog / field error | composable 內部 ref |
- Query 對應 App Store 的 **Intent**:取得資料、回傳 model。
- Command 對應 App Store 的 **Action**:執行副作用、不回傳 model、觸發 Query 重新整理。
### 3.4 Promise-based 頁面載入(可選進階)
```vue
<!-- PageResolver.vue 對齊 App Store PageResolver.svelte -->
<template>
<Suspense>
<template #default>
<component :is="pageComponent" :page="resolvedPage" />
</template>
<template #fallback>
<PageLoadingSpinner />
</template>
</Suspense>
</template>
```
- 若頁面資料支援 async setup 或 `usePageDriver` 回傳 Promise,可用 Vue `<Suspense>` 達到與 App Store `{#await page}` 類似的效果。
- **短期**:維持 reactive ref,但將 loading / error 統一封裝在 `usePageDriver`
- **長期**:當多數頁面都使用統一 driver 後,可考慮引入 Suspense。
### 3.5 全域狀態 vs 頁面狀態的邊界
| 狀態類型 | 存放位置 | 生命周期 |
|----------|----------|----------|
| 認證、選單、語言、主題 | `src/stores/*.ts` | 應用級 |
| 搜尋條件、分頁、dialog visible | `src/composables/useXxxPage.ts` | 頁面級(離開頁面可選保留或重置) |
| 表單 dirty / validation | `src/composables/useXxxForm.ts` | dialog / form 級 |
| 表格排序、過濾 | `src/composables/useXxxTable.ts` | 區塊級 |
---
## 四、優化後的元件分層策略
### 4.1 五層結構
```
src/
├── shell/ ← 新增:App Shell(原 App.vue 拆分)
│ ├── AppShell.vue ← layout 切換、全域 overlay 掛載點
│ ├── GlobalOverlays.vue ← snackbar、確認 dialog、toast
│ └── AppTabs.vue ← 頁籤(從 App.vue 抽出)
├── views/ ← 維持:自含頁面,邏輯與 UI 同檔
│ └── maint/
│ └── SingleRecord.vue ← ~50 行:組裝 pageModel + MaintShell 外殼
├── components/
│ ├── sections/ ← 新增:Section / Shelf 層
│ │ ├── SectionSearchPanel.vue
│ │ ├── SectionDataTable.vue
│ │ └── SectionFormPanel.vue
│ │
│ ├── items/ ← 新增:Item / Atom 層(領域獨立)
│ │ ├── ItemDataRow.vue
│ │ └── ItemFormField.vue
│ │
│ ├── layouts/ ← 維持:App Shell Layout
│ │ ├── MainLayout.vue
│ │ └── PlainLayout.vue
│ │
│ └── base/ ← 維持:真正跨頁共用
│ └── DraggableDialog.vue
├── composables/
│ ├── page-drivers/ ← 新增:頁面資料協調(僅複雜頁面需要)
│ │ └── useSingleRecordMaintenancePage.ts
│ ├── commands/ ← 新增:命令流程(對齊 Jet Action)
│ │ └── useCrudCommands.ts
│ ├── forms/ ← 維持/重組:表單狀態機
│ │ └── useForm.ts
│ └── layout/ ← 維持
├── models/ ← 新增:領域模型與 Page Union
│ ├── page.ts
│ └── student.ts
├── stores/ ← 維持:跨頁共享狀態
├── services/ ← 維持:HTTP 閘道
└── router/ ← 維持:路由與 meta
```
### 4.2 各層職責與規範
#### Layer 1: App Shell`src/shell/`
- **職責**layout 切換、全域 overlaysnackbar、dialog)、頁籤容器、事件總線橋接。
- **禁止**:頁面專屬業務流程、頁面資料組裝、特定 dialog 內容。
- **對齊**App Store 的 `App.svelte` + `browser.ts` 的 overlay 部分。
#### Layer 2: Page Driver`src/views/`
- **職責**
1. 呼叫 `useXxxPage()` 取得 `pageModel`
2.`pageModel` 與事件處理器傳給對應的 `PageXxx.vue`
3. 處理 route param 解析(僅限轉換,不含業務邏輯)。
- **目標行數**< 80 行。
- **禁止**:大量模板、dialog 定義、form 欄位、直接操作 store。
```vue
<!-- views/maint/SingleRecord.vue優化後 -->
<script setup lang="ts">
import MaintShell from '@/components/MaintShell.vue'
import { useSingleRecordMaintenancePage } from '@/composables/page-drivers/useSingleRecordMaintenancePage'
const { commands, formPanelEvents, formPanelProps, pageModel, searchPanelOpen } = useSingleRecordMaintenancePage()
</script>
<template>
<PageMaintenance :page="pageModel" />
</template>
```
#### Layer 3: View`src/views/`
- **職責**:自含頁面的完整入口 — 組裝 page model、協調 composable、撰寫頁面 template。
- **禁止**:頁面 UI 不再拆到另一個 page component 層。
- **對齊**:標準 Vue SPA 慣例。
```vue
<!-- views/maint/SingleRecord.vue -->
<script setup lang="ts">
import MaintShell from '@/components/MaintShell.vue'
import { useSingleRecordMaintenancePage } from '@/composables/page-drivers/useSingleRecordMaintenancePage'
const { pageModel, searchPanelOpen, commands, search, students, ... } = useSingleRecordMaintenancePage()
</script>
<template>
<MaintShell :title="pageModel.title" @create="commands.openAddDialog">
<template #table>...</template>
</MaintShell>
</template>
```
#### Layer 4: Section / Shelf`src/components/sections/`
- **職責**:決定「這一區的佈局方式」(水平捲軸、網格、列表、摺疊面板)。
- **禁止**:知道上層 page 的業務邏輯、不直接呼叫 API。
- **對齊**App Store 的 `Shelf.svelte``ShelfItemLayout.svelte`
```vue
<!-- SectionDataTable.vue -->
<template>
<v-data-table
:headers="resolvedHeaders"
:items="records"
fixed-header
>
<template v-for="slot in customSlots" :key="slot.key" #[slot.key]="{ item }">
<slot :name="slot.key" :item="item">
<!-- 預設 item 渲染 -->
<component :is="slot.itemComponent" :data="item" />
</slot>
</template>
</v-data-table>
</template>
```
#### Layer 5: Item / Atom`src/components/items/`
- **職責**:純粹呈現單一資料單位。
- **禁止**:知道自己是水平捲軸還是網格、不管理任何狀態。
- **對齊**App Store 的 `BrickItem.svelte``LargeLockupItem.svelte`
```vue
<!-- ItemDataRow.vue -->
<template>
<div class="d-flex ga-2">
<v-chip size="small" :color="statusColor(data.status)">
{{ data.status }}
</v-chip>
<span>{{ data.name }}</span>
</div>
</template>
```
### 4.3 容器/內容分離的具體規範
| 場景 | 容器(Section | 內容(Item |
|------|-----------------|--------------|
| 資料表格 | `SectionDataTable`(決定 headers、分頁、排序) | `ItemDataRow`(決定單列呈現) |
| 搜尋面板 | `SectionSearchPanel`(決定展開/收合、grid 佈局) | `ItemFormField`(單一輸入框呈現) |
| 圖文列表 | `SectionCardGrid`(決定欄數、gap、RWD | `ItemProductCard`(卡片內容) |
| 表單對話框 | `SectionFormPanel`(決定 dialog 外殼、actions | `ItemFormFieldGroup`(欄位群組) |
- **原則**:若同一組資料在不同頁面需要「水平捲軸 vs 網格」兩種呈現,只換 Section,不換 Item。
### 4.4 Provide / Inject 作為跨層依賴注入
對齊 App Store 的 `getJet()` / `getI18n()`,在 Vue 中建立明確的 inject API
```ts
// src/providers/page.ts
import { inject, provide } from 'vue'
import type { PageDriver } from '@/composables/page-drivers/types'
const PageDriverKey = Symbol('page-driver')
export function providePageDriver(driver: PageDriver) {
provide(PageDriverKey, driver)
}
export function usePageDriverInjected(): PageDriver {
const driver = inject(PageDriverKey)
if (!driver) throw new Error('usePageDriverInjected called without provider')
return driver
}
```
- **提供時機**Page Component (`PageXxx.vue`) 在掛載時 provide。
- **使用時機**:深層的 Item 元件需要觸發 page-level action 時 inject,避免 props drilling。
- **禁止**:用 provide/inject 傳遞會頻繁變動的 UI 狀態(如 `dialogVisible`)。這類狀態應透過 props + emits。
### 4.5 Dialog 外層化策略
將 dialog 從 view 中完全抽出,形成「Dialog Shell + Content Slot」模式:
```
views/xxx.vue
└── PageXxx.vue
├── SectionDataTable
└── SectionFormPaneldialog shell
├── MntDialogCard(外殼:標題、toolbar、actions
└── ItemFormFieldGroup(內容:欄位)
```
- `SectionFormPanel` 管理 dialog 的開關、mode、loading、saving。
- `ItemFormFieldGroup` 純粹呈現欄位,不知道自己在 dialog 裡。
- View 中不再出現 `<teleport>``<v-overlay>``<v-dialog>` 的具體定義。
---
## 五、重構優先順序與遷移路徑
### Phase 1:建立基礎設施(不動既有 view) ✅ 已完成
1. [x] 新增 `src/models/`:定義領域模型與 Page union type。
- `src/models/student.ts`:抽出 `StudentRecord``stores/students.ts` 改為 re-export 以保持向後相容。
- `src/models/page.ts`:定義 `BasePageModel``MaintenancePageModel``PageModel` union。
2. [x] 新增 `src/shell/`:從 `App.vue` 抽出 `GlobalOverlays.vue``AppTabs.vue`
- `src/shell/AppShell.vue`layout 切換與全域 overlay 掛載點。
- `src/shell/AppTabs.vue`:頁籤管理與 router-view 容器。
- `src/shell/GlobalOverlays.vue`snackbar、搜尋 dialog、訊息 dialog。
3. [x] 新增 `src/components/pages/`:建立第一個 `PageMaintenance.vue`(可從 `PageMaint.vue` 擴展)。
- 定義 `MaintenancePageModel` props 與 `create/edit/view/delete/search` emits。
- 使用 `MaintShell.vue` 作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
4. [x] 新增 `src/composables/page-drivers/`:建立第一個 page driver 範例。
- 協調搜尋條件、分頁與 `pageModel`
- 提供 `load()``resetSearch()` 供 Page Driver 呼叫。
- 後續已刪除純包裝型 driver(如 `useMaintenancePage`)。僅當頁面需要協調多個 composable 時才建立 page driver。
### Phase 2:遷移最厚的 viewSingleRecord.vue ✅ 已完成
1. [x] 將 `SingleRecord.vue` 縮減為 route-level Page Driver。
- 目前只負責呼叫 `useSingleRecordMaintenancePage()`,並組裝 `PageMaintenance``SectionSearchPanel``SectionDataTable``SectionFormPanel`
- 行數已由 921 行縮減至 52 行,達成 < 80 行目標。
2. [x] 將搜尋區塊抽出到 `src/components/sections/SectionSearchPanel.vue`
- 負責搜尋欄位佈局與 reset 事件,不直接操作 store。
3. [x] 將表格與分頁抽出到 `src/components/sections/SectionDataTable.vue`
- 負責 headers、資料列 slot、操作按鈕與分頁 footer,透過 emit 回傳 view/edit/delete/page 事件。
4. [x] 將 dialog 抽出到 `src/components/sections/SectionFormPanel.vue`
- 包含側邊 overlay、`MntDialogCard`、record navigation toolbar 與確認 dialog。
- View 中不再直接定義 `<teleport>``<v-overlay>` 或多個確認 dialog。
5. [x] 將表單欄位抽出到 `src/components/items/ItemFormFieldGroup.vue`
- 只呈現欄位與欄位錯誤,透過 `v-model``clear-field-error` 與上層互動。
6. [x] 將 CRUD command 流程抽出到 `src/composables/commands/useCrudCommands.ts`
- 負責新增、載入編輯/檢視資料、儲存前確認、實際儲存與 highlight 流程。
7. [x] 新增 `src/composables/page-drivers/useSingleRecordMaintenancePage.ts` 作為本頁協調層。
- 集中組裝 page model、搜尋狀態、表格分頁、表單狀態、CRUD command 與 dialog flow。
- `SingleRecord.vue` 不再直接操作 `studentStore`
### Phase 3:推廣到所有 maintenance 頁面 ✅ 已完成
> 後續簡化時,B/C/EditableGrid 的薄 page driver 已 inline 回 view,只保留有真實複雜邏輯的 driver。
1. [x] `EditableGrid.vue` 依 Page Driver + Page Component 模式重構。
- `src/views/maint/EditableGrid.vue` 縮減為 10 行 route-level wiring。
- 新增 `src/composables/page-drivers/useEditableGridMaintenancePage.ts`
- 新增 `src/components/pages/PageEditableGridMaintenance.vue`,保留既有 `src/components/maint/EditableGrid.vue` 作為主要內容元件。
2. [x] `MasterDetailA.vue` 依 Page Driver + Page Component 模式重構。
- `src/views/maint/MasterDetailA.vue` 縮減為 34 行。
- 新增 `src/composables/page-drivers/useMasterDetailAMaintenancePage.ts`
- 新增 `src/components/pages/PageMasterDetailAMaintenance.vue` 承接原本主從維護 UI。
3. [x] `MasterDetailB.vue``MasterDetailC.vue` 依 Page Driver + Page Component 模式重構。
- `src/views/maint/MasterDetailB.vue``src/views/maint/MasterDetailC.vue` 均縮減為 10 行。
- 新增 `src/composables/page-drivers/useMasterDetailBMaintenancePage.ts``src/composables/page-drivers/useMasterDetailCMaintenancePage.ts`
- 新增 `src/components/pages/PageMasterDetailBMaintenance.vue``src/components/pages/PageMasterDetailCMaintenance.vue`
4. [x] 通用方向已落地為「每頁 page driver + page component」與既有 `useCrudCommands()`
- Phase 3 未再抽出跨 A/B/C 的大型共用 driver,避免在 demo 變體尚未收斂前建立過早抽象。
### Phase 4:非 maintenance 頁面統一 ✅ 已完成
> 後續簡化時,Settings/FncPage 的薄 page driver 已 inline 回 view,型別移至 page component 自身。
1. [x] `Home.vue``Settings.vue``FncPage.vue` 套用 Page Driver + Page Component 模式。
- `src/views/Home.vue` 縮減為 17 行,新增 `src/components/pages/PageHome.vue``src/composables/page-drivers/useHomePage.ts`
- `src/views/Settings.vue` 縮減為 10 行,新增 `src/components/pages/PageSettings.vue``src/composables/page-drivers/useSettingsPage.ts`
- `src/views/FncPage.vue` 縮減為 10 行,新增 `src/components/pages/PageFunction.vue``src/composables/page-drivers/useFunctionPage.ts`
2. [x] `App.vue` 最終只保留 shell 掛載。
- `src/App.vue` 縮減為 7 行,只掛載 `AppShell`
- `src/shell/AppShell.vue` 承接 layout 切換、layout props/events、breadcrumb actions、tabs router-view 與 `GlobalOverlays` 掛載。
- `src/composables/layout/useAppShell.ts` 承接 menu 合併、favorite、breadcrumb、layout action、手動/強制登出流程。
### Phase 5:移除 Page Component 層 ✅ 已完成
> 所有 page component 已合併回對應的 view`src/components/pages/` 目錄已刪除。page driver 簡化為僅複雜頁面才使用的選配層,view 回歸標準 Vue SPA 慣例:自含 page model + 頁面 UI + section 組合。
---
## 六、命名規範總結
| 層級 | 目錄 | 檔名前綴/範例 |
|------|------|---------------|
| App Shell | `src/shell/` | `AppShell.vue``GlobalOverlays.vue` |
| View(自含頁面) | `src/views/` | `SingleRecord.vue` |
| Section / Shelf | `src/components/sections/` | `SectionDataTable.vue``SectionSearchPanel.vue` |
| Item / Atom | `src/components/items/` | `ItemDataRow.vue``ItemFormField.vue` |
| Layout | `src/components/layouts/` | `MainLayout.vue`(維持) |
| Base | `src/components/base/` | `DraggableDialog.vue`(維持) |
| Page Driver Composable | `src/composables/page-drivers/` | `useSingleRecordMaintenancePage.ts` |
| Command Composable | `src/composables/commands/` | `useCrudCommands.ts` |
| Form Composable | `src/composables/forms/` | `useForm.ts` |
| Domain Store | `src/stores/` | `students.ts`(維持) |
| Service Module | `src/services/modules/` | `students.ts`(維持) |
| Domain Model | `src/models/` | `student.ts``page.ts` |
---
## 七、對齊檢查清單(新增/重構時使用)
- [ ] 這個 view 超過 200 行了嗎?→ 考慮抽出 Section 或 composable。
- [ ] 這個元件知道自己是水平捲軸還是網格嗎?→ Item 層不該知道,移到 Section 層。
- [ ] 這個 dialog 定義在 view 裡嗎?→ 抽出到 SectionFormPanel。
- [ ] 這個元件直接呼叫 `studentStore.updateStudent()` 嗎?→ 改為觸發 command 或 emit event。
- [ ] 這個狀態需要跨頁共享嗎?→ 是:store;否:composable。
- [ ] 這個邏輯是「取得資料」還是「執行動作」?→ query 用 page drivercommand 用 command composable。
- [ ] 這個元件只服務單一 domain 嗎?→ 是:留在 `components/items/``components/sections/` 的 domain 子目錄;否:才進 `base/`
- [ ] 這個抽象降低了理解成本嗎?→ 否:不要抽。
---
*本文件取代 `docs/frontend-layering.md` 與 `src/components/GUIDE.md` 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。*