ac7e1959cf
Refresh the development guidance to point to layered src GUIDE files and document Phase 4 completion, including AppShell extraction and page driver/component adoption for non-maintenance pages.docs: update LLM guides for completed architecture phase Refresh the development guidance to point to layered src GUIDE files and document Phase 4 completion, including AppShell extraction and page driver/component adoption for non-maintenance pages.
21 KiB
21 KiB
資料流與元件分層優化策略
一、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
二、我們專案的現況診斷
Read only when needed: analyse now
三、優化後的資料流策略
3.1 核心資料流(單向 + 集中閘道)
┌─────────────────────────────────────────────────────────────┐
│ App Shell │
│ (App.vue → Layout → Global Overlays: Snackbar/Dialogs) │
└──────────────────────────┬──────────────────────────────────┘
│ page model (reactive)
▼
┌─────────────────────────────────────────────────────────────┐
│ Page Driver │
│ (views/*.vue — 極薄,只負責:組裝 page model / 事件轉發) │
└──────────────────────────┬──────────────────────────────────┘
│ props / emits
▼
┌─────────────────────────────────────────────────────────────┐
│ Page Component │
│ (PageXxx.vue — 組裝完整頁面,決定 Section 順序與 override) │
└──────────────────────────┬──────────────────────────────────┘
│ 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 的職責從「管理資料 + 管理狀態 + 組裝模板」縮減為「呼叫 usePageDriver() 取得 page model,傳給 Page component」。
- Page model 可以來自:
- store(已有快取)
- service(直接 API)
- composable(組裝多個來源)
範例:
// src/composables/usePageDriver.ts
export function useMaintenancePage() {
const studentStore = useStudentStore()
const { records, loading, error, load } = useCrudDriver({
store: studentStore,
loadAction: () => studentStore.fetchStudents(),
})
const pageModel = computed(() => ({
title: '單筆資料維護',
records: records.value,
loading: loading.value,
error: error.value,
}))
return { pageModel, load }
}
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 頁面載入(可選進階)
<!-- 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/ ← 維持:Page Driver(極薄)
│ └── maint/
│ └── SingleRecord.vue ← ~50 行:組裝 pageModel + 掛載 PageMaintDriver
│
├── components/
│ ├── pages/ ← 新增:Page Component 層
│ │ ├── PageMaintenance.vue
│ │ └── PageReport.vue
│ │
│ ├── 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/ ← 新增:頁面資料協調
│ │ └── useMaintenancePage.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 切換、全域 overlay(snackbar、dialog)、頁籤容器、事件總線橋接。
- 禁止:頁面專屬業務流程、頁面資料組裝、特定 dialog 內容。
- 對齊:App Store 的
App.svelte+browser.ts的 overlay 部分。
Layer 2: Page Driver(src/views/)
- 職責:
- 呼叫
useXxxPage()取得pageModel。 - 將
pageModel與事件處理器傳給對應的PageXxx.vue。 - 處理 route param 解析(僅限轉換,不含業務邏輯)。
- 呼叫
- 目標行數:< 80 行。
- 禁止:大量模板、dialog 定義、form 欄位、直接操作 store。
<!-- views/maint/SingleRecord.vue(優化後) -->
<script setup lang="ts">
import { useMaintenancePage } from '@/composables/page-drivers/useMaintenancePage'
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
const { pageModel, load } = useMaintenancePage({
title: '單筆資料維護',
records: [],
})
load()
</script>
<template>
<PageMaintenance :page="pageModel" />
</template>
Layer 3: Page Component(src/components/pages/)
- 職責:組裝完整頁面的 section 順序、處理 page-level slot override、分發頁面級事件。
- 命名:一律
Page前綴。 - 對齊:App Store 的
ProductPage.svelte、TodayPage.svelte、DefaultPage.svelte。
<!-- components/pages/PageMaintenance.vue -->
<template>
<PageMaintShell :title="page.title">
<template #search>
<SectionSearchPanel :fields="page.searchFields" @search="emit('search', $event)" />
</template>
<template #content>
<SectionDataTable :records="page.records" @edit="emit('edit', $event)" />
</template>
</PageMaintShell>
<!-- 頁面級 dialog 外掛,內容再拆到 SectionFormPanel -->
<SectionFormPanel
v-model="formVisible"
:mode="formMode"
:record="activeRecord"
@save="emit('save', $event)"
/>
</template>
Layer 4: Section / Shelf(src/components/sections/)
- 職責:決定「這一區的佈局方式」(水平捲軸、網格、列表、摺疊面板)。
- 禁止:知道上層 page 的業務邏輯、不直接呼叫 API。
- 對齊:App Store 的
Shelf.svelte、ShelfItemLayout.svelte。
<!-- 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。
<!-- 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:
// 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
└── SectionFormPanel(dialog shell)
├── MntDialogCard(外殼:標題、toolbar、actions)
└── ItemFormFieldGroup(內容:欄位)
SectionFormPanel管理 dialog 的開關、mode、loading、saving。ItemFormFieldGroup純粹呈現欄位,不知道自己在 dialog 裡。- View 中不再出現
<teleport>、<v-overlay>、<v-dialog>的具體定義。
五、重構優先順序與遷移路徑
Phase 1:建立基礎設施(不動既有 view) ✅ 已完成
- 新增
src/models/:定義領域模型與 Page union type。src/models/student.ts:抽出StudentRecord,stores/students.ts改為 re-export 以保持向後相容。src/models/page.ts:定義BasePageModel、MaintenancePageModel與PageModelunion。
- 新增
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。
- 新增
src/components/pages/:建立第一個PageMaintenance.vue(可從PageMaint.vue擴展)。- 定義
MaintenancePageModelprops 與create/edit/view/delete/searchemits。 - 使用
PageMaint.vue作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
- 定義
- 新增
src/composables/page-drivers/:建立useMaintenancePage.ts。- 透過 options 傳入 title 與 records,協調搜尋條件、分頁與
pageModel。 - 提供
load()與resetSearch()供 Page Driver 呼叫。
- 透過 options 傳入 title 與 records,協調搜尋條件、分頁與
Phase 2:遷移最厚的 view(SingleRecord.vue) ✅ 已完成
- 將
SingleRecord.vue縮減為 route-level Page Driver。- 目前只負責呼叫
useSingleRecordMaintenancePage(),並組裝PageMaintenance、SectionSearchPanel、SectionDataTable、SectionFormPanel。 - 行數已由 921 行縮減至 52 行,達成 < 80 行目標。
- 目前只負責呼叫
- 將搜尋區塊抽出到
src/components/sections/SectionSearchPanel.vue。- 負責搜尋欄位佈局與 reset 事件,不直接操作 store。
- 將表格與分頁抽出到
src/components/sections/SectionDataTable.vue。- 負責 headers、資料列 slot、操作按鈕與分頁 footer,透過 emit 回傳 view/edit/delete/page 事件。
- 將 dialog 抽出到
src/components/sections/SectionFormPanel.vue。- 包含側邊 overlay、
MntDialogCard、record navigation toolbar 與確認 dialog。 - View 中不再直接定義
<teleport>、<v-overlay>或多個確認 dialog。
- 包含側邊 overlay、
- 將表單欄位抽出到
src/components/items/ItemFormFieldGroup.vue。- 只呈現欄位與欄位錯誤,透過
v-model與clear-field-error與上層互動。
- 只呈現欄位與欄位錯誤,透過
- 將 CRUD command 流程抽出到
src/composables/commands/useCrudCommands.ts。- 負責新增、載入編輯/檢視資料、儲存前確認、實際儲存與 highlight 流程。
- 新增
src/composables/page-drivers/useSingleRecordMaintenancePage.ts作為本頁協調層。- 集中組裝 page model、搜尋狀態、表格分頁、表單狀態、CRUD command 與 dialog flow。
SingleRecord.vue不再直接操作studentStore。
Phase 3:推廣到所有 maintenance 頁面 ✅ 已完成
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作為主要內容元件。
MasterDetailA.vue依 Page Driver + Page Component 模式重構。src/views/maint/MasterDetailA.vue縮減為 34 行。- 新增
src/composables/page-drivers/useMasterDetailAMaintenancePage.ts。 - 新增
src/components/pages/PageMasterDetailAMaintenance.vue承接原本主從維護 UI。
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。
- 通用方向已落地為「每頁 page driver + page component」與既有
useCrudCommands()。- Phase 3 未再抽出跨 A/B/C 的大型共用 driver,避免在 demo 變體尚未收斂前建立過早抽象。
Phase 4:非 maintenance 頁面統一 ✅ 已完成
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。
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、手動/強制登出流程。
六、命名規範總結
| 層級 | 目錄 | 檔名前綴/範例 |
|---|---|---|
| App Shell | src/shell/ |
AppShell.vue、GlobalOverlays.vue |
| Page Driver | src/views/ |
SingleRecord.vue(route view,不改名) |
| Page Component | src/components/pages/ |
PageMaintenance.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/ |
useMaintenancePage.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 超過 100 行了嗎?→ 考慮抽出 Page Component。
- 這個元件知道自己是水平捲軸還是網格嗎?→ Item 層不該知道,移到 Section 層。
- 這個 dialog 定義在 view 裡嗎?→ 抽出到 SectionFormPanel。
- 這個元件直接呼叫
studentStore.updateStudent()嗎?→ 改為觸發 command 或 emit event。 - 這個狀態需要跨頁共享嗎?→ 是:store;否:composable。
- 這個邏輯是「取得資料」還是「執行動作」?→ query 用 page driver,command 用 command composable。
- 這個元件只服務單一 domain 嗎?→ 是:留在
components/items/或components/sections/的 domain 子目錄;否:才進base/。 - 這個抽象降低了理解成本嗎?→ 否:不要抽。
本文件取代 docs/frontend-layering.md 與 src/components/README.md 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。