From 005ba663d6875572952a7bbbcead5d1a0db979ad Mon Sep 17 00:00:00 2001 From: skytek_xinliang Date: Mon, 18 May 2026 16:56:21 +0800 Subject: [PATCH] docs: add architecture strategy and Vuetify MCP guidance --- AGENTS.md | 17 ++ docs/architecture-strategy.md | 524 ++++++++++++++++++++++++++++++++++ opencode.json | 10 + 3 files changed, 551 insertions(+) create mode 100644 docs/architecture-strategy.md create mode 100644 opencode.json diff --git a/AGENTS.md b/AGENTS.md index a48a4fb..264ecba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,3 +25,20 @@ import { mdiAccount } from '@mdi/js' ``` + +## 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。 diff --git a/docs/architecture-strategy.md b/docs/architecture-strategy.md new file mode 100644 index 0000000..d2ab4c7 --- /dev/null +++ b/docs/architecture-strategy.md @@ -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` | `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` prop 驅動。 +- `PageResolver` 處理 `Promise` 的 loading / error 狀態。 +- `Page.svelte` 用 type guard 分發到對應的 page component:`isProductPage(page)` → ``。 +- **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 drilling:Jet、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 | `` 或 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 + + +``` + +- 若頁面資料支援 async setup 或 `usePageDriver` 回傳 Promise,可用 Vue `` 達到與 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 切換、全域 overlay(snackbar、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 + + + + +``` + +#### Layer 3: Page Component(`src/components/pages/`) + +- **職責**:組裝完整頁面的 section 順序、處理 page-level slot override、分發頁面級事件。 +- **命名**:一律 `Page` 前綴。 +- **對齊**:App Store 的 `ProductPage.svelte`、`TodayPage.svelte`、`DefaultPage.svelte`。 + +```vue + + +``` + +#### Layer 4: Section / Shelf(`src/components/sections/`) + +- **職責**:決定「這一區的佈局方式」(水平捲軸、網格、列表、摺疊面板)。 +- **禁止**:知道上層 page 的業務邏輯、不直接呼叫 API。 +- **對齊**:App Store 的 `Shelf.svelte`、`ShelfItemLayout.svelte`。 + +```vue + + +``` + +#### Layer 5: Item / Atom(`src/components/items/`) + +- **職責**:純粹呈現單一資料單位。 +- **禁止**:知道自己是水平捲軸還是網格、不管理任何狀態。 +- **對齊**:App Store 的 `BrickItem.svelte`、`LargeLockupItem.svelte`。 + +```vue + + +``` + +### 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 + └── SectionFormPanel(dialog shell) + ├── MntDialogCard(外殼:標題、toolbar、actions) + └── ItemFormFieldGroup(內容:欄位) +``` + +- `SectionFormPanel` 管理 dialog 的開關、mode、loading、saving。 +- `ItemFormFieldGroup` 純粹呈現欄位,不知道自己在 dialog 裡。 +- View 中不再出現 ``、``、`` 的具體定義。 + +--- + +## 五、重構優先順序與遷移路徑 + +### 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:遷移最厚的 view(SingleRecord.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 driver,command 用 command composable。 +- [ ] 這個元件只服務單一 domain 嗎?→ 是:留在 `components/items/` 或 `components/sections/` 的 domain 子目錄;否:才進 `base/`。 +- [ ] 這個抽象降低了理解成本嗎?→ 否:不要抽。 + +--- + +*本文件取代 `docs/frontend-layering.md` 與 `src/components/README.md` 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。* diff --git a/opencode.json b/opencode.json new file mode 100644 index 0000000..d3fea7f --- /dev/null +++ b/opencode.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "vuetify-mcp": { + "type": "remote", + "url": "https://mcp.vuetifyjs.com/mcp", + "enabled": true + } + } +} \ No newline at end of file