docs: simplify page architecture and component guidance

Update the src documentation to emphasize building pages from route views,
composables, sections, and items instead of a dedicated pages layer.

Clarify the recommended data flow and new feature workflow so template users
start from views and only introduce page-driver composables when coordination
logic becomes complex.docs: simplify page architecture and component guidance

Update the src documentation to emphasize building pages from route views,
composables, sections, and items instead of a dedicated pages layer.

Clarify the recommended data flow and new feature workflow so template users
start from views and only introduce page-driver composables when coordination
logic becomes complex.
This commit is contained in:
skytek_xinliang
2026-05-27 11:50:40 +08:00
parent ad00f5c195
commit 7b99087cbb
25 changed files with 2797 additions and 3174 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import PageEditableGridMaintenance from '@/components/pages/PageEditableGridMaintenance.vue'
import EditableStudentGrid from '@/components/maint/EditableGrid.vue'
import type { MaintenancePageModel } from '@/models/page'
import { useStudentStore } from '@/stores/students'
@@ -15,5 +15,5 @@ const pageModel = computed<MaintenancePageModel>(() => ({
</script>
<template>
<PageEditableGridMaintenance :page="pageModel" />
<EditableStudentGrid :title="pageModel.title" />
</template>
+3 -5
View File
@@ -1,12 +1,12 @@
# Maintenance Views Guide
`views/maint` 是維護頁 demo。所有檔案都應是薄 route entry實際 UI 與流程分別放在 `components/pages``components/sections``components/items``composables/page-drivers`
`views/maint` 是維護頁 demo。所有檔案都是自含的 route entryUI 與流程直接在 view 中組合 `PageMaint``components/sections``components/items` 與 composable。
## 目前範本
- `SingleRecord.vue`:單主檔 CRUD + dialog。
- `SingleRecord.vue`:單主檔 CRUD + dialog(使用 page driver composable
- `EditableGrid.vue`:可編輯表格。
- `MasterDetailA.vue`:主檔 + 側邊明細 panel。
- `MasterDetailA.vue`:主檔 + 側邊明細 panel(使用 page driver composable
- `MasterDetailB.vue`:主檔 + collapse / full-height 明細。
- `MasterDetailC.vue`:主檔 + 簡化明細清單。
@@ -15,8 +15,6 @@
複製維護頁時同步調整:
- `router/routes.ts``path``name``component``meta.layout`
- page driver 名稱與 import
- page component 名稱與 import
- 頁面標題、查詢欄位、表格欄位、form 型別、驗證規則
- store、service、model、語系、menu/favorites/breadcrumb 相關資料
+356 -23
View File
@@ -1,38 +1,371 @@
<script setup lang="ts">
import PageMasterDetailAMaintenance from '@/components/pages/PageMasterDetailAMaintenance.vue'
import ConfirmDialog from '@/components/maint/CommonConfirmDialog.vue'
import DetailNavigation from '@/components/maint/master-detail/DetailNavigation.vue'
import DetailSidePanel from '@/components/maint/master-detail/DetailSidePanel.vue'
import MasterFileFormFields from '@/components/maint/MasterFileFormFields.vue'
import MntDialogCard from '@/components/maint/MntDialogCard.vue'
import MntRecordNavToolbar from '@/components/maint/MntRecordNavToolbar.vue'
import PageMaint from '@/components/PageMaint.vue'
import SectionDataTable from '@/components/sections/SectionDataTable.vue'
import SectionSearchPanel from '@/components/sections/SectionSearchPanel.vue'
import { useMasterDetailAMaintenancePage } from '@/composables/page-drivers/useMasterDetailAMaintenancePage'
const {
currentPage, formState, itemsPerPage, masterDetailEvents, masterDetailProps,
openAddDialog, openEditDialog, openViewDialog, pageCount, pageModel, pageSummary,
resetSearch, search, searchPanelOpen, snackbarVisible, students, tableHeaders,
confirmSave, currentPage, departments, detailForm, flow, formState,
gradeOptions, itemsPerPage, masterDetailEvents, masterDetailProps,
openAddDialog, openEditDialog, openViewDialog, pageCount, pageModel,
pageSummary, requestSaveConfirmation, resetSearch, scrollToField,
search, searchPanelOpen, snackbarVisible, statuses, students, tableHeaders,
} = useMasterDetailAMaintenancePage()
</script>
<template>
<PageMasterDetailAMaintenance
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"
<PageMaint
:search-panel-open="searchPanelOpen"
:title="pageModel.title"
@toggle-search="searchPanelOpen = !searchPanelOpen"
@create="openAddDialog"
@edit="openEditDialog"
@reset-search="resetSearch"
@update:current-page="currentPage = $event"
@view="openViewDialog"
v-on="masterDetailEvents"
>
<template #search-fields>
<SectionSearchPanel
v-model="search"
:departments="masterDetailProps.departments"
:grade-options="masterDetailProps.gradeOptions"
:statuses="masterDetailProps.statuses"
@reset="resetSearch"
/>
</template>
<template #table>
<SectionDataTable
: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($event)"
@edit="openEditDialog($event)"
@update:current-page="currentPage = $event"
@view="openViewDialog($event)"
/>
</template>
</PageMaint>
<teleport to="body">
<v-overlay
class="dialog-overlay"
:close-on-content-click="false"
:model-value="masterDetailProps.dialogVisible"
scrim="rgba(0, 0, 0, 0.45)"
scroll-strategy="block"
@update:model-value="flow.handleDialogVisibility($event)"
>
<div class="dialog-panel" :class="{ 'is-mobile': masterDetailProps.isMobile }">
<div
v-if="!masterDetailProps.isMobile || masterDetailProps.activeMobilePanel === 'detail'"
class="detail-panel-wrapper"
:class="{ 'is-active': !!masterDetailProps.selectedSemesterId, 'is-mobile': masterDetailProps.isMobile }"
>
<DetailSidePanel
v-model:detail-form="detailForm"
:is-detail-editing="masterDetailProps.isDetailEditing"
:is-mobile="masterDetailProps.isMobile"
:is-view-mode="masterDetailProps.isViewMode"
:selected-semester="masterDetailProps.selectedSemester"
v-on="masterDetailEvents"
/>
</div>
<MntDialogCard
v-if="!masterDetailProps.isMobile || masterDetailProps.activeMobilePanel === 'master'"
:dialog-subtitle="masterDetailProps.dialogSubtitle"
:dialog-title="masterDetailProps.dialogTitle"
:is-edit-mode="masterDetailProps.isEditMode"
:is-view-mode="masterDetailProps.isViewMode"
:width="masterDetailProps.isMobile ? '100%' : 760"
>
<template #toolbar>
<MntRecordNavToolbar
:has-next-record="masterDetailProps.hasNextRecord"
:has-prev-record="masterDetailProps.hasPrevRecord"
:is-edit-mode="masterDetailProps.isEditMode"
:is-view-mode="masterDetailProps.isViewMode"
:mobile="masterDetailProps.isMobile"
v-on="masterDetailEvents"
/>
</template>
<template #content>
<v-alert
v-if="masterDetailProps.errorSummary.length > 0 && !masterDetailProps.isLoading"
class="mb-4"
type="error"
variant="tonal"
>
<div class="text-subtitle-2 mb-2">請先修正以下問題</div>
<div class="d-flex flex-column ga-1">
<v-btn
v-for="error in masterDetailProps.errorSummary"
:key="error.field"
color="error"
size="small"
variant="text"
@click="scrollToField(error.field)"
>
{{ error.message }}
</v-btn>
</div>
</v-alert>
<v-skeleton-loader
v-if="masterDetailProps.isLoading"
class="mt-4"
type="subtitle,paragraph"
width="100%"
/>
<v-form
v-else
:class="{ 'form-readonly': masterDetailProps.isFormReadonly }"
@submit.prevent="requestSaveConfirmation()"
>
<MasterFileFormFields
:departments="masterDetailProps.departments"
:enroll-years="masterDetailProps.enrollYears"
:field-errors="masterDetailProps.fieldErrors"
:form="formState.form.value"
:grade-options="masterDetailProps.gradeOptions"
:is-form-locked="masterDetailProps.isFormLocked"
:is-form-readonly="masterDetailProps.isFormReadonly"
:statuses="masterDetailProps.statuses"
v-on="masterDetailEvents"
/>
<v-divider />
<DetailNavigation
:is-mobile="masterDetailProps.isMobile"
:is-view-mode="masterDetailProps.isViewMode"
:selected-semester-id="masterDetailProps.selectedSemesterId"
:semesters="masterDetailProps.semesters"
v-on="masterDetailEvents"
/>
</v-form>
</template>
<template #actions>
<template v-if="masterDetailProps.isMobile">
<v-btn :disabled="masterDetailProps.isSaving" variant="text" @click="flow.requestCloseDialog()">取消</v-btn>
<v-btn
v-if="masterDetailProps.isEditMode"
color="error"
:disabled="masterDetailProps.isSaving"
variant="tonal"
@click="flow.requestDeleteCurrent()"
>
刪除
</v-btn>
<v-btn
v-if="!masterDetailProps.isViewMode"
color="primary"
:disabled="!masterDetailProps.isDirty || masterDetailProps.isLoading"
:loading="masterDetailProps.isSaving"
variant="flat"
@click="requestSaveConfirmation()"
>
儲存
</v-btn>
<v-btn v-else color="primary" variant="flat" @click="flow.requestCloseDialog()">關閉</v-btn>
</template>
<template v-else>
<v-spacer />
<v-btn :disabled="masterDetailProps.isSaving" variant="text" @click="flow.requestCloseDialog()">取消</v-btn>
<v-btn
v-if="masterDetailProps.isEditMode"
color="error"
:disabled="masterDetailProps.isSaving"
variant="tonal"
@click="flow.requestDeleteCurrent()"
>
刪除
</v-btn>
<v-btn
v-if="!masterDetailProps.isViewMode"
color="primary"
:disabled="!masterDetailProps.isDirty || masterDetailProps.isLoading"
:loading="masterDetailProps.isSaving"
variant="flat"
@click="requestSaveConfirmation()"
>
儲存
</v-btn>
<v-btn v-else color="primary" variant="flat" @click="flow.requestCloseDialog()">關閉</v-btn>
</template>
</template>
</MntDialogCard>
</div>
</v-overlay>
</teleport>
<ConfirmDialog
:model-value="masterDetailProps.confirmCloseVisible"
confirm-color="error"
confirm-text="關閉不儲存"
message="目前有尚未儲存的內容,確定要關閉嗎?"
title="未儲存變更"
@confirm="flow.confirmClose()"
@update:model-value="flow.confirmCloseVisible.value = $event"
/>
<ConfirmDialog
:model-value="masterDetailProps.confirmSaveVisible"
:confirm-loading="masterDetailProps.isSaving"
confirm-text="確認儲存"
max-width="520"
title="確認儲存變更"
@confirm="confirmSave()"
@update:model-value="flow.confirmSaveVisible.value = $event"
>
<div v-if="masterDetailProps.saveSummary.length > 0" class="d-flex flex-column ga-2">
<div v-for="item in masterDetailProps.saveSummary" :key="item.label" class="d-flex flex-column">
<div class="text-caption text-medium-emphasis">{{ item.label }}</div>
<div v-if="item.before !== null" class="text-body-2">
<span class="text-medium-emphasis"></span>
<span :class="{ 'text-medium-emphasis': item.before === '—' }">
{{ item.before }}
</span>
</div>
<div class="text-body-2">
<span class="text-medium-emphasis"></span>
<span :class="{ 'text-medium-emphasis': item.after === '—' }">
{{ item.after }}
</span>
</div>
</div>
</div>
<div v-else class="text-body-2">目前沒有可儲存的變更</div>
</ConfirmDialog>
<ConfirmDialog
:model-value="masterDetailProps.confirmDeleteVisible"
confirm-color="error"
confirm-text="確定刪除"
:message="`確定要刪除 ${masterDetailProps.pendingDeleteLabel} 嗎?此操作無法復原。`"
title="確認刪除"
@confirm="flow.confirmDelete()"
@update:model-value="flow.confirmDeleteVisible.value = $event"
/>
<ConfirmDialog
:model-value="masterDetailProps.confirmSwitchVisible"
confirm-text="確定切換"
max-width="480"
message="目前有尚未儲存的內容,切換為檢視模式將會捨棄變更,確定要切換嗎?"
title="未儲存變更"
@confirm="flow.confirmSwitch()"
@update:model-value="flow.confirmSwitchVisible.value = $event"
/>
<ConfirmDialog
:model-value="masterDetailProps.confirmNavigateVisible"
confirm-text="確定切換"
max-width="480"
message="目前有尚未儲存的內容,切換到其他資料將會捨棄變更,確定要切換嗎?"
title="未儲存變更"
@confirm="flow.confirmNavigate()"
@update:model-value="flow.confirmNavigateVisible.value = $event"
/>
<v-snackbar v-model="snackbarVisible" color="success" location="bottom right" :timeout="2200">
儲存成功
</v-snackbar>
</template>
<style scoped>
.dialog-overlay :deep(.v-overlay__content) {
height: 100vh;
display: flex;
justify-content: flex-end;
width: 100vw;
max-width: 100vw;
}
.dialog-panel {
width: auto;
max-width: 100%;
height: 100vh;
background: transparent;
padding: 0;
display: flex;
}
.dialog-panel > .v-card {
box-shadow: -4px 0 16px rgba(0, 0, 0, 0.12);
}
.detail-panel-wrapper {
width: 0;
opacity: 0;
overflow: hidden;
transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
margin-right: 0;
}
.detail-panel-wrapper.is-active {
width: 600px;
opacity: 1;
margin-right: 0;
}
.dialog-panel.is-mobile {
width: 100%;
}
.dialog-panel.is-mobile :deep(.dialog-title) {
padding: 16px 20px 12px;
}
.dialog-panel.is-mobile :deep(.dialog-toolbar) {
padding: 8px 12px;
}
.dialog-panel.is-mobile :deep(.dialog-actions) {
gap: 8px;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
}
.dialog-panel.is-mobile :deep(.dialog-actions .v-btn) {
flex: 1 1 0;
min-width: 0;
}
.dialog-panel.is-mobile :deep(.v-card-text) {
padding-bottom: 88px;
}
.detail-panel-wrapper.is-mobile {
width: 100%;
opacity: 1;
overflow: visible;
}
.detail-panel-wrapper.is-mobile.is-active {
width: 100%;
}
.form-readonly :deep(.v-field) {
pointer-events: none;
}
@media (max-width: 600px) {
.dialog-panel {
width: 100%;
}
.dialog-panel > .v-card {
width: 100%;
box-shadow: none;
}
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -5
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import PageMaintenance from '@/components/pages/PageMaintenance.vue'
import PageMaint from '@/components/PageMaint.vue'
import SectionDataTable from '@/components/sections/SectionDataTable.vue'
import SectionFormPanel from '@/components/sections/SectionFormPanel.vue'
import SectionSearchPanel from '@/components/sections/SectionSearchPanel.vue'
@@ -14,9 +14,10 @@ const {
</script>
<template>
<PageMaintenance
v-model:search-panel-open="searchPanelOpen"
:page="pageModel"
<PageMaint
:title="pageModel.title"
:search-panel-open="searchPanelOpen"
@toggle-search="searchPanelOpen = !searchPanelOpen"
@create="commands.openAddDialog"
>
<template #search-fields>
@@ -44,7 +45,7 @@ const {
@view="commands.openViewDialog"
/>
</template>
</PageMaintenance>
</PageMaint>
<SectionFormPanel
v-bind="formPanelProps"