docs: add architecture strategy and Vuetify MCP guidance

This commit is contained in:
skytek_xinliang
2026-05-18 16:56:21 +08:00
parent 130a907351
commit 005ba663d6
3 changed files with 551 additions and 0 deletions
+17
View File
@@ -25,3 +25,20 @@ import { mdiAccount } from '@mdi/js'
<v-icon :icon="mdiAccount" /> <v-icon :icon="mdiAccount" />
</template> </template>
``` ```
## Vuetify MCP
- When looking up Vuetify versions, release notes, component APIs, directive APIs, installation guides, FAQs, feature guides, or package exports, use Vuetify MCP first.
- When a question involves Vuetify component props, events, slots, exposed methods, generated DOM, accessibility output, or officially supported extension points, verify with Vuetify MCP before changing the implementation.
- Prefer the official API and documentation information returned by MCP. Do not infer Vuetify behavior.
### 常用工具
- `get_release_notes_by_version`: 查詢指定版本或 latest 的 release notes。
- `get_component_api_by_version`: 查詢指定 Vuetify 元件的 props、events、slots、exposed methods。
- `get_directive_api_by_version`: 查詢指定 Vuetify directive 的 API。
- `get_vuetify_api_by_version`: 下載並快取指定版本的 Vuetify API types。
- `get_installation_guide`: 查詢 Vite、Nuxt、Laravel、CDN 等安裝方式。
- `get_feature_guide`: 查詢 theme、icons、i18n、display、layout 等功能指南。
- `get_exposed_exports`: 查詢 Vuetify npm package 可匯出的項目。
- `get_frequently_asked_questions`: 查詢 Vuetify FAQ。
+524
View File
@@ -0,0 +1,524 @@
# 資料流與元件分層優化策略
> 分析標的:`~/code/apps.apple.com-main`Apple App Store Web — Svelte + Jet 架構)
> 適用目標:`/home/carl/git/skt-vuetify-templates`Vue 3 + Vuetify + Pinia
> 原則:不強制遷移既有程式碼,但所有新增功能與重構以本文件為最高準則。
---
## 一、Apple App Store 專案的核心架構特徵
### 1.1 單一業務邏輯門面(Jet Facade)
```
browser.ts → bootstrap → Jet.load → runtime + objectGraph
UI 層僅透過 jet.dispatch(intent) / jet.perform(action) 溝通
```
- **Jet** 封裝所有業務邏輯:路由、資料取得、動作分發、metrics。
- UI 層**不直接**呼叫 API、不直接操作 storage、不直接操作 history。
- 所有外部依賴(fetch、storage、locale、user)統一注入 `Dependencies`,再組裝成 `ObjectGraph`
### 1.2 Intent / Action 分離(查詢與命令)
| 類型 | 職責 | 回傳值 | 例子 |
|------|------|--------|------|
| **Intent** | 取得頁面資料(Query | `Promise<Page>` | `RouteUrlIntent` → 回傳 `ProductPage` |
| **Action** | 執行副作用(Command | `'performed' \| 'unsupported'` | `FlowAction` → 導航到新頁面 |
- `FlowAction` 是主要導航機制:內含 `destination Intent` + `pageUrl` + `presentationContext`
- Action handler 註冊採用**型別註冊制**:`jet.onAction('flowAction', handler)`
### 1.3 Page Model 驅動 UI(資料驅動)
```ts
// App.svelte 的介面極簡
export let page: Promise<Page> | Page
```
- 整個應用由單一 `page` prop 驅動。
- `PageResolver` 處理 `Promise<Page>` 的 loading / error 狀態。
- `Page.svelte` 用 type guard 分發到對應的 page component`isProductPage(page)``<ProductPage>`
- **Page 是 union type**,不是 route-based 的硬編碼映射。
### 1.4 Shelf / Item 分層(容器與內容分離)
```
Page (TodayPage / ProductPage / ...)
└── Shelf[] (水平捲軸 / 網格)
└── ShelfItemLayout (佈局抽象:HorizontalShelf or Grid)
└── Item (BrickItem / LargeLockupItem / ...)
```
- **Shelf** = 容器邏輯:決定是水平捲軸還是網格、rowsPerColumn、邊框。
- **ShelfItemLayout** = 佈局中介:根據 `isHorizontal` 選擇 `HorizontalShelf``Grid`
- **Item** = 純粹內容渲染:只關心單一資料單位的呈現,不知道自己是水平還是網格。
- **FallbackShelf** = 優雅的降級策略:遇到未實作的 shelf 類型顯示 placeholder,不 crash。
### 1.5 Svelte Context 作為跨層依賴注入
```ts
// bootstrap.ts
context.set('jet', jet)
context.set('i18n', i18nStore)
// 深層元件
const jet = getJet() // 從 Svelte Context 取得
const i18n = getI18n() // 從 Svelte Context 取得
```
- 避免 props drillingJet、i18n、accessibility layout、today-card layout 都透過 context 傳遞。
- Context 在啟動時注入,生命周期與應用一致,不是用來傳遞 UI 狀態的。
### 1.6 命令式外殼 + 聲明式 UI
```ts
// browser.ts(命令式啟動層)
const app = new App({ target: container, context, hydrate: true })
registerActionHandlers({
jet,
updateApp: (props) => app.$set(props), // 橋接命令式 → 聲明式
})
```
- 導航、歷史管理、scroll 復原由命令式的 action handler 處理。
- UI 渲染完全聲明式,只接收 `page``isFirstPage` 兩個 prop。
---
## 二、我們專案的現況診斷
### 2.1 App.vue 過度臃腫(~590 行)
| 職責 | 行數 | 應屬層級 |
|------|------|----------|
| Layout 切換 | ~20 | App Shell |
| Tabs 管理 | ~80 | Page Driver |
| Breadcrumb 組裝 | ~40 | Layout |
| Favorites 管理 | ~60 | Store |
| Search Dialog | ~80 | App Shell / Widget |
| Message Dialog | ~60 | App Shell / Widget |
| Snackbar | ~10 | Global Overlay |
| Logout / Force logout | ~30 | Auth Flow |
| HTTP Toast | ~20 | Service Layer |
- **問題**App.vue 同時承擔 App Shell、Page Driver、Global Widget、Auth Flow 四種責任。
- **對比**App Store 的 `App.svelte` 只有 161 行,只負責 `Navigation + PageResolver + Footer`
### 2.2 Views 過厚(SingleRecord.vue ~830 行)
- 混雜:表格呈現、搜尋表單、dialog 模板、表單狀態、CRUD 流程、驗證邏輯、分頁、snackbar。
- **對比**App Store 的 `ProductPage.svelte` 只有 77 行,只負責「把 page 轉成 DefaultPageRequirements + 一個 slot override」。
### 2.3 缺乏統一的頁面資料門面
```
現況:
view → store → service(直接鏈式呼叫)
view 自己管理 loading / error / dialog visible
App Store
UI → jet.dispatch(intent) → runtime → controller → page model
UI 只接收 page model,不管理載入狀態
```
### 2.4 Dialog 狀態與模板內嵌於 View
- `SingleRecord.vue` 內含 5 個 `ConfirmDialog` 實例 + 1 個大 form overlay。
- 任何 dialog 更動都需要修改 view 檔案。
### 2.5 沒有容器/內容分離的 Section 層
- 表格、表單、搜尋區塊都是直接寫在 view 或 page component 中。
- 缺乏類似 `ShelfItemLayout` 的通用佈局抽象:「這一區是水平捲軸還是網格」應該由容器決定,裡面的內容元件不應該知道。
---
## 三、優化後的資料流策略
### 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(組裝多個來源)
範例:
```ts
// src/composables/usePageDriver.ts
export function useStudentMaintenancePage() {
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 頁面載入(可選進階)
```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/ ← 維持:Page Driver(極薄)
│ └── maint/
│ └── SingleRecord.vue ← ~50 行:組裝 pageModel + 掛載 PageMaintDriver
├── components/
│ ├── pages/ ← 新增:Page Component 層
│ │ ├── PageStudentMaintenance.vue
│ │ └── PageReport.vue
│ │
│ ├── sections/ ← 新增:Section / Shelf 層
│ │ ├── SectionSearchPanel.vue
│ │ ├── SectionDataTable.vue
│ │ └── SectionFormPanel.vue
│ │
│ ├── items/ ← 新增:Item / Atom 層(領域獨立)
│ │ ├── ItemStudentRow.vue
│ │ └── ItemFormField.vue
│ │
│ ├── layouts/ ← 維持:App Shell Layout
│ │ ├── MainLayout.vue
│ │ └── PlainLayout.vue
│ │
│ └── base/ ← 維持:真正跨頁共用
│ └── DraggableDialog.vue
├── composables/
│ ├── page-drivers/ ← 新增:頁面資料協調
│ │ └── useStudentMaintenancePage.ts
│ ├── commands/ ← 新增:命令流程(對齊 Jet Action)
│ │ └── useStudentCrudCommands.ts
│ ├── forms/ ← 維持/重組:表單狀態機
│ │ └── useStudentForm.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 { useStudentMaintenancePage } from '@/composables/page-drivers/useStudentMaintenancePage'
import PageStudentMaintenance from '@/components/pages/PageStudentMaintenance.vue'
const { pageModel, load } = useStudentMaintenancePage()
load()
</script>
<template>
<PageStudentMaintenance :page="pageModel" />
</template>
```
#### Layer 3: Page Component`src/components/pages/`
- **職責**:組裝完整頁面的 section 順序、處理 page-level slot override、分發頁面級事件。
- **命名**:一律 `Page` 前綴。
- **對齊**App Store 的 `ProductPage.svelte``TodayPage.svelte``DefaultPage.svelte`
```vue
<!-- components/pages/PageStudentMaintenance.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`
```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
<!-- ItemStudentRow.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、分頁、排序) | `ItemStudentRow`(決定單列呈現) |
| 搜尋面板 | `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. 新增 `src/models/`:定義領域模型與 Page union type。
2. 新增 `src/shell/`:從 `App.vue` 抽出 `GlobalOverlays.vue``AppTabs.vue`
3. 新增 `src/components/pages/`:建立第一個 `PageStudentMaintenance.vue`(可從 `PageMaint.vue` 擴展)。
4. 新增 `src/composables/page-drivers/`:建立 `useStudentMaintenancePage.ts`
### Phase 2:遷移最厚的 viewSingleRecord.vue
1.`SingleRecord.vue` 的模板抽出到 `PageStudentMaintenance.vue`
2. 將 dialog 抽出到 `SectionFormPanel.vue`
3. 將表單欄位抽出到 `ItemFormFieldGroup.vue``ItemFormField.vue[]`
4. 將 CRUD 流程抽出到 `composables/commands/useStudentCrudCommands.ts`
5. `SingleRecord.vue` 目標縮減到 < 80 行。
### Phase 3:推廣到所有 maintenance 頁面
1. `EditableGrid.vue``MasterDetailA/B/C.vue` 依同樣模式重構。
2. 建立通用的 `useCrudPageDriver()``useCrudCommands()`,減少重複。
### Phase 4:非 maintenance 頁面統一
1. `Home.vue``Settings.vue``FncPage.vue` 套用 Page Driver + Page Component 模式。
2. `App.vue` 最終只保留 layout 切換與 `GlobalOverlays` 掛載。
---
## 六、命名規範總結
| 層級 | 目錄 | 檔名前綴/範例 |
|------|------|---------------|
| App Shell | `src/shell/` | `AppShell.vue``GlobalOverlays.vue` |
| Page Driver | `src/views/` | `SingleRecord.vue`route view,不改名) |
| Page Component | `src/components/pages/` | `PageStudentMaintenance.vue` |
| Section / Shelf | `src/components/sections/` | `SectionDataTable.vue``SectionSearchPanel.vue` |
| Item / Atom | `src/components/items/` | `ItemStudentRow.vue``ItemFormField.vue` |
| Layout | `src/components/layouts/` | `MainLayout.vue`(維持) |
| Base | `src/components/base/` | `DraggableDialog.vue`(維持) |
| Page Driver Composable | `src/composables/page-drivers/` | `useStudentMaintenancePage.ts` |
| Command Composable | `src/composables/commands/` | `useStudentCrudCommands.ts` |
| Form Composable | `src/composables/forms/` | `useStudentForm.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 drivercommand 用 command composable。
- [ ] 這個元件只服務單一 domain 嗎?→ 是:留在 `components/items/``components/sections/` 的 domain 子目錄;否:才進 `base/`
- [ ] 這個抽象降低了理解成本嗎?→ 否:不要抽。
---
*本文件取代 `docs/frontend-layering.md` 與 `src/components/README.md` 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。*
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"vuetify-mcp": {
"type": "remote",
"url": "https://mcp.vuetifyjs.com/mcp",
"enabled": true
}
}
}