docs: add architecture strategy and Vuetify MCP guidance
This commit is contained in:
@@ -25,3 +25,20 @@ import { mdiAccount } from '@mdi/js'
|
||||
<v-icon :icon="mdiAccount" />
|
||||
</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。
|
||||
|
||||
@@ -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 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 | `<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 切換、全域 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
|
||||
<!-- 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
|
||||
└── SectionFormPanel(dialog 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:遷移最厚的 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` 成為新增功能與重構的最高準則。既有檔案可保留作為歷史參考,但後續開發以本文為準。*
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"vuetify-mcp": {
|
||||
"type": "remote",
|
||||
"url": "https://mcp.vuetifyjs.com/mcp",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user