refactor: simplify page models and view driver usage

Move simple page models into page components and build trivial computed
models directly in views to avoid unnecessary page drivers. Update views
to destructure page driver returns and rely on template ref unwrapping,
and document the guidance for when page drivers should be introduced.refactor: simplify page models and view driver usage

Move simple page models into page components and build trivial computed
models directly in views to avoid unnecessary page drivers. Update views
to destructure page driver returns and rely on template ref unwrapping,
and document the guidance for when page drivers should be introduced.
This commit is contained in:
skytek_xinliang
2026-05-27 11:10:34 +08:00
parent 799b16578d
commit b8664b5c3e
19 changed files with 139 additions and 220 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
<script setup lang="ts">
import type { FunctionPageModel } from '@/composables/page-drivers/useFunctionPage'
export interface FunctionPageModel {
fncId: string
}
defineProps<{
page: FunctionPageModel
+3 -1
View File
@@ -1,5 +1,7 @@
<script setup lang="ts">
import type { SettingsPageModel } from '@/composables/page-drivers/useSettingsPage'
export interface SettingsPageModel {
title: string
}
defineProps<{
page: SettingsPageModel
+13 -4
View File
@@ -22,14 +22,23 @@
## Page Driver
Page driver 負責
Page driver 只應在「需要協調多個 composable / store / route」時才成立。若頁面邏輯只有
- 組裝一個 `computed` page model3-5 個欄位)
- 沒有搜尋、沒有 dialog、沒有複雜事件
則**不要建立 page driver**,直接在 view 裡寫 `computed` 即可。
當需要 page driver 時,它負責:
- route param/query 轉成頁面資料
- 組裝 page model
- 組裝 page component 需要的 props/events
- 協調 store、command composable、表單 composable
- 組裝 page component 需要的 props/events
View 只呼叫 page driver 並掛載 page component。
View 以 destructure 方式取用 page driver 回傳值:
```ts
const { pageModel, search, handleSubmit } = useXxxPage()
```
模板中直接使用,不寫 `.value``:page="pageModel"``v-model="search"`
## Commands
@@ -1,19 +0,0 @@
import { computed } from 'vue'
import type { MaintenancePageModel } from '@/models/page'
import { useStudentStore } from '@/stores/students'
export function useEditableGridMaintenancePage() {
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '可編輯表格維護示範',
records: studentStore.students,
loading: false,
error: null,
}))
return {
pageModel,
}
}
@@ -1,18 +0,0 @@
import { computed } from 'vue'
import { useRoute } from 'vue-router'
export interface FunctionPageModel {
fncId: string
}
export function useFunctionPage() {
const route = useRoute()
const pageModel = computed<FunctionPageModel>(() => ({
fncId: String(route.params.fncId ?? ''),
}))
return {
pageModel,
}
}
@@ -1,46 +0,0 @@
import { computed, ref } from 'vue'
import type { MaintenancePageModel } from '@/models/page'
export interface UseMaintenancePageOptions {
title: string
records: unknown[]
itemsPerPage?: number
}
export function useMaintenancePage(options: UseMaintenancePageOptions) {
const search = ref<Record<string, unknown>>({})
const searchPanelOpen = ref(false)
const currentPage = ref(1)
const itemsPerPage = options.itemsPerPage ?? 10
const pageCount = computed(() =>
Math.max(1, Math.ceil(options.records.length / itemsPerPage))
)
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: options.title,
records: options.records,
loading: false,
error: null,
}))
function load() {
// 由呼叫方在 load 中觸發資料載入;未來可擴充為非同步
}
function resetSearch() {
search.value = {}
}
return {
pageModel,
search,
searchPanelOpen,
currentPage,
itemsPerPage,
pageCount,
load,
resetSearch,
}
}
@@ -1,20 +0,0 @@
import { computed } from 'vue'
import type { MaintenancePageModel } from '@/models/page'
import { useStudentStore } from '@/stores/students'
export function useMasterDetailBMaintenancePage() {
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '主從資料維護示範B',
records: studentStore.students,
loading: false,
error: null,
}))
return {
pageModel,
}
}
@@ -1,20 +0,0 @@
import { computed } from 'vue'
import type { MaintenancePageModel } from '@/models/page'
import { useStudentStore } from '@/stores/students'
export function useMasterDetailCMaintenancePage() {
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '主從資料維護示範C',
records: studentStore.students,
loading: false,
error: null,
}))
return {
pageModel,
}
}
@@ -1,13 +0,0 @@
import { computed } from 'vue'
export interface SettingsPageModel {
title: string
}
export function useSettingsPage() {
const pageModel = computed<SettingsPageModel>(() => ({
title: '設定頁面',
}))
return { pageModel }
}
+7 -2
View File
@@ -1,8 +1,13 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import PageFunction from '@/components/pages/PageFunction.vue'
import { useFunctionPage } from '@/composables/page-drivers/useFunctionPage'
import type { FunctionPageModel } from '@/components/pages/PageFunction.vue'
const { pageModel } = useFunctionPage()
const route = useRoute()
const pageModel = computed<FunctionPageModel>(() => ({
fncId: String(route.params.fncId ?? ''),
}))
</script>
<template>
+7 -7
View File
@@ -2,16 +2,16 @@
import PageHome from '@/components/pages/PageHome.vue'
import { useHomePage } from '@/composables/page-drivers/useHomePage'
const page = useHomePage()
const { handleMessageCenter, handleNews, handleQuick, isNewsDialogOpen, pageModel, selectedNews } = useHomePage()
</script>
<template>
<PageHome
v-model:news-dialog-open="page.isNewsDialogOpen.value"
:page="page.pageModel.value"
:selected-news="page.selectedNews.value"
@message-center="page.handleMessageCenter"
@news="page.handleNews"
@quick="page.handleQuick"
v-model:news-dialog-open="isNewsDialogOpen"
:page="pageModel"
:selected-news="selectedNews"
@message-center="handleMessageCenter"
@news="handleNews"
@quick="handleQuick"
/>
</template>
+5 -2
View File
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue'
import PageSettings from '@/components/pages/PageSettings.vue'
import { useSettingsPage } from '@/composables/page-drivers/useSettingsPage'
import type { SettingsPageModel } from '@/components/pages/PageSettings.vue'
const { pageModel } = useSettingsPage()
const pageModel = computed<SettingsPageModel>(() => ({
title: '設定頁面',
}))
</script>
<template>
+6 -7
View File
@@ -2,16 +2,15 @@
import PageSectionFormPageDemo from '@/components/pages/PageSectionFormPageDemo.vue'
import { useSectionsDemoPage } from '@/composables/page-drivers/useSectionsDemoPage'
// Demo view 維持薄層,只掛 page driver,並把 page model / actions 傳給 page component。
const page = useSectionsDemoPage()
const { demoForm, handleFormBack, handleFormSubmit, pageModel, resetDemoForm } = useSectionsDemoPage()
</script>
<template>
<PageSectionFormPageDemo
v-model:demo-form="page.demoForm.value"
:page="page.pageModel.value"
@back="page.handleFormBack"
@reset="page.resetDemoForm"
@submit="page.handleFormSubmit"
v-model:demo-form="demoForm"
:page="pageModel"
@back="handleFormBack"
@reset="resetDemoForm"
@submit="handleFormSubmit"
/>
</template>
+5 -6
View File
@@ -2,15 +2,14 @@
import PageSectionQueryPageDemo from '@/components/pages/PageSectionQueryPageDemo.vue'
import { useSectionsDemoPage } from '@/composables/page-drivers/useSectionsDemoPage'
// Demo view 維持薄層,只掛 page driver,並把 page model / actions 傳給 page component。
const page = useSectionsDemoPage()
const { handleQueryBack, handleQuerySearch, pageModel, queryFilters } = useSectionsDemoPage()
</script>
<template>
<PageSectionQueryPageDemo
v-model:query-filters="page.queryFilters.value"
:page="page.pageModel.value"
@back="page.handleQueryBack"
@search="page.handleQuerySearch"
v-model:query-filters="queryFilters"
:page="pageModel"
@back="handleQueryBack"
@search="handleQuerySearch"
/>
</template>
+12 -3
View File
@@ -1,10 +1,19 @@
<script setup lang="ts">
import { computed } from 'vue'
import PageEditableGridMaintenance from '@/components/pages/PageEditableGridMaintenance.vue'
import { useEditableGridMaintenancePage } from '@/composables/page-drivers/useEditableGridMaintenancePage'
import type { MaintenancePageModel } from '@/models/page'
import { useStudentStore } from '@/stores/students'
const page = useEditableGridMaintenancePage()
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '可編輯表格維護示範',
records: studentStore.students,
loading: false,
error: null,
}))
</script>
<template>
<PageEditableGridMaintenance :page="page.pageModel.value" />
<PageEditableGridMaintenance :page="pageModel" />
</template>
+25 -21
View File
@@ -2,33 +2,37 @@
import PageMasterDetailAMaintenance from '@/components/pages/PageMasterDetailAMaintenance.vue'
import { useMasterDetailAMaintenancePage } from '@/composables/page-drivers/useMasterDetailAMaintenancePage'
const page = useMasterDetailAMaintenancePage()
const {
currentPage, formState, itemsPerPage, masterDetailEvents, masterDetailProps,
openAddDialog, openEditDialog, openViewDialog, pageCount, pageModel, pageSummary,
resetSearch, search, searchPanelOpen, snackbarVisible, students, tableHeaders,
} = useMasterDetailAMaintenancePage()
</script>
<template>
<PageMasterDetailAMaintenance
v-model:search="page.search.value"
v-model:search-panel-open="page.searchPanelOpen.value"
v-bind="page.masterDetailProps.value"
:current-page="page.currentPage.value"
:grade-label="page.formState.gradeLabel"
:headers="page.tableHeaders.value"
:items="page.students.value"
:items-per-page="page.itemsPerPage"
:page="page.pageModel.value"
:page-count="page.pageCount.value"
:page-summary="page.pageSummary.value"
:row-props="page.formState.rowProps"
:status-color="page.formState.statusColor"
@create="page.openAddDialog"
@edit="page.openEditDialog"
@reset-search="page.resetSearch"
@update:current-page="page.currentPage.value = $event"
@view="page.openViewDialog"
v-on="page.masterDetailEvents"
v-model:search="search"
v-model:search-panel-open="searchPanelOpen"
v-bind="masterDetailProps"
:current-page="currentPage"
:grade-label="formState.gradeLabel"
:headers="tableHeaders"
:items="students"
:items-per-page="itemsPerPage"
:page="pageModel"
:page-count="pageCount"
:page-summary="pageSummary"
:row-props="formState.rowProps"
:status-color="formState.statusColor"
@create="openAddDialog"
@edit="openEditDialog"
@reset-search="resetSearch"
@update:current-page="currentPage = $event"
@view="openViewDialog"
v-on="masterDetailEvents"
/>
<v-snackbar v-model="page.snackbarVisible.value" color="success" location="bottom right" :timeout="2200">
<v-snackbar v-model="snackbarVisible" color="success" location="bottom right" :timeout="2200">
儲存成功
</v-snackbar>
</template>
+12 -3
View File
@@ -1,10 +1,19 @@
<script setup lang="ts">
import { computed } from 'vue'
import PageMasterDetailBMaintenance from '@/components/pages/PageMasterDetailBMaintenance.vue'
import { useMasterDetailBMaintenancePage } from '@/composables/page-drivers/useMasterDetailBMaintenancePage'
import type { MaintenancePageModel } from '@/models/page'
import { useStudentStore } from '@/stores/students'
const page = useMasterDetailBMaintenancePage()
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '主從資料維護示範B',
records: studentStore.students,
loading: false,
error: null,
}))
</script>
<template>
<PageMasterDetailBMaintenance :page="page.pageModel.value" />
<PageMasterDetailBMaintenance :page="pageModel" />
</template>
+12 -3
View File
@@ -1,10 +1,19 @@
<script setup lang="ts">
import { computed } from 'vue'
import PageMasterDetailCMaintenance from '@/components/pages/PageMasterDetailCMaintenance.vue'
import { useMasterDetailCMaintenancePage } from '@/composables/page-drivers/useMasterDetailCMaintenancePage'
import type { MaintenancePageModel } from '@/models/page'
import { useStudentStore } from '@/stores/students'
const page = useMasterDetailCMaintenancePage()
const studentStore = useStudentStore()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '主從資料維護示範C',
records: studentStore.students,
loading: false,
error: null,
}))
</script>
<template>
<PageMasterDetailCMaintenance :page="page.pageModel.value" />
<PageMasterDetailCMaintenance :page="pageModel" />
</template>
+29 -24
View File
@@ -5,48 +5,53 @@ import SectionFormPanel from '@/components/sections/SectionFormPanel.vue'
import SectionSearchPanel from '@/components/sections/SectionSearchPanel.vue'
import { useSingleRecordMaintenancePage } from '@/composables/page-drivers/useSingleRecordMaintenancePage'
const page = useSingleRecordMaintenancePage()
const {
commands, currentPage, departments, flow, formPanelEvents, formPanelProps,
formState, gradeOptions, itemsPerPage, pageCount, pageModel, pageSummary,
resetSearch, search, searchPanelOpen, snackbarVisible,
statuses, students, tableHeaders,
} = useSingleRecordMaintenancePage()
</script>
<template>
<PageMaintenance
v-model:search-panel-open="page.searchPanelOpen.value"
:page="page.pageModel.value"
@create="page.commands.openAddDialog"
v-model:search-panel-open="searchPanelOpen"
:page="pageModel"
@create="commands.openAddDialog"
>
<template #search-fields>
<SectionSearchPanel
v-model="page.search.value"
:departments="page.departments"
:grade-options="page.gradeOptions"
:statuses="page.statuses"
@reset="page.resetSearch"
v-model="search"
:departments="departments"
:grade-options="gradeOptions"
:statuses="statuses"
@reset="resetSearch"
/>
</template>
<template #table>
<SectionDataTable
v-model:current-page="page.currentPage.value"
:grade-label="page.formState.gradeLabel"
:headers="page.tableHeaders.value"
:items="page.students.value"
:items-per-page="page.itemsPerPage"
:page-count="page.pageCount.value"
:page-summary="page.pageSummary.value"
:row-props="page.formState.rowProps"
:status-color="page.formState.statusColor"
@delete="page.flow.requestDeleteConfirmation"
@edit="page.commands.openEditDialog"
@view="page.commands.openViewDialog"
v-model:current-page="currentPage"
:grade-label="formState.gradeLabel"
:headers="tableHeaders"
:items="students"
:items-per-page="itemsPerPage"
:page-count="pageCount"
:page-summary="pageSummary"
:row-props="formState.rowProps"
:status-color="formState.statusColor"
@delete="flow.requestDeleteConfirmation"
@edit="commands.openEditDialog"
@view="commands.openViewDialog"
/>
</template>
</PageMaintenance>
<SectionFormPanel
v-bind="page.formPanelProps.value"
v-on="page.formPanelEvents"
v-bind="formPanelProps"
v-on="formPanelEvents"
/>
<v-snackbar v-model="page.snackbarVisible.value" color="success" location="bottom right" :timeout="2200">
<v-snackbar v-model="snackbarVisible" color="success" location="bottom right" :timeout="2200">
儲存成功
</v-snackbar>
</template>