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:
@@ -20,7 +20,7 @@
|
|||||||
- `src/services/modules/<domain>.ts` — service modules
|
- `src/services/modules/<domain>.ts` — service modules
|
||||||
- Examples of correct vs. incorrect naming:
|
- Examples of correct vs. incorrect naming:
|
||||||
- ❌ `PageStudentMaintenance.vue` → ✅ `PageMaintenance.vue`
|
- ❌ `PageStudentMaintenance.vue` → ✅ `PageMaintenance.vue`
|
||||||
- ❌ `useStudentMaintenancePage.ts` → ✅ `useMaintenancePage.ts`
|
- ❌ `useStudentMaintenancePage.ts` → ✅ `useSingleRecordMaintenancePage.ts`
|
||||||
- ❌ `ItemStudentRow.vue` → ✅ `ItemDataRow.vue`
|
- ❌ `ItemStudentRow.vue` → ✅ `ItemDataRow.vue`
|
||||||
- ❌ `useStudentCrudCommands.ts` → ✅ `useCrudCommands.ts`
|
- ❌ `useStudentCrudCommands.ts` → ✅ `useCrudCommands.ts`
|
||||||
- ✅ `models/student.ts`, `stores/students.ts` — domain layer, specific names are correct
|
- ✅ `models/student.ts`, `stores/students.ts` — domain layer, specific names are correct
|
||||||
|
|||||||
+26
-48
@@ -7,18 +7,22 @@
|
|||||||
目前新增一般頁面的預設資料流:
|
目前新增一般頁面的預設資料流:
|
||||||
|
|
||||||
```txt
|
```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 driver,view 只負責掛載。
|
## 1. 新增 view(含 page model)
|
||||||
|
|
||||||
```ts
|
簡單頁面的 page model 直接在 view 裡用 `computed` 組裝,不需要額外建立 page driver。
|
||||||
// src/composables/page-drivers/useReportsPage.ts
|
|
||||||
|
```vue
|
||||||
|
<!-- src/views/reports/Reports.vue -->
|
||||||
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import PageReports from '@/components/pages/PageReports.vue'
|
||||||
import { useSnackbarStore } from '@/stores/snackbar'
|
import { useSnackbarStore } from '@/stores/snackbar'
|
||||||
|
|
||||||
export interface ReportSummary {
|
export interface ReportSummary {
|
||||||
@@ -27,37 +31,29 @@ export interface ReportSummary {
|
|||||||
owner: string
|
owner: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReportsPageModel {
|
|
||||||
title: string
|
|
||||||
rows: ReportSummary[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialRows: ReportSummary[] = [
|
const initialRows: ReportSummary[] = [
|
||||||
{ id: 1, title: '學生統計', owner: '教務處' },
|
{ id: 1, title: '學生統計', owner: '教務處' },
|
||||||
{ id: 2, title: '課程統計', owner: '課務組' },
|
{ id: 2, title: '課程統計', owner: '課務組' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function useReportsPage() {
|
const snackbar = useSnackbarStore()
|
||||||
const snackbar = useSnackbarStore()
|
const rows = ref<ReportSummary[]>(initialRows)
|
||||||
const rows = ref<ReportSummary[]>(initialRows)
|
const pageModel = computed(() => ({
|
||||||
|
|
||||||
const pageModel = computed<ReportsPageModel>(() => ({
|
|
||||||
title: '報表清單',
|
title: '報表清單',
|
||||||
rows: rows.value,
|
rows: rows.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function openReport(row: ReportSummary) {
|
function openReport(row: ReportSummary) {
|
||||||
snackbar.show({ message: `開啟:${row.title}`, color: 'info' })
|
snackbar.show({ message: `開啟:${row.title}`, color: 'info' })
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
pageModel,
|
|
||||||
openReport,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageReports :page="pageModel" @open="openReport" />
|
||||||
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
若資料來自 API,page driver 可呼叫 store 或 composable;底層 HTTP 細節仍放在 `services/modules/*`。
|
若頁面需要協調多個 composable(搜尋、表單、CRUD flow、dialog 狀態),才建立 page driver。page driver 的慣例見 `src/composables/GUIDE.md`。
|
||||||
|
|
||||||
## 2. 新增 page component
|
## 2. 新增 page component
|
||||||
|
|
||||||
@@ -66,10 +62,10 @@ export function useReportsPage() {
|
|||||||
```vue
|
```vue
|
||||||
<!-- src/components/pages/PageReports.vue -->
|
<!-- src/components/pages/PageReports.vue -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReportSummary, ReportsPageModel } from '@/composables/page-drivers/useReportsPage'
|
import type { ReportSummary } from '@/views/reports/Reports.vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
page: ReportsPageModel
|
page: { title: string; rows: ReportSummary[] }
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -106,25 +102,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
若畫面是固定的「篩選條件 + 查詢按鈕 + 結果表格」,優先使用 `components/sections/SectionQueryPage.vue`。若是「表單欄位 + 送出/存檔按鈕」,優先使用 `components/sections/SectionFormPage.vue`。
|
若畫面是固定的「篩選條件 + 查詢按鈕 + 結果表格」,優先使用 `components/sections/SectionQueryPage.vue`。若是「表單欄位 + 送出/存檔按鈕」,優先使用 `components/sections/SectionFormPage.vue`。
|
||||||
|
|
||||||
## 3. 新增 route view
|
## 3. 加入 route
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
route 加在 `src/router/routes.ts` 的 `routes` 陣列中,並放在 `/:pathMatch(.*)*` catch-all 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。
|
- 新功能若使用後端選單,優先調整後端選單資料或對應 API mock,不要把頁面專屬選單邏輯塞進 layout。
|
||||||
- 若只是新增 route,通常不需要修改 `MainLayout.vue` 或 `src/shell/*`。
|
- 若只是新增 route,通常不需要修改 `MainLayout.vue` 或 `src/shell/*`。
|
||||||
|
|
||||||
## 5. 需要 API 時新增 service module
|
## 4. 需要 API 時新增 service module
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// src/services/modules/reports.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`。
|
`httpClient` 的 `baseURL` 來自 `VITE_API_BASE_URL`,沒有設定時預設 `/service/api`。開發模式下,Vite proxy 會將 `/service/*` 轉送到 `VITE_PROXY_TARGET`。
|
||||||
|
|
||||||
## 6. 需要共享狀態時新增 store
|
## 5. 需要共享狀態時新增 store
|
||||||
|
|
||||||
只有跨頁共享、需要快取、或全域狀態才新增 store。單頁暫時狀態留在 view、component 或 composable。
|
只有跨頁共享、需要快取、或全域狀態才新增 store。單頁暫時狀態留在 view、component 或 composable。
|
||||||
|
|
||||||
@@ -202,7 +180,7 @@ export const useReportsStore = defineStore('reports', () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. 驗證
|
## 6. 驗證
|
||||||
|
|
||||||
至少執行:
|
至少執行:
|
||||||
|
|
||||||
|
|||||||
@@ -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。
|
|
||||||
@@ -67,23 +67,15 @@ Read only when needed: [analyse now](./analyse-now.md)
|
|||||||
範例:
|
範例:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// src/composables/usePageDriver.ts
|
// views/maint/Example.vue — 簡單頁面直接在 view 組裝 page model
|
||||||
export function useMaintenancePage() {
|
const studentStore = useStudentStore()
|
||||||
const studentStore = useStudentStore()
|
const pageModel = computed<MaintenancePageModel>(() => ({
|
||||||
const { records, loading, error, load } = useCrudDriver({
|
type: 'maintenance',
|
||||||
store: studentStore,
|
|
||||||
loadAction: () => studentStore.fetchStudents(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const pageModel = computed(() => ({
|
|
||||||
title: '單筆資料維護',
|
title: '單筆資料維護',
|
||||||
records: records.value,
|
records: studentStore.students,
|
||||||
loading: loading.value,
|
loading: false,
|
||||||
error: error.value,
|
error: null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return { pageModel, load }
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3 查詢(Query)與命令(Command)分離
|
### 3.3 查詢(Query)與命令(Command)分離
|
||||||
@@ -164,8 +156,8 @@ src/
|
|||||||
│ └── DraggableDialog.vue
|
│ └── DraggableDialog.vue
|
||||||
│
|
│
|
||||||
├── composables/
|
├── composables/
|
||||||
│ ├── page-drivers/ ← 新增:頁面資料協調
|
│ ├── page-drivers/ ← 新增:頁面資料協調(僅複雜頁面需要)
|
||||||
│ │ └── useMaintenancePage.ts
|
│ │ └── useSingleRecordMaintenancePage.ts
|
||||||
│ ├── commands/ ← 新增:命令流程(對齊 Jet Action)
|
│ ├── commands/ ← 新增:命令流程(對齊 Jet Action)
|
||||||
│ │ └── useCrudCommands.ts
|
│ │ └── useCrudCommands.ts
|
||||||
│ ├── forms/ ← 維持/重組:表單狀態機
|
│ ├── forms/ ← 維持/重組:表單狀態機
|
||||||
@@ -201,14 +193,10 @@ src/
|
|||||||
```vue
|
```vue
|
||||||
<!-- views/maint/SingleRecord.vue(優化後) -->
|
<!-- views/maint/SingleRecord.vue(優化後) -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMaintenancePage } from '@/composables/page-drivers/useMaintenancePage'
|
|
||||||
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
|
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
|
||||||
|
import { useSingleRecordMaintenancePage } from '@/composables/page-drivers/useSingleRecordMaintenancePage'
|
||||||
|
|
||||||
const { pageModel, load } = useMaintenancePage({
|
const { pageModel, commands, formPanelProps, formPanelEvents } = useSingleRecordMaintenancePage()
|
||||||
title: '單筆資料維護',
|
|
||||||
records: [],
|
|
||||||
})
|
|
||||||
load()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -356,9 +344,10 @@ views/xxx.vue
|
|||||||
3. [x] 新增 `src/components/pages/`:建立第一個 `PageMaintenance.vue`(可從 `PageMaint.vue` 擴展)。
|
3. [x] 新增 `src/components/pages/`:建立第一個 `PageMaintenance.vue`(可從 `PageMaint.vue` 擴展)。
|
||||||
- 定義 `MaintenancePageModel` props 與 `create/edit/view/delete/search` emits。
|
- 定義 `MaintenancePageModel` props 與 `create/edit/view/delete/search` emits。
|
||||||
- 使用 `PageMaint.vue` 作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
|
- 使用 `PageMaint.vue` 作為佈局外殼,搜尋與表格區塊以 slot 開放,不綁定特定領域型別。
|
||||||
4. [x] 新增 `src/composables/page-drivers/`:建立 `useMaintenancePage.ts`。
|
4. [x] 新增 `src/composables/page-drivers/`:建立第一個 page driver 範例。
|
||||||
- 透過 options 傳入 title 與 records,協調搜尋條件、分頁與 `pageModel`。
|
- 協調搜尋條件、分頁與 `pageModel`。
|
||||||
- 提供 `load()` 與 `resetSearch()` 供 Page Driver 呼叫。
|
- 提供 `load()` 與 `resetSearch()` 供 Page Driver 呼叫。
|
||||||
|
- 後續已刪除純包裝型 driver(如 `useMaintenancePage`)。僅當頁面需要協調多個 composable 時才建立 page driver。
|
||||||
|
|
||||||
### Phase 2:遷移最厚的 view(SingleRecord.vue) ✅ 已完成
|
### Phase 2:遷移最厚的 view(SingleRecord.vue) ✅ 已完成
|
||||||
|
|
||||||
@@ -382,6 +371,8 @@ views/xxx.vue
|
|||||||
|
|
||||||
### Phase 3:推廣到所有 maintenance 頁面 ✅ 已完成
|
### Phase 3:推廣到所有 maintenance 頁面 ✅ 已完成
|
||||||
|
|
||||||
|
> 後續簡化時,B/C/EditableGrid 的薄 page driver 已 inline 回 view,只保留有真實複雜邏輯的 driver。
|
||||||
|
|
||||||
1. [x] `EditableGrid.vue` 依 Page Driver + Page Component 模式重構。
|
1. [x] `EditableGrid.vue` 依 Page Driver + Page Component 模式重構。
|
||||||
- `src/views/maint/EditableGrid.vue` 縮減為 10 行 route-level wiring。
|
- `src/views/maint/EditableGrid.vue` 縮減為 10 行 route-level wiring。
|
||||||
- 新增 `src/composables/page-drivers/useEditableGridMaintenancePage.ts`。
|
- 新增 `src/composables/page-drivers/useEditableGridMaintenancePage.ts`。
|
||||||
@@ -399,6 +390,8 @@ views/xxx.vue
|
|||||||
|
|
||||||
### Phase 4:非 maintenance 頁面統一 ✅ 已完成
|
### Phase 4:非 maintenance 頁面統一 ✅ 已完成
|
||||||
|
|
||||||
|
> 後續簡化時,Settings/FncPage 的薄 page driver 已 inline 回 view,型別移至 page component 自身。
|
||||||
|
|
||||||
1. [x] `Home.vue`、`Settings.vue`、`FncPage.vue` 套用 Page Driver + 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/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/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` |
|
| Item / Atom | `src/components/items/` | `ItemDataRow.vue`、`ItemFormField.vue` |
|
||||||
| Layout | `src/components/layouts/` | `MainLayout.vue`(維持) |
|
| Layout | `src/components/layouts/` | `MainLayout.vue`(維持) |
|
||||||
| Base | `src/components/base/` | `DraggableDialog.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` |
|
| Command Composable | `src/composables/commands/` | `useCrudCommands.ts` |
|
||||||
| Form Composable | `src/composables/forms/` | `useForm.ts` |
|
| Form Composable | `src/composables/forms/` | `useForm.ts` |
|
||||||
| Domain Store | `src/stores/` | `students.ts`(維持) |
|
| Domain Store | `src/stores/` | `students.ts`(維持) |
|
||||||
|
|||||||
@@ -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 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。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
+1
-1
@@ -76,7 +76,7 @@ router -> AppShell -> layout -> view(Page Driver) -> Page Component -> Section -
|
|||||||
|
|
||||||
1. 新增或修改 `views/*` route entry。
|
1. 新增或修改 `views/*` route entry。
|
||||||
2. 若有完整頁面 UI,新增 `components/pages/PageXxx.vue`。
|
2. 若有完整頁面 UI,新增 `components/pages/PageXxx.vue`。
|
||||||
3. 若有頁面資料協調或 route param 轉換,新增 `composables/page-drivers/useXxxPage.ts`。
|
3. 若有複雜的資料協調(多 composable、搜尋狀態、CRUD flow、dialog 狀態),新增 `composables/page-drivers/useXxxPage.ts`。簡單頁面直接在 view 用 `computed` 組裝 page model。
|
||||||
4. 若畫面有獨立區塊,拆到 `components/sections/*`。
|
4. 若畫面有獨立區塊,拆到 `components/sections/*`。
|
||||||
5. 若區塊內有欄位群組或單筆資料呈現,拆到 `components/items/*`。
|
5. 若區塊內有欄位群組或單筆資料呈現,拆到 `components/items/*`。
|
||||||
6. 跨頁共享狀態才新增或修改 `stores/*`。
|
6. 跨頁共享狀態才新增或修改 `stores/*`。
|
||||||
|
|||||||
+1
-1
@@ -23,4 +23,4 @@
|
|||||||
- 各頁面的 specific model 擴展 `BasePageModel`(例如 `MaintenancePageModel` 加 `type`、`records`)。
|
- 各頁面的 specific model 擴展 `BasePageModel`(例如 `MaintenancePageModel` 加 `type`、`records`)。
|
||||||
- `PageModel` union 供 page component props 型別使用。
|
- `PageModel` union 供 page component props 型別使用。
|
||||||
|
|
||||||
新增頁面類型時,先擴充 `PageModel` union 再新增對應的 page driver。
|
新增頁面類型時,先擴充 `PageModel` union。若頁面需要協調多個 composable(搜尋、表單、CRUD flow、dialog 狀態),再建立對應的 page driver;簡單頁面直接在 view 用 `computed` 組裝 page model 即可。
|
||||||
|
|||||||
+4
-2
@@ -18,14 +18,16 @@
|
|||||||
import PageReports from '@/components/pages/PageReports.vue'
|
import PageReports from '@/components/pages/PageReports.vue'
|
||||||
import { useReportsPage } from '@/composables/page-drivers/useReportsPage'
|
import { useReportsPage } from '@/composables/page-drivers/useReportsPage'
|
||||||
|
|
||||||
const page = useReportsPage()
|
const { pageModel } = useReportsPage()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageReports :page="page.pageModel.value" />
|
<PageReports :page="pageModel" />
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
若頁面只是簡單的 `computed` 組裝(無搜尋、無 dialog、無複雜事件),直接在 view 寫 page model,不需要建立 page driver。以 destructure 方式取用 composable 回傳值,模板不寫 `.value`。
|
||||||
|
|
||||||
## Login.vue 開關
|
## Login.vue 開關
|
||||||
|
|
||||||
`Login.vue` 是登入頁的組合層,登入頁功能開關集中在 view 內宣告,再透過 `PageLogin` / composable 往下傳遞,不在子元件各自決定是否啟用。
|
`Login.vue` 是登入頁的組合層,登入頁功能開關集中在 view 內宣告,再透過 `PageLogin` / composable 往下傳遞,不在子元件各自決定是否啟用。
|
||||||
|
|||||||
Reference in New Issue
Block a user