docs: clarify optional page drivers in page guide

Update documentation to show that simple pages can define page models
directly in views without creating a page driver. Adjust examples,
section numbering, and naming guidance to better distinguish simple view
state from reusable page-driver patterns.docs: clarify optional page drivers in page guide

Update documentation to show that simple pages can define page models
directly in views without creating a page driver. Adjust examples,
section numbering, and naming guidance to better distinguish simple view
state from reusable page-driver patterns.
This commit is contained in:
skytek_xinliang
2026-05-27 11:18:19 +08:00
parent b8664b5c3e
commit ad00f5c195
8 changed files with 59 additions and 232 deletions
+30 -52
View File
@@ -7,18 +7,22 @@
目前新增一般頁面的預設資料流:
```txt
router -> view -> page driver -> page component -> sections/items
router -> view -> (page driver) -> page component -> sections/items
store/composable -> service
store/composable -> service
```
## 1. 新增 page driver
Page driver 不是必備層:若頁面邏輯只有簡單的 `computed` page model(無搜尋、無 dialog、無複雜事件協調),直接在 view 裡寫即可,不需要建立 page driver
頁面資料、事件與暫時 UI state 優先放在 page driverview 只負責掛載。
## 1. 新增 view(含 page model
```ts
// src/composables/page-drivers/useReportsPage.ts
簡單頁面的 page model 直接在 view 裡用 `computed` 組裝,不需要額外建立 page driver。
```vue
<!-- src/views/reports/Reports.vue -->
<script setup lang="ts">
import { computed, ref } from 'vue'
import PageReports from '@/components/pages/PageReports.vue'
import { useSnackbarStore } from '@/stores/snackbar'
export interface ReportSummary {
@@ -27,37 +31,29 @@ export interface ReportSummary {
owner: string
}
export interface ReportsPageModel {
title: string
rows: ReportSummary[]
}
const initialRows: ReportSummary[] = [
{ id: 1, title: '學生統計', owner: '教務處' },
{ id: 2, title: '課程統計', owner: '課務組' },
]
export function useReportsPage() {
const snackbar = useSnackbarStore()
const rows = ref<ReportSummary[]>(initialRows)
const snackbar = useSnackbarStore()
const rows = ref<ReportSummary[]>(initialRows)
const pageModel = computed(() => ({
title: '報表清單',
rows: rows.value,
}))
const pageModel = computed<ReportsPageModel>(() => ({
title: '報表清單',
rows: rows.value,
}))
function openReport(row: ReportSummary) {
snackbar.show({ message: `開啟:${row.title}`, color: 'info' })
}
return {
pageModel,
openReport,
}
function openReport(row: ReportSummary) {
snackbar.show({ message: `開啟:${row.title}`, color: 'info' })
}
</script>
<template>
<PageReports :page="pageModel" @open="openReport" />
</template>
```
資料來自 APIpage driver 可呼叫 store 或 composable;底層 HTTP 細節仍放在 `services/modules/*`
頁面需要協調多個 composable(搜尋、表單、CRUD flow、dialog 狀態),才建立 page driver。page driver 的慣例見 `src/composables/GUIDE.md`
## 2. 新增 page component
@@ -66,10 +62,10 @@ export function useReportsPage() {
```vue
<!-- src/components/pages/PageReports.vue -->
<script setup lang="ts">
import type { ReportSummary, ReportsPageModel } from '@/composables/page-drivers/useReportsPage'
import type { ReportSummary } from '@/views/reports/Reports.vue'
defineProps<{
page: ReportsPageModel
page: { title: string; rows: ReportSummary[] }
}>()
const emit = defineEmits<{
@@ -106,25 +102,7 @@ const emit = defineEmits<{
若畫面是固定的「篩選條件 + 查詢按鈕 + 結果表格」,優先使用 `components/sections/SectionQueryPage.vue`。若是「表單欄位 + 送出/存檔按鈕」,優先使用 `components/sections/SectionFormPage.vue`
## 3. 新增 route view
view 維持薄層,只呼叫 page driver 並掛載 page component。
```vue
<!-- src/views/reports/Reports.vue -->
<script setup lang="ts">
import PageReports from '@/components/pages/PageReports.vue'
import { useReportsPage } from '@/composables/page-drivers/useReportsPage'
const page = useReportsPage()
</script>
<template>
<PageReports :page="page.pageModel.value" @open="page.openReport" />
</template>
```
## 4. 加入 route
## 3. 加入 route
route 加在 `src/router/routes.ts``routes` 陣列中,並放在 `/:pathMatch(.*)*` catch-all route 前面。
@@ -147,7 +125,7 @@ route 加在 `src/router/routes.ts` 的 `routes` 陣列中,並放在 `/:pathMa
- 新功能若使用後端選單,優先調整後端選單資料或對應 API mock,不要把頁面專屬選單邏輯塞進 layout。
- 若只是新增 route,通常不需要修改 `MainLayout.vue``src/shell/*`
## 5. 需要 API 時新增 service module
## 4. 需要 API 時新增 service module
```ts
// src/services/modules/reports.ts
@@ -170,7 +148,7 @@ service 只封裝 HTTP 細節,不持有 UI 狀態。
`httpClient``baseURL` 來自 `VITE_API_BASE_URL`,沒有設定時預設 `/service/api`。開發模式下,Vite proxy 會將 `/service/*` 轉送到 `VITE_PROXY_TARGET`
## 6. 需要共享狀態時新增 store
## 5. 需要共享狀態時新增 store
只有跨頁共享、需要快取、或全域狀態才新增 store。單頁暫時狀態留在 view、component 或 composable。
@@ -202,7 +180,7 @@ export const useReportsStore = defineStore('reports', () => {
})
```
## 7. 驗證
## 6. 驗證
至少執行:
-65
View File
@@ -1,65 +0,0 @@
## 二、我們專案的現況診斷
本文件是 `docs/architecture-strategy.md` 第二章的現況快照。分層細節以 `docs/architecture-strategy.md``src/**/GUIDE.md` 為準。
### 2.1 App Shell 已拆分
`App.vue` 目前只掛載 `src/shell/AppShell.vue`,不再承擔 layout props、tabs、搜尋 dialog、訊息 dialog 或 snackbar 的具體組裝。
目前責任分布:
| 職責 | 目前位置 |
|------|----------|
| Layout 切換 | `src/shell/AppShell.vue` |
| Tabs / keep-alive router-view | `src/shell/AppTabs.vue` |
| Breadcrumb / favorites / menu wiring | `src/composables/layout/useAppShell.ts` + `AppShell.vue` |
| Search Dialog / Message Dialog / Snackbar | `src/shell/GlobalOverlays.vue` |
| Logout / force logout | `src/composables/layout/useAppShell.ts` |
| HTTP Toast | `src/services/http-toast.ts` + `GlobalOverlays.vue` |
### 2.2 Views 已大幅變薄
維護頁與一般頁面目前多數已轉為 route-level wiring
- `Home.vue`:呼叫 `useHomePage()`,掛載 `PageHome`
- `Settings.vue`:呼叫 `useSettingsPage()`,掛載 `PageSettings`
- `FncPage.vue`:呼叫 `useFunctionPage()`,掛載 `PageFunction`
- `views/maint/*`:呼叫對應 page driver,掛載 `components/pages/*Maintenance.vue`
`SingleRecord.vue` 已不再直接管理 store mutation、大型 dialog 模板、表格分頁與 CRUD 細節;這些流程已移到 page driver、section component、item component 與 command composable。
`Login.vue` 是 template core 例外,仍負責登入頁組合、功能開關、小型提示 dialog 與登入流程協調。登入頁的 captcha、announcement、忘記密碼與記住帳號流程已透過 composable / props / emits 拆分,後續調整應維持該模式。
### 2.3 Page Driver / Command / Page Component 已落地
目前已存在的主要分層:
```txt
view -> page driver -> page component -> section/item
command/store/service
```
- `src/composables/page-drivers/*`:組裝 page model、route/query 轉換與頁面事件。
- `src/composables/commands/useCrudCommands.ts`:承接維護頁 CRUD 命令流程。
- `src/components/pages/*`:完整頁面的主畫面組裝。
- `src/components/sections/*`:搜尋區、表格區、表單 dialog/panel、表單/查詢頁外殼。
- `src/components/items/*`:欄位群組或單筆資料呈現。
### 2.4 Dialog 與區塊拆分狀態
維護頁的大型 dialog 與表單欄位已從 view 抽出:
- `SectionFormPanel.vue`:維護頁表單 overlay/dialog shell。
- `MntDialogCard.vue``MntRecordNavToolbar.vue`:維護頁 dialog 內部骨架。
- `ItemFormFieldGroup.vue`:表單欄位群組。
新增頁面時,若只是小型提示 dialog 且只屬於單一路由,可先留在 page driver / page component。若 dialog 包含大型表單、確認流程或可重用骨架,優先抽到 section 或 feature component。
### 2.5 仍需注意的邊界
- `src/models/page.ts` 目前主要服務 maintenance page model;部分頁面仍在各自 page driver 內定義局部 page model 型別。
- `components/maint/*` 與 maintenance page components 屬於 demo / maintenance 領域,不應直接升格為全域 base 元件。
- `src/components/base` 目前只放跨頁共用基礎元件,例如 `DraggableDialog``BaseFormTextField``BaseFormSelect`
- `src/stores/app.ts` 仍是 Pinia scaffold,尚未承擔實際 app state。
- 一般功能需求不應修改 `App.vue``src/shell/*`、layout、router guard 或 HTTP core,除非需求明確牽涉這些 template core。
+22 -29
View File
@@ -67,23 +67,15 @@ Read only when needed: [analyse now](./analyse-now.md)
範例:
```ts
// 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 }
}
// 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)分離
@@ -164,8 +156,8 @@ src/
│ └── DraggableDialog.vue
├── composables/
│ ├── page-drivers/ ← 新增:頁面資料協調
│ │ └── useMaintenancePage.ts
│ ├── page-drivers/ ← 新增:頁面資料協調(僅複雜頁面需要)
│ │ └── useSingleRecordMaintenancePage.ts
│ ├── commands/ ← 新增:命令流程(對齊 Jet Action)
│ │ └── useCrudCommands.ts
│ ├── forms/ ← 維持/重組:表單狀態機
@@ -201,14 +193,10 @@ src/
```vue
<!-- views/maint/SingleRecord.vue優化後 -->
<script setup lang="ts">
import { useMaintenancePage } from '@/composables/page-drivers/useMaintenancePage'
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
import { useSingleRecordMaintenancePage } from '@/composables/page-drivers/useSingleRecordMaintenancePage'
const { pageModel, load } = useMaintenancePage({
title: '單筆資料維護',
records: [],
})
load()
const { pageModel, commands, formPanelProps, formPanelEvents } = useSingleRecordMaintenancePage()
</script>
<template>
@@ -356,9 +344,10 @@ views/xxx.vue
3. [x] 新增 `src/components/pages/`:建立第一個 `PageMaintenance.vue`(可從 `PageMaint.vue` 擴展)。
- 定義 `MaintenancePageModel` props 與 `create/edit/view/delete/search` emits。
- 使用 `PageMaint.vue` 作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
4. [x] 新增 `src/composables/page-drivers/`:建立 `useMaintenancePage.ts`
- 透過 options 傳入 title 與 records協調搜尋條件、分頁與 `pageModel`
- 提供 `load()``resetSearch()` 供 Page Driver 呼叫。
4. [x] 新增 `src/composables/page-drivers/`:建立第一個 page driver 範例
- 協調搜尋條件、分頁與 `pageModel`
- 提供 `load()``resetSearch()` 供 Page Driver 呼叫。
- 後續已刪除純包裝型 driver(如 `useMaintenancePage`)。僅當頁面需要協調多個 composable 時才建立 page driver。
### Phase 2:遷移最厚的 viewSingleRecord.vue ✅ 已完成
@@ -382,6 +371,8 @@ views/xxx.vue
### 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`
@@ -399,6 +390,8 @@ views/xxx.vue
### 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`
@@ -421,7 +414,7 @@ views/xxx.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` |
| 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`(維持) |
-81
View File
@@ -1,81 +0,0 @@
## 一、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。
---