Compare commits
2 Commits
9e8cf28d77
...
ec62fcee51
| Author | SHA1 | Date | |
|---|---|---|---|
| ec62fcee51 | |||
| cad44db4c7 |
@@ -8,5 +8,6 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Page component 接收 page model,再把頁面標題轉交給既有 editable grid feature component。 -->
|
||||||
<EditableStudentGrid :title="page.title" />
|
<EditableStudentGrid :title="page.title" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Page component 只呈現 page driver 解析後的功能代碼,不直接讀 route params。 -->
|
||||||
<v-sheet height="100%" width="100%">
|
<v-sheet height="100%" width="100%">
|
||||||
{{ page.fncId }}
|
{{ page.fncId }}
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
|
|||||||
@@ -7,16 +7,19 @@ defineProps<{
|
|||||||
selectedNews: HomeNewsItem | null
|
selectedNews: HomeNewsItem | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 首頁互動都往上 emit,讓 page driver 統一處理 dialog、訊息中心與 snackbar。
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
news: [item: HomeNewsItem]
|
news: [item: HomeNewsItem]
|
||||||
'message-center': []
|
'message-center': []
|
||||||
quick: [item: HomeQuickItem]
|
quick: [item: HomeQuickItem]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 新聞 dialog 開關是 PageIndex 的雙向 UI 狀態,由 view/page driver 持有。
|
||||||
const isNewsDialogOpen = defineModel<boolean>('newsDialogOpen', { default: false })
|
const isNewsDialogOpen = defineModel<boolean>('newsDialogOpen', { default: false })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- PageHome 作為 page component,把 page model 拆給既有 PageIndex 外殼與事件。 -->
|
||||||
<PageIndex
|
<PageIndex
|
||||||
v-model:is-news-dialog-open="isNewsDialogOpen"
|
v-model:is-news-dialog-open="isNewsDialogOpen"
|
||||||
:news-items="page.newsItems"
|
:news-items="page.newsItems"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ defineProps<{
|
|||||||
page: MaintenancePageModel
|
page: MaintenancePageModel
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// PageMaintenance 只轉發維護頁使用者意圖,CRUD 副作用交給 page driver / command composable。
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'create'): void
|
(e: 'create'): void
|
||||||
(e: 'edit', record: unknown): void
|
(e: 'edit', record: unknown): void
|
||||||
@@ -14,10 +15,12 @@ const emit = defineEmits<{
|
|||||||
(e: 'search', criteria: Record<string, unknown>): void
|
(e: 'search', criteria: Record<string, unknown>): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// 搜尋面板開關是頁面 UI 狀態,用 v-model 交回 view/page driver。
|
||||||
const searchPanelOpen = defineModel<boolean>('searchPanelOpen', { default: false })
|
const searchPanelOpen = defineModel<boolean>('searchPanelOpen', { default: false })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- PageMaint 提供維護頁外殼;搜尋欄位與表格內容由 slot 交給各頁組合。 -->
|
||||||
<PageMaint
|
<PageMaint
|
||||||
:title="page.title"
|
:title="page.title"
|
||||||
:search-panel-open="searchPanelOpen"
|
:search-panel-open="searchPanelOpen"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ defineProps<{
|
|||||||
|
|
||||||
const form = defineModel<StudentFormState>('form', { required: true })
|
const form = defineModel<StudentFormState>('form', { required: true })
|
||||||
const detailFormModel = defineModel<SemesterRecord | null>('detailForm', { required: true })
|
const detailFormModel = defineModel<SemesterRecord | null>('detailForm', { required: true })
|
||||||
|
// 主檔表單、子檔表單與搜尋條件都由 page driver 持有,Page component 只透過 v-model 回寫。
|
||||||
const search = defineModel<{
|
const search = defineModel<{
|
||||||
studentId: string
|
studentId: string
|
||||||
name: string
|
name: string
|
||||||
@@ -82,6 +83,7 @@ const search = defineModel<{
|
|||||||
}>('search', { required: true })
|
}>('search', { required: true })
|
||||||
const searchPanelOpen = defineModel<boolean>('searchPanelOpen', { required: true })
|
const searchPanelOpen = defineModel<boolean>('searchPanelOpen', { required: true })
|
||||||
|
|
||||||
|
// 主從維護頁的 CRUD 與導覽意圖都往上 emit,讓 page driver / command composable 統一處理。
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'add-semester'): void
|
(e: 'add-semester'): void
|
||||||
(e: 'cancel-detail-edit'): void
|
(e: 'cancel-detail-edit'): void
|
||||||
@@ -122,11 +124,13 @@ const emit = defineEmits<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- PageMaintenance 提供維護頁外殼;主從頁在 slots 中組合搜尋、表格與子檔內容。 -->
|
||||||
<PageMaintenance
|
<PageMaintenance
|
||||||
v-model:search-panel-open="searchPanelOpen"
|
v-model:search-panel-open="searchPanelOpen"
|
||||||
:page="page"
|
:page="page"
|
||||||
@create="emit('create')"
|
@create="emit('create')"
|
||||||
>
|
>
|
||||||
|
<!-- 搜尋欄位沿用 SectionSearchPanel,搜尋條件透過 v-model 回到 page driver。 -->
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<SectionSearchPanel
|
<SectionSearchPanel
|
||||||
v-model="search"
|
v-model="search"
|
||||||
@@ -136,6 +140,7 @@ const emit = defineEmits<{
|
|||||||
@reset="emit('reset-search')"
|
@reset="emit('reset-search')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 主檔表格沿用 SectionDataTable,列操作只 emit 使用者意圖。 -->
|
||||||
<template #table>
|
<template #table>
|
||||||
<SectionDataTable
|
<SectionDataTable
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- Page component 組合 PageMaint 外殼、主檔表格、子檔區與 dialog;流程狀態集中在本頁 script。 -->
|
||||||
<page-maint
|
<page-maint
|
||||||
:search-panel-open="searchPanelOpen"
|
:search-panel-open="searchPanelOpen"
|
||||||
:title="page.title"
|
:title="page.title"
|
||||||
@create="openAddDialog"
|
@create="openAddDialog"
|
||||||
@toggle-search="searchPanelOpen = !searchPanelOpen"
|
@toggle-search="searchPanelOpen = !searchPanelOpen"
|
||||||
>
|
>
|
||||||
|
<!-- 搜尋欄位放在 PageMaint 的 search-fields slot,讓外殼固定、欄位由頁面決定。 -->
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- table slot 放主檔表格與列操作,操作事件再交給頁面流程函式處理。 -->
|
||||||
<template #table>
|
<template #table>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
v-model:page="currentPage"
|
v-model:page="currentPage"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- Page component 組合 PageMaint 外殼、主檔表格、子檔區與 dialog;流程狀態集中在本頁 script。 -->
|
||||||
<page-maint
|
<page-maint
|
||||||
:search-panel-open="searchPanelOpen"
|
:search-panel-open="searchPanelOpen"
|
||||||
:title="page.title"
|
:title="page.title"
|
||||||
@create="openAddDialog"
|
@create="openAddDialog"
|
||||||
@toggle-search="searchPanelOpen = !searchPanelOpen"
|
@toggle-search="searchPanelOpen = !searchPanelOpen"
|
||||||
>
|
>
|
||||||
|
<!-- 搜尋欄位放在 PageMaint 的 search-fields slot,讓外殼固定、欄位由頁面決定。 -->
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
<v-btn color="primary" disabled :prepend-icon="mdiMagnify" variant="tonal">查詢</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- table slot 放主檔表格與列操作,操作事件再交給頁面流程函式處理。 -->
|
||||||
<template #table>
|
<template #table>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
v-model:page="currentPage"
|
v-model:page="currentPage"
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import BaseFormSelect from '@/components/base/BaseFormSelect.vue'
|
||||||
|
import BaseFormTextField from '@/components/base/BaseFormTextField.vue'
|
||||||
|
import SectionFormPage from '@/components/sections/SectionFormPage.vue'
|
||||||
|
import type {
|
||||||
|
DemoFormState,
|
||||||
|
SectionsDemoPageModel,
|
||||||
|
} from '@/composables/page-drivers/useSectionsDemoPage'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
page: SectionsDemoPageModel
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 表單內容由 page driver 持有,Page component 只透過 v-model 呈現與回寫。
|
||||||
|
const demoForm = defineModel<DemoFormState>('demoForm', { required: true })
|
||||||
|
|
||||||
|
// 送出、清除、返回都往上 emit,讓 page driver 統一處理訊息與副作用。
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'back'): void
|
||||||
|
(e: 'reset'): void
|
||||||
|
(e: 'submit'): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SectionFormPage
|
||||||
|
back-label="回到列表"
|
||||||
|
reset-label="清除"
|
||||||
|
submit-label="送出"
|
||||||
|
:message="page.formMessage"
|
||||||
|
title="SectionFormPage 報表申請"
|
||||||
|
@back="emit('back')"
|
||||||
|
@reset="emit('reset')"
|
||||||
|
@submit="emit('submit')"
|
||||||
|
>
|
||||||
|
<!-- SectionFormPage 決定表單頁外殼;fields slot 放實際欄位組合。 -->
|
||||||
|
<template #fields>
|
||||||
|
<v-row density="compact">
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<BaseFormTextField v-model="demoForm.title" label="標題" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<BaseFormSelect v-model="demoForm.owner" label="單位" :items="page.ownerOptions" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="3">
|
||||||
|
<BaseFormSelect v-model="demoForm.category" label="類型" :items="page.categoryOptions" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<BaseFormTextField v-model="demoForm.description" label="說明" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- sections slot 放表單主欄位以外的子區段,例如明細表格或補充資訊。 -->
|
||||||
|
<template #sections>
|
||||||
|
<v-card class="mb-2">
|
||||||
|
<v-card-title class="text-title-medium font-weight-bold">明細</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-table density="compact">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>欄位</th>
|
||||||
|
<th>值</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>單位</td>
|
||||||
|
<td>{{ demoForm.owner }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>類型</td>
|
||||||
|
<td>{{ demoForm.category }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- notices slot 放配合事項,讓外殼固定、內容由頁面決定。 -->
|
||||||
|
<template #notices>
|
||||||
|
<v-list class="bg-yellow-lighten-5" density="compact">
|
||||||
|
<v-list-item>送出前確認標題與單位。</v-list-item>
|
||||||
|
<v-list-item>表單狀態由 page driver 管理。</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
</SectionFormPage>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import BaseFormSelect from '@/components/base/BaseFormSelect.vue'
|
||||||
|
import BaseFormTextField from '@/components/base/BaseFormTextField.vue'
|
||||||
|
import SectionQueryPage from '@/components/sections/SectionQueryPage.vue'
|
||||||
|
import type {
|
||||||
|
ReportFilters,
|
||||||
|
SectionsDemoPageModel,
|
||||||
|
} from '@/composables/page-drivers/useSectionsDemoPage'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
page: SectionsDemoPageModel
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// Page component 只接收 page driver 組好的 page model;查詢條件用 v-model 回寫給 view/page driver。
|
||||||
|
const queryFilters = defineModel<ReportFilters>('queryFilters', { required: true })
|
||||||
|
|
||||||
|
// 使用者意圖往上 emit,由 page driver 決定查詢、返回等副作用。
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'back'): void
|
||||||
|
(e: 'search'): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SectionQueryPage
|
||||||
|
back-label="回到列表"
|
||||||
|
title="查詢頁DEMO"
|
||||||
|
@back="emit('back')"
|
||||||
|
@search="emit('search')"
|
||||||
|
>
|
||||||
|
<!-- SectionQueryPage 決定查詢頁外殼;欄位內容由 filters slot 交給頁面自行組合。 -->
|
||||||
|
<template #filters>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<BaseFormTextField v-model="queryFilters.keyword" label="關鍵字" />
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<BaseFormSelect v-model="queryFilters.owner" label="單位" :items="page.ownerOptions" />
|
||||||
|
</v-col>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- results slot 放查詢結果;資料仍由 page model 提供,這裡只負責呈現。 -->
|
||||||
|
<template #results>
|
||||||
|
<v-alert v-if="page.queryMessage" class="mb-3" type="success" variant="tonal">
|
||||||
|
{{ page.queryMessage }}
|
||||||
|
</v-alert>
|
||||||
|
<v-table density="compact">
|
||||||
|
<thead class="bg-primary">
|
||||||
|
<tr>
|
||||||
|
<th>名稱</th>
|
||||||
|
<th>單位</th>
|
||||||
|
<th>狀態</th>
|
||||||
|
<th>更新日</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="page.reports.length === 0">
|
||||||
|
<td class="text-center" colspan="4">尚無查詢結果</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="row in page.reports" :key="row.id">
|
||||||
|
<td>{{ row.title }}</td>
|
||||||
|
<td>{{ row.owner }}</td>
|
||||||
|
<td>{{ row.status }}</td>
|
||||||
|
<td>{{ row.updatedAt }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</v-table>
|
||||||
|
</template>
|
||||||
|
</SectionQueryPage>
|
||||||
|
</template>
|
||||||
@@ -7,5 +7,6 @@ defineProps<{
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- Page component 只呈現 page driver 組好的設定頁 model。 -->
|
||||||
<div>{{ page.title }}</div>
|
<div>{{ page.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const emit = defineEmits<{
|
|||||||
<template>
|
<template>
|
||||||
<v-form @submit.prevent="emit('submit')">
|
<v-form @submit.prevent="emit('submit')">
|
||||||
<v-container fluid class="pt-2 px-1">
|
<v-container fluid class="pt-2 px-1">
|
||||||
<v-card>
|
<v-card class="mb-2">
|
||||||
<v-card-title class="bg-primary text-title-large text-center py-2">
|
<v-card-title class="bg-primary text-title-large text-center py-2">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid class="pt-2 px-1">
|
<v-container fluid class="pt-2 px-1">
|
||||||
<v-card>
|
<v-card class="mb-2">
|
||||||
<v-card-title class="text-title-large bg-primary">{{ title }}</v-card-title>
|
<v-card-title class="text-title-large bg-primary">{{ title }}</v-card-title>
|
||||||
<v-card-text class="pa-4">
|
<v-card-text class="pa-4">
|
||||||
<v-alert v-if="error" class="mb-4" type="error" variant="tonal">{{ error }}</v-alert>
|
<v-alert v-if="error" class="mb-4" type="error" variant="tonal">{{ error }}</v-alert>
|
||||||
|
|||||||
@@ -29,6 +29,18 @@ const fixedMenuItems: LayoutMenuItem[] = [
|
|||||||
{ title: '可編輯表格維護', icon: mdiTableEdit, path: '/editable-grid-maintenance' },
|
{ title: '可編輯表格維護', icon: mdiTableEdit, path: '/editable-grid-maintenance' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '範例頁面',
|
||||||
|
navigable: false,
|
||||||
|
subItems: [
|
||||||
|
{
|
||||||
|
title: 'SectionQueryPage',
|
||||||
|
icon: mdiFileDocumentOutline,
|
||||||
|
path: '/demos/sections/query-page',
|
||||||
|
},
|
||||||
|
{ title: 'SectionFormPage', icon: mdiFileDocumentOutline, path: '/demos/sections/form-page' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{ title: '登入頁', path: '/login' },
|
{ title: '登入頁', path: '/login' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import type { StudentRecord } from '@/models/student'
|
||||||
|
import { useSnackbarStore } from '@/stores/snackbar'
|
||||||
|
import type {
|
||||||
|
SaveSummaryItem,
|
||||||
|
StudentFormState,
|
||||||
|
} from '@/composables/maint/useStudentMaintenanceForm'
|
||||||
|
|
||||||
|
export interface ReportSummary {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
owner: string
|
||||||
|
status: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportFilters {
|
||||||
|
keyword: string
|
||||||
|
owner: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DemoFormState {
|
||||||
|
title: string
|
||||||
|
owner: string
|
||||||
|
category: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaintenanceSearchState {
|
||||||
|
studentId: string
|
||||||
|
name: string
|
||||||
|
department: string
|
||||||
|
grade: number | null
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SectionsDemoPageModel {
|
||||||
|
title: string
|
||||||
|
ownerOptions: string[]
|
||||||
|
categoryOptions: string[]
|
||||||
|
queryMessage: string
|
||||||
|
formMessage: string
|
||||||
|
reports: ReportSummary[]
|
||||||
|
departments: string[]
|
||||||
|
gradeOptions: GradeOption[]
|
||||||
|
enrollYears: number[]
|
||||||
|
statuses: string[]
|
||||||
|
maintenanceHeaders: Array<Record<string, unknown>>
|
||||||
|
maintenanceItems: StudentRecord[]
|
||||||
|
maintenanceItemsPerPage: number
|
||||||
|
maintenancePageCount: number
|
||||||
|
maintenancePageSummary: string
|
||||||
|
formPanelProps: FormPanelProps
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GradeOption {
|
||||||
|
title: string
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type FieldErrors = Record<keyof StudentFormState, string[]>
|
||||||
|
|
||||||
|
interface FormPanelProps {
|
||||||
|
confirmCloseVisible: boolean
|
||||||
|
confirmDeleteVisible: boolean
|
||||||
|
confirmNavigateVisible: boolean
|
||||||
|
confirmSaveVisible: boolean
|
||||||
|
confirmSwitchVisible: boolean
|
||||||
|
departments: string[]
|
||||||
|
dialogSubtitle: string
|
||||||
|
dialogTitle: string
|
||||||
|
dialogVisible: boolean
|
||||||
|
enrollYears: number[]
|
||||||
|
errorSummary: Array<{ field: string; message: string }>
|
||||||
|
fieldErrors: FieldErrors
|
||||||
|
gradeOptions: GradeOption[]
|
||||||
|
hasNextRecord: boolean
|
||||||
|
hasPrevRecord: boolean
|
||||||
|
isDirty: boolean
|
||||||
|
isEditMode: boolean
|
||||||
|
isFormLocked: boolean
|
||||||
|
isFormReadonly: boolean
|
||||||
|
isLoading: boolean
|
||||||
|
isSaving: boolean
|
||||||
|
isViewMode: boolean
|
||||||
|
pendingDeleteLabel: string
|
||||||
|
saveSummary: SaveSummaryItem[]
|
||||||
|
statuses: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const reports: ReportSummary[] = [
|
||||||
|
{ id: 1, title: '學生統計', owner: '教務處', status: '已發布', updatedAt: '2026-05-01' },
|
||||||
|
{ id: 2, title: '課程統計', owner: '課務組', status: '草稿', updatedAt: '2026-05-08' },
|
||||||
|
{ id: 3, title: '系統使用量', owner: '資訊中心', status: '已發布', updatedAt: '2026-05-15' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const ownerOptions = ['全部', '教務處', '課務組', '資訊中心']
|
||||||
|
const categoryOptions = ['一般報表', '申請表單', '維護資料']
|
||||||
|
const departments = ['資訊工程', '企業管理', '應用外語', '視覺設計', '財務金融']
|
||||||
|
const gradeOptions: GradeOption[] = [
|
||||||
|
{ title: '大一', value: 1 },
|
||||||
|
{ title: '大二', value: 2 },
|
||||||
|
{ title: '大三', value: 3 },
|
||||||
|
{ title: '大四', value: 4 },
|
||||||
|
]
|
||||||
|
const enrollYears = [2026, 2025, 2024, 2023]
|
||||||
|
const statuses = ['在學', '休學', '畢業']
|
||||||
|
const maintenanceItemsPerPage = 5
|
||||||
|
|
||||||
|
const students: StudentRecord[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
studentId: 'S2026001',
|
||||||
|
name: '王小明',
|
||||||
|
department: '資訊工程',
|
||||||
|
grade: 1,
|
||||||
|
enrollYear: 2026,
|
||||||
|
credits: 18,
|
||||||
|
advisor: '陳教授',
|
||||||
|
email: 'ming@example.edu',
|
||||||
|
phone: '0912000001',
|
||||||
|
status: '在學',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
studentId: 'S2025007',
|
||||||
|
name: '林雅婷',
|
||||||
|
department: '企業管理',
|
||||||
|
grade: 2,
|
||||||
|
enrollYear: 2025,
|
||||||
|
credits: 42,
|
||||||
|
advisor: '李教授',
|
||||||
|
email: 'yating@example.edu',
|
||||||
|
phone: '0912000002',
|
||||||
|
status: '在學',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
studentId: 'S2024012',
|
||||||
|
name: '張志豪',
|
||||||
|
department: '應用外語',
|
||||||
|
grade: 3,
|
||||||
|
enrollYear: 2024,
|
||||||
|
credits: 86,
|
||||||
|
advisor: '黃教授',
|
||||||
|
email: 'zhihao@example.edu',
|
||||||
|
phone: '0912000003',
|
||||||
|
status: '休學',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const maintenanceHeaders = [
|
||||||
|
{ title: '學號', key: 'studentId', sortable: true, width: 120 },
|
||||||
|
{ title: '姓名', key: 'name', sortable: true, width: 100 },
|
||||||
|
{ title: '系所', key: 'department', sortable: true, width: 140 },
|
||||||
|
{ title: '年級', key: 'grade', sortable: true, width: 90 },
|
||||||
|
{ title: 'Email', key: 'email', sortable: true, width: 200 },
|
||||||
|
{ title: '狀態', key: 'status', sortable: true, width: 90 },
|
||||||
|
{ title: '操作', key: 'actions', sortable: false, width: 220 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultQueryFilters: ReportFilters = {
|
||||||
|
keyword: '',
|
||||||
|
owner: '全部',
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDemoForm: DemoFormState = {
|
||||||
|
title: '',
|
||||||
|
owner: '教務處',
|
||||||
|
category: '一般報表',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultMaintenanceSearch: MaintenanceSearchState = {
|
||||||
|
studentId: '',
|
||||||
|
name: '',
|
||||||
|
department: '',
|
||||||
|
grade: null,
|
||||||
|
status: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFormPanelForm: StudentFormState = {
|
||||||
|
studentId: '',
|
||||||
|
name: '',
|
||||||
|
department: departments[0] ?? '',
|
||||||
|
grade: gradeOptions[0]?.value ?? 1,
|
||||||
|
enrollYear: enrollYears[0] ?? 2026,
|
||||||
|
credits: 0,
|
||||||
|
advisor: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
status: statuses[0] ?? '',
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyFieldErrors(): FieldErrors {
|
||||||
|
return {
|
||||||
|
studentId: [],
|
||||||
|
name: [],
|
||||||
|
department: [],
|
||||||
|
grade: [],
|
||||||
|
enrollYear: [],
|
||||||
|
credits: [],
|
||||||
|
advisor: [],
|
||||||
|
email: [],
|
||||||
|
phone: [],
|
||||||
|
status: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSectionsDemoPage() {
|
||||||
|
const snackbar = useSnackbarStore()
|
||||||
|
const queryFilters = ref<ReportFilters>({ ...defaultQueryFilters })
|
||||||
|
const demoForm = ref<DemoFormState>({ ...defaultDemoForm })
|
||||||
|
const maintenanceSearch = ref<MaintenanceSearchState>({ ...defaultMaintenanceSearch })
|
||||||
|
const maintenanceCurrentPage = ref(1)
|
||||||
|
const formPanelVisible = ref(false)
|
||||||
|
const formPanelForm = ref<StudentFormState>({ ...defaultFormPanelForm })
|
||||||
|
const fieldErrors = ref<FieldErrors>(createEmptyFieldErrors())
|
||||||
|
const queryMessage = ref('')
|
||||||
|
const formMessage = ref('')
|
||||||
|
|
||||||
|
const filteredReports = computed(() => {
|
||||||
|
const keyword = queryFilters.value.keyword.trim().toLowerCase()
|
||||||
|
const owner = queryFilters.value.owner
|
||||||
|
return reports.filter((item) => {
|
||||||
|
const keywordMatched =
|
||||||
|
!keyword ||
|
||||||
|
item.title.toLowerCase().includes(keyword) ||
|
||||||
|
item.owner.toLowerCase().includes(keyword)
|
||||||
|
const ownerMatched = owner === '全部' || item.owner === owner
|
||||||
|
return keywordMatched && ownerMatched
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const maintenanceItems = computed(() => {
|
||||||
|
const keywordId = maintenanceSearch.value.studentId.trim().toLowerCase()
|
||||||
|
const keywordName = maintenanceSearch.value.name.trim().toLowerCase()
|
||||||
|
return students.filter((item) => {
|
||||||
|
const idMatched = !keywordId || item.studentId.toLowerCase().includes(keywordId)
|
||||||
|
const nameMatched = !keywordName || item.name.toLowerCase().includes(keywordName)
|
||||||
|
const departmentMatched =
|
||||||
|
!maintenanceSearch.value.department || item.department === maintenanceSearch.value.department
|
||||||
|
const gradeMatched =
|
||||||
|
maintenanceSearch.value.grade == null || item.grade === maintenanceSearch.value.grade
|
||||||
|
const statusMatched = !maintenanceSearch.value.status || item.status === maintenanceSearch.value.status
|
||||||
|
return idMatched && nameMatched && departmentMatched && gradeMatched && statusMatched
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const maintenancePageCount = computed(() =>
|
||||||
|
Math.max(1, Math.ceil(maintenanceItems.value.length / maintenanceItemsPerPage))
|
||||||
|
)
|
||||||
|
|
||||||
|
const maintenancePageSummary = computed(() => {
|
||||||
|
const total = maintenanceItems.value.length
|
||||||
|
if (total === 0) return '第 0-0 筆 / 共 0 筆'
|
||||||
|
const start = (maintenanceCurrentPage.value - 1) * maintenanceItemsPerPage + 1
|
||||||
|
const end = Math.min(maintenanceCurrentPage.value * maintenanceItemsPerPage, total)
|
||||||
|
return `第 ${start}-${end} 筆 / 共 ${total} 筆`
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFormPanelDirty = computed(
|
||||||
|
() => JSON.stringify(formPanelForm.value) !== JSON.stringify(defaultFormPanelForm)
|
||||||
|
)
|
||||||
|
|
||||||
|
const formPanelProps = computed<FormPanelProps>(() => ({
|
||||||
|
confirmCloseVisible: false,
|
||||||
|
confirmDeleteVisible: false,
|
||||||
|
confirmNavigateVisible: false,
|
||||||
|
confirmSaveVisible: false,
|
||||||
|
confirmSwitchVisible: false,
|
||||||
|
departments,
|
||||||
|
dialogSubtitle: formPanelForm.value.studentId || '尚未輸入學號',
|
||||||
|
dialogTitle: 'SectionFormPanel 範例',
|
||||||
|
dialogVisible: formPanelVisible.value,
|
||||||
|
enrollYears,
|
||||||
|
errorSummary: [],
|
||||||
|
fieldErrors: fieldErrors.value,
|
||||||
|
gradeOptions,
|
||||||
|
hasNextRecord: false,
|
||||||
|
hasPrevRecord: false,
|
||||||
|
isDirty: isFormPanelDirty.value,
|
||||||
|
isEditMode: false,
|
||||||
|
isFormLocked: false,
|
||||||
|
isFormReadonly: false,
|
||||||
|
isLoading: false,
|
||||||
|
isSaving: false,
|
||||||
|
isViewMode: false,
|
||||||
|
pendingDeleteLabel: formPanelForm.value.name || '目前資料',
|
||||||
|
saveSummary: [],
|
||||||
|
statuses,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const pageModel = computed<SectionsDemoPageModel>(() => ({
|
||||||
|
title: '新增頁面與 Section 範例',
|
||||||
|
ownerOptions,
|
||||||
|
categoryOptions,
|
||||||
|
queryMessage: queryMessage.value,
|
||||||
|
formMessage: formMessage.value,
|
||||||
|
reports: filteredReports.value,
|
||||||
|
departments,
|
||||||
|
gradeOptions,
|
||||||
|
enrollYears,
|
||||||
|
statuses,
|
||||||
|
maintenanceHeaders,
|
||||||
|
maintenanceItems: maintenanceItems.value,
|
||||||
|
maintenanceItemsPerPage,
|
||||||
|
maintenancePageCount: maintenancePageCount.value,
|
||||||
|
maintenancePageSummary: maintenancePageSummary.value,
|
||||||
|
formPanelProps: formPanelProps.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
watch(maintenancePageCount, (value) => {
|
||||||
|
if (maintenanceCurrentPage.value > value) maintenanceCurrentPage.value = value
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleQuerySearch() {
|
||||||
|
queryMessage.value = `查詢完成,共 ${filteredReports.value.length} 筆`
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQueryBack() {
|
||||||
|
snackbar.show({ message: '查詢頁返回事件', color: 'info' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormSubmit() {
|
||||||
|
formMessage.value = demoForm.value.title.trim()
|
||||||
|
? `已送出:${demoForm.value.title.trim()}`
|
||||||
|
: '請輸入標題後再送出'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDemoForm() {
|
||||||
|
demoForm.value = { ...defaultDemoForm }
|
||||||
|
formMessage.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormBack() {
|
||||||
|
snackbar.show({ message: '表單頁返回事件', color: 'info' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetMaintenanceSearch() {
|
||||||
|
maintenanceSearch.value = { ...defaultMaintenanceSearch }
|
||||||
|
maintenanceCurrentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMaintenanceAction(action: string, record: StudentRecord) {
|
||||||
|
snackbar.show({ message: `${action}:${record.studentId} ${record.name}`, color: 'info' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFormPanel() {
|
||||||
|
formPanelVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeFormPanel() {
|
||||||
|
formPanelVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormPanelVisibleChange(value: boolean) {
|
||||||
|
formPanelVisible.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormPanelSave() {
|
||||||
|
formPanelVisible.value = false
|
||||||
|
snackbar.show({ message: 'SectionFormPanel 儲存事件', color: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFormPanelFieldError(field: keyof StudentFormState | string) {
|
||||||
|
const key = field as keyof StudentFormState
|
||||||
|
if (!fieldErrors.value[key]?.length) return
|
||||||
|
fieldErrors.value[key] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function gradeLabel(grade: number) {
|
||||||
|
return gradeOptions.find((option) => option.value === grade)?.title ?? `年級 ${grade}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusColor(status: string) {
|
||||||
|
if (status === '在學') return 'success'
|
||||||
|
if (status === '休學') return 'warning'
|
||||||
|
if (status === '畢業') return 'secondary'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
function rowProps() {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
demoForm,
|
||||||
|
formPanelForm,
|
||||||
|
maintenanceCurrentPage,
|
||||||
|
maintenanceSearch,
|
||||||
|
pageModel,
|
||||||
|
queryFilters,
|
||||||
|
clearFormPanelFieldError,
|
||||||
|
closeFormPanel,
|
||||||
|
gradeLabel,
|
||||||
|
handleFormBack,
|
||||||
|
handleFormPanelSave,
|
||||||
|
handleFormPanelVisibleChange,
|
||||||
|
handleFormSubmit,
|
||||||
|
handleMaintenanceAction,
|
||||||
|
handleQueryBack,
|
||||||
|
handleQuerySearch,
|
||||||
|
openFormPanel,
|
||||||
|
resetDemoForm,
|
||||||
|
resetMaintenanceSearch,
|
||||||
|
rowProps,
|
||||||
|
statusColor,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,6 +49,22 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/maint/EditableGrid.vue'),
|
component: () => import('@/views/maint/EditableGrid.vue'),
|
||||||
meta: { layout: 'default' },
|
meta: { layout: 'default' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/demos/sections',
|
||||||
|
redirect: '/demos/sections/query-page',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/demos/sections/query-page',
|
||||||
|
name: 'demo-section-query-page',
|
||||||
|
component: () => import('@/views/demos/SectionQueryPageDemo.vue'),
|
||||||
|
meta: { title: 'SectionQueryPage 示範', layout: 'default' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/demos/sections/form-page',
|
||||||
|
name: 'demo-section-form-page',
|
||||||
|
component: () => import('@/views/demos/SectionFormPageDemo.vue'),
|
||||||
|
meta: { title: 'SectionFormPage 示範', layout: 'default' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:fncId([0-9A-Z]{5,6})',
|
path: '/:fncId([0-9A-Z]{5,6})',
|
||||||
name: 'fnc-page',
|
name: 'fnc-page',
|
||||||
|
|||||||
@@ -39,5 +39,6 @@ const page = useReportsPage()
|
|||||||
|
|
||||||
## 子目錄
|
## 子目錄
|
||||||
|
|
||||||
|
- `views/demos` 是一般頁面與 section 使用方式的 demo route entry,仍需維持薄 view。
|
||||||
- `views/maint` 是 maintenance demo route entry。詳見 `src/views/maint/GUIDE.md`。
|
- `views/maint` 是 maintenance demo route entry。詳見 `src/views/maint/GUIDE.md`。
|
||||||
- `views/errors` 是錯誤頁入口,通常使用 `meta.layout = 'none'`。每個錯誤頁(`Forbidden.vue`、`ServerError.vue`、`NotFound.vue` 等)只傳入 props 給共用的 `ErrorShell.vue`,不再各自重複佈局邏輯。`ErrorShell.vue` 提供標題、圖示、顏色、描述、後端訊息、操作按鈕(返回上頁 / 回首頁 / 前往登入)等 slots。
|
- `views/errors` 是錯誤頁入口,通常使用 `meta.layout = 'none'`。每個錯誤頁(`Forbidden.vue`、`ServerError.vue`、`NotFound.vue` 等)只傳入 props 給共用的 `ErrorShell.vue`,不再各自重複佈局邏輯。`ErrorShell.vue` 提供標題、圖示、顏色、描述、後端訊息、操作按鈕(返回上頁 / 回首頁 / 前往登入)等 slots。
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageSectionFormPageDemo
|
||||||
|
v-model:demo-form="page.demoForm.value"
|
||||||
|
:page="page.pageModel.value"
|
||||||
|
@back="page.handleFormBack"
|
||||||
|
@reset="page.resetDemoForm"
|
||||||
|
@submit="page.handleFormSubmit"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
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()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PageSectionQueryPageDemo
|
||||||
|
v-model:query-filters="page.queryFilters.value"
|
||||||
|
:page="page.pageModel.value"
|
||||||
|
@back="page.handleQueryBack"
|
||||||
|
@search="page.handleQuerySearch"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user