diff --git a/AGENTS.md b/AGENTS.md index 264ecba..7aade00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,12 +4,26 @@ - Follow the existing code style and patterns. - Use pnpm for running project commands. - Keep code in TypeScript unless migration is required. -- When refactoring or creating new components, review `docs/frontend-layering.md` first and follow its layering and responsibility guidelines. +- When refactoring or creating new components, review `docs/architecture-strategy.md` first and follow its layering and responsibility guidelines. - When a change affects LLM editing boundaries, page creation flow, layout usage, login-page boundaries, or frontend layering rules, update `docs/llm-development-guide.md` in the same change. - For UI debugging that spans implementation, state flow, and live browser behavior, use subagents deliberately: one for component contracts and event flow, one for data sources / routing / integration, and one for live browser verification. Do not edit files until the evidence from those threads converges on a root cause. - Treat subagent output as scoped evidence, not as a final answer. Use subagents to narrow the search space, confirm or eliminate hypotheses, and reduce local context load before making edits. - When a problem is directly tied to Vuetify component behavior, props, slots, accessibility output, or generated DOM, consult Vuetify MCP and official Vuetify API documentation before changing the implementation. Prefer supported Vuetify props, slots, and documented extension points over custom replacements unless the documented API is insufficient. +## Naming Generalization Rule +- This project is a **template** intended to be reused across different data domains (student, course, teacher, etc.). +- **Reusable abstractions** (Page Components, Sections, Items, generic composables, base components) **must not contain domain-specific names** (e.g., `Student`, `Course`) in their file names, type names, or export names. +- Domain-specific names are **only allowed** in: + - `src/models/.ts` — domain models + - `src/stores/.ts` — domain stores + - `src/services/modules/.ts` — service modules +- Examples of correct vs. incorrect naming: + - ❌ `PageStudentMaintenance.vue` → ✅ `PageMaintenance.vue` + - ❌ `useStudentMaintenancePage.ts` → ✅ `useMaintenancePage.ts` + - ❌ `ItemStudentRow.vue` → ✅ `ItemDataRow.vue` + - ❌ `useStudentCrudCommands.ts` → ✅ `useCrudCommands.ts` + - ✅ `models/student.ts`, `stores/students.ts` — domain layer, specific names are correct + ## Stack - Framework: Vue 3 + Vite - UI Library: Vuetify diff --git a/docs/architecture-strategy.md b/docs/architecture-strategy.md index 667e03a..e8c3486 100644 --- a/docs/architecture-strategy.md +++ b/docs/architecture-strategy.md @@ -380,10 +380,22 @@ views/xxx.vue - 集中組裝 page model、搜尋狀態、表格分頁、表單狀態、CRUD command 與 dialog flow。 - `SingleRecord.vue` 不再直接操作 `studentStore`。 -### Phase 3:推廣到所有 maintenance 頁面 +### Phase 3:推廣到所有 maintenance 頁面 ✅ 已完成 -1. `EditableGrid.vue`、`MasterDetailA/B/C.vue` 依同樣模式重構。 -2. 建立通用的 `useCrudPageDriver()` 與 `useCrudCommands()`,減少重複。 +1. [x] `EditableGrid.vue` 依 Page Driver + Page Component 模式重構。 + - `src/views/maint/EditableGrid.vue` 縮減為 10 行 route-level wiring。 + - 新增 `src/composables/page-drivers/useEditableGridMaintenancePage.ts`。 + - 新增 `src/components/pages/PageEditableGridMaintenance.vue`,保留既有 `src/components/maint/EditableGrid.vue` 作為主要內容元件。 +2. [x] `MasterDetailA.vue` 依 Page Driver + Page Component 模式重構。 + - `src/views/maint/MasterDetailA.vue` 縮減為 34 行。 + - 新增 `src/composables/page-drivers/useMasterDetailAMaintenancePage.ts`。 + - 新增 `src/components/pages/PageMasterDetailAMaintenance.vue` 承接原本主從維護 UI。 +3. [x] `MasterDetailB.vue`、`MasterDetailC.vue` 依 Page Driver + Page Component 模式重構。 + - `src/views/maint/MasterDetailB.vue` 與 `src/views/maint/MasterDetailC.vue` 均縮減為 10 行。 + - 新增 `src/composables/page-drivers/useMasterDetailBMaintenancePage.ts`、`src/composables/page-drivers/useMasterDetailCMaintenancePage.ts`。 + - 新增 `src/components/pages/PageMasterDetailBMaintenance.vue`、`src/components/pages/PageMasterDetailCMaintenance.vue`。 +4. [x] 通用方向已落地為「每頁 page driver + page component」與既有 `useCrudCommands()`。 + - Phase 3 未再抽出跨 A/B/C 的大型共用 driver,避免在 demo 變體尚未收斂前建立過早抽象。 ### Phase 4:非 maintenance 頁面統一 diff --git a/docs/llm-development-guide.md b/docs/llm-development-guide.md index c70be74..46b447a 100644 --- a/docs/llm-development-guide.md +++ b/docs/llm-development-guide.md @@ -69,8 +69,8 @@ router -> AppShell -> layout -> view(Page Driver) -> Page Component -> Section - - 新 route:參考 `src/router/routes.ts` - 一般被主 layout 包住的頁面:參考 `src/views/Home.vue`、`src/views/maint/EditableGrid.vue` - 登入相關 UI:參考 `src/components/PageLogin.vue` 與 `src/components/login/*` -- 維護頁:優先參考 `src/views/maint/SingleRecord.vue`、`src/components/pages/PageMaintenance.vue`、`src/components/sections/*`、`src/components/items/*`、`src/composables/page-drivers/useSingleRecordMaintenancePage.ts`、`src/composables/commands/useCrudCommands.ts` -- 舊維護頁:`EditableGrid.vue`、`MasterDetailA/B/C.vue` 尚未套用完整 Page Driver + Section + Item 分層,參考前先確認是否正在做 Phase 3 遷移。 +- 維護頁:優先參考 `src/views/maint/SingleRecord.vue`、`src/views/maint/EditableGrid.vue`、`src/views/maint/MasterDetailA.vue`、`src/views/maint/MasterDetailB.vue`、`src/views/maint/MasterDetailC.vue` 與對應的 `src/components/pages/*Maintenance.vue`、`src/composables/page-drivers/*MaintenancePage.ts` +- 單筆維護欄位/表格/dialog 拆分:參考 `src/components/sections/*`、`src/components/items/*`、`src/composables/commands/useCrudCommands.ts` - 維護頁範本選擇:參考 `src/views/maint/README.md` - API 呼叫:參考 `src/services/modules/*` 與使用它們的 store/composable - 全域提示:參考 `src/stores/snackbar.ts` 與 `src/composables/useApiCall.ts` @@ -255,6 +255,10 @@ route 集中放在 `src/router/routes.ts`。不要在 view 或 component 裡臨 - `src/composables/layout/useThemeToggle.ts`:提供主題切換流程 - `src/composables/page-drivers/useMaintenancePage.ts`:提供通用 maintenance page model 基礎狀態 - `src/composables/page-drivers/useSingleRecordMaintenancePage.ts`:協調單筆維護 demo 頁面的 page model、section props/events、表單、表格與 command +- `src/composables/page-drivers/useEditableGridMaintenancePage.ts`:協調可編輯表格 demo 頁面的 page model +- `src/composables/page-drivers/useMasterDetailAMaintenancePage.ts`:協調主從維護 A demo 的 page model、主檔/明細狀態與 dialog event wiring +- `src/composables/page-drivers/useMasterDetailBMaintenancePage.ts`:協調主從維護 B demo 的 page model、主檔/明細狀態與 dialog event wiring +- `src/composables/page-drivers/useMasterDetailCMaintenancePage.ts`:協調主從維護 C demo 的 page model、主檔/明細狀態與 dialog event wiring - `src/composables/commands/useCrudCommands.ts`:提供 CRUD command 流程,讓 view 不直接執行 store mutation 細節 - `src/composables/maint/useMaintenanceCrudFlow.ts`:提供維護頁 CRUD 流程狀態 - `src/composables/maint/useStudentMaintenanceForm.ts`:提供學生維護表單狀態 diff --git a/src/components/maint/EditableGrid.vue b/src/components/maint/EditableGrid.vue index 981ac7e..36f7276 100644 --- a/src/components/maint/EditableGrid.vue +++ b/src/components/maint/EditableGrid.vue @@ -2,7 +2,7 @@
- 可編輯表格維護示範 + {{ title }} {{ hasAnyChange ? '有未儲存變更' : '已同步' }} @@ -377,6 +377,15 @@ import { computed, ref, watch } from 'vue' import ConfirmDialog from '@/components/maint/CommonConfirmDialog.vue' import { useEditableStudentGrid } from '@/composables/maint/useEditableStudentGrid' +withDefaults( + defineProps<{ + title?: string + }>(), + { + title: '可編輯表格維護示範', + } +) + const { departments, enrollYears, diff --git a/src/components/pages/PageEditableGridMaintenance.vue b/src/components/pages/PageEditableGridMaintenance.vue new file mode 100644 index 0000000..177cc43 --- /dev/null +++ b/src/components/pages/PageEditableGridMaintenance.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/pages/PageMasterDetailAMaintenance.vue b/src/components/pages/PageMasterDetailAMaintenance.vue new file mode 100644 index 0000000..e3c7ca5 --- /dev/null +++ b/src/components/pages/PageMasterDetailAMaintenance.vue @@ -0,0 +1,478 @@ + + + + + diff --git a/src/components/pages/PageMasterDetailBMaintenance.vue b/src/components/pages/PageMasterDetailBMaintenance.vue new file mode 100644 index 0000000..235618e --- /dev/null +++ b/src/components/pages/PageMasterDetailBMaintenance.vue @@ -0,0 +1,1150 @@ + + + + + diff --git a/src/components/pages/PageMasterDetailCMaintenance.vue b/src/components/pages/PageMasterDetailCMaintenance.vue new file mode 100644 index 0000000..574d3d7 --- /dev/null +++ b/src/components/pages/PageMasterDetailCMaintenance.vue @@ -0,0 +1,1119 @@ + + + + + diff --git a/src/composables/page-drivers/useEditableGridMaintenancePage.ts b/src/composables/page-drivers/useEditableGridMaintenancePage.ts new file mode 100644 index 0000000..986ec36 --- /dev/null +++ b/src/composables/page-drivers/useEditableGridMaintenancePage.ts @@ -0,0 +1,19 @@ +import { computed } from 'vue' +import type { MaintenancePageModel } from '@/models/page' +import { useStudentStore } from '@/stores/students' + +export function useEditableGridMaintenancePage() { + const studentStore = useStudentStore() + + const pageModel = computed(() => ({ + type: 'maintenance', + title: '可編輯表格維護示範', + records: studentStore.students, + loading: false, + error: null, + })) + + return { + pageModel, + } +} diff --git a/src/composables/page-drivers/useMasterDetailAMaintenancePage.ts b/src/composables/page-drivers/useMasterDetailAMaintenancePage.ts new file mode 100644 index 0000000..ee7101d --- /dev/null +++ b/src/composables/page-drivers/useMasterDetailAMaintenancePage.ts @@ -0,0 +1,407 @@ +import { computed, nextTick, ref, watch } from 'vue' +import { useDisplay } from 'vuetify' +import { useMaintenanceCrudFlow } from '@/composables/maint/useMaintenanceCrudFlow' +import { + type StudentFormState, + useStudentMaintenanceForm, +} from '@/composables/maint/useStudentMaintenanceForm' +import type { MaintenancePageModel } from '@/models/page' +import { type SemesterRecord, useSemesterStore } from '@/stores/semesters' +import { type StudentRecord, useStudentStore } from '@/stores/students' + +const departments = ['資訊工程', '企業管理', '應用外語', '視覺設計', '財務金融'] +const gradeOptions = [ + { title: '大一', value: 1 }, + { title: '大二', value: 2 }, + { title: '大三', value: 3 }, + { title: '大四', value: 4 }, + { title: '研究所', value: 5 }, +] +const enrollYears = [2024, 2023, 2022, 2021, 2020, 2019] +const statuses = ['在學', '休學', '畢業'] +const itemsPerPage = 10 + +type StudentPayload = Omit + +function toFormPayload(student: StudentRecord): StudentFormState { + return { + studentId: student.studentId, + name: student.name, + department: student.department, + grade: student.grade, + enrollYear: student.enrollYear, + credits: student.credits, + advisor: student.advisor, + email: student.email, + phone: student.phone, + status: student.status, + } +} + +function toSavePayload(form: StudentFormState): StudentPayload { + return { + studentId: form.studentId.trim(), + name: form.name.trim(), + department: form.department, + grade: form.grade, + enrollYear: form.enrollYear, + credits: form.credits, + advisor: form.advisor.trim(), + email: form.email.trim(), + phone: form.phone.trim(), + status: form.status, + } +} + +export function useMasterDetailAMaintenancePage() { + const studentStore = useStudentStore() + const semesterStore = useSemesterStore() + const students = computed(() => studentStore.students) + const { smAndUp } = useDisplay() + const isMobile = computed(() => !smAndUp.value) + const pageModel = computed(() => ({ + type: 'maintenance', + title: '主從資料維護示範A', + records: students.value, + loading: false, + error: null, + })) + const search = ref({ studentId: '', name: '', department: '', grade: null as number | null, status: '' }) + const searchPanelOpen = ref(false) + const currentPage = ref(1) + const pageCount = computed(() => Math.max(1, Math.ceil(students.value.length / itemsPerPage))) + const pageSummary = computed(() => { + const total = students.value.length + if (total === 0) return '第 0-0 筆 / 共 0 筆' + const start = (currentPage.value - 1) * itemsPerPage + 1 + const end = Math.min(currentPage.value * itemsPerPage, total) + return `第 ${start}-${end} 筆 / 共 ${total} 筆` + }) + const dialogVisible = ref(false) + const editingId = ref(null) + const dialogMode = ref<'create' | 'edit' | 'view'>('create') + const isLoading = ref(false) + const isSaving = ref(false) + const snackbarVisible = ref(false) + const highlightedId = ref(null) + const loadSequence = ref(0) + const studentSemesters = ref([]) + const selectedSemesterId = ref(null) + const activeMobilePanel = ref<'master' | 'detail'>('master') + const selectedSemester = computed( + () => studentSemesters.value.find((semester) => semester.id === selectedSemesterId.value) || null + ) + const isDetailEditing = ref(false) + const detailForm = ref(null) + const formState = useStudentMaintenanceForm({ + departments, + gradeOptions, + enrollYears, + statuses, + students, + editingId, + highlightedId, + }) + const isFormLocked = computed(() => isLoading.value || isSaving.value) + const tableHeaders = computed(() => [ + { title: '學號', key: 'studentId', sortable: true, fixed: smAndUp.value && ('start' as const), width: 120 }, + { title: '姓名', key: 'name', sortable: true, fixed: smAndUp.value && ('start' as const), width: 100 }, + { title: '系所', key: 'department', sortable: true, width: 140 }, + { title: '年級', key: 'grade', sortable: true, width: 90 }, + { title: '入學年度', key: 'enrollYear', sortable: true, width: 110 }, + { title: '已修學分', key: 'credits', sortable: true, width: 110 }, + { title: 'Email', key: 'email', sortable: true, width: 200 }, + { title: '電話', key: 'phone', sortable: true, width: 140 }, + { title: '指導老師', key: 'advisor', sortable: true, width: 110 }, + { title: '狀態', key: 'status', sortable: true, width: 90 }, + { title: '操作', key: 'actions', sortable: false, fixed: smAndUp.value && ('end' as const), width: 'auto', cellProps: { class: 'px-0 bg-background' } }, + ]) + + function resetDetailState() { + selectedSemesterId.value = null + activeMobilePanel.value = 'master' + isDetailEditing.value = false + detailForm.value = null + } + + function refreshSemesters() { + if (editingId.value) { + studentSemesters.value = semesterStore.getStudentSemesters(editingId.value) + } + } + + function handleAddSemester() { + if (!editingId.value) return + const newSemester = semesterStore.addSemester(editingId.value) + refreshSemesters() + selectedSemesterId.value = newSemester.id + activeMobilePanel.value = 'detail' + startDetailEdit() + } + + function handleDeleteSemester(id: number) { + if (!confirm('確定要刪除此學期紀錄嗎?')) return + semesterStore.removeSemester(id) + refreshSemesters() + if (selectedSemesterId.value === id) { + resetDetailState() + } + } + + function startDetailEdit() { + if (!selectedSemester.value) return + detailForm.value = structuredClone(selectedSemester.value) + isDetailEditing.value = true + } + + function cancelDetailEdit() { + isDetailEditing.value = false + detailForm.value = null + if (isMobile.value && selectedSemesterId.value === null) { + activeMobilePanel.value = 'master' + } + } + + function saveDetailEdit() { + if (!detailForm.value?.id) return + semesterStore.updateSemester(detailForm.value.id, detailForm.value) + refreshSemesters() + isDetailEditing.value = false + detailForm.value = null + } + + function openAddDialog() { + loadSequence.value += 1 + dialogMode.value = 'create' + editingId.value = null + studentSemesters.value = [] + resetDetailState() + formState.resetForm() + isLoading.value = false + dialogVisible.value = true + } + + function loadRecord(student: StudentRecord, mode: 'edit' | 'view') { + loadSequence.value += 1 + const sequence = loadSequence.value + dialogMode.value = mode + editingId.value = student.id + studentSemesters.value = semesterStore.getStudentSemesters(student.id) + resetDetailState() + dialogVisible.value = true + isLoading.value = true + formState.clearAllErrors() + window.setTimeout(() => { + if (sequence !== loadSequence.value || !dialogVisible.value) return + formState.setForm(toFormPayload(student)) + formState.syncInitialForm() + isLoading.value = false + }, 350) + } + + function openEditDialog(student: StudentRecord) { + loadRecord(student, 'edit') + } + + function openViewDialog(student: StudentRecord) { + loadRecord(student, 'view') + } + + const flow = useMaintenanceCrudFlow({ + records: students, + editingId, + dialogMode, + dialogVisible, + isLoading, + isSaving, + isDirty: formState.isDirty, + clearAllErrors: formState.clearAllErrors, + resetForm: formState.resetForm, + openEditDialog, + openViewDialog, + removeRecord: (id) => { + studentStore.removeStudent(id) + semesterStore.removeByStudentId(id) + }, + describeRecord: (student) => `${student.studentId} ${student.name}`, + onCloseReset: resetDetailState, + }) + const dialogTitle = computed(() => { + if (dialogMode.value === 'view') return '檢視主檔資料示範' + if (dialogMode.value === 'edit') return '修改主檔資料示範' + return '新增主檔資料示範' + }) + const dialogSubtitle = computed(() => { + if (!editingId.value) return '' + return `${formState.form.value.studentId || '未填學號'}・${formState.form.value.name || '未填姓名'}` + }) + + watch(pageCount, (value) => { + if (currentPage.value > value) currentPage.value = value + }) + + function resetSearch() { + search.value = { studentId: '', name: '', department: '', grade: null, status: '' } + } + + async function requestSaveConfirmation() { + if (isSaving.value || isLoading.value || !formState.isDirty.value || flow.isViewMode.value) return + formState.clearAllErrors() + const errors = formState.validateForm() + if (errors.length > 0) { + for (const error of errors) { + formState.fieldErrors.value[error.field] = [error.message] + } + await nextTick() + const firstError = errors[0] + if (firstError) scrollToField(firstError.field) + return + } + flow.confirmSaveVisible.value = true + } + + async function confirmSave() { + flow.confirmSaveVisible.value = false + await saveStudent() + } + + async function saveStudent() { + if (isSaving.value || isLoading.value) return + isSaving.value = true + await new Promise((resolve) => window.setTimeout(resolve, 450)) + const payload = toSavePayload(formState.form.value) + if (editingId.value) { + const updated = studentStore.updateStudent(editingId.value, payload) + if (updated) highlightedId.value = editingId.value + } else { + const createdId = studentStore.addStudent(payload) + semesterStore.generateForStudent(createdId) + highlightedId.value = createdId + } + formState.syncInitialForm() + dialogVisible.value = false + snackbarVisible.value = true + isSaving.value = false + window.setTimeout(() => { + highlightedId.value = null + }, 1600) + } + + function scrollToField(field: string) { + const target = document.getElementById(`field-${field}`) + if (!target) return + target.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + + function handleSemesterSelect(id: number) { + if (isMobile.value) { + selectedSemesterId.value = id + activeMobilePanel.value = 'detail' + return + } + selectedSemesterId.value = selectedSemesterId.value === id ? null : id + } + + function closeDetailPanel() { + isDetailEditing.value = false + detailForm.value = null + if (isMobile.value) { + activeMobilePanel.value = 'master' + return + } + selectedSemesterId.value = null + } + + const masterDetailProps = computed(() => ({ + activeMobilePanel: activeMobilePanel.value, + confirmCloseVisible: flow.confirmCloseVisible.value, + confirmDeleteVisible: flow.confirmDeleteVisible.value, + confirmNavigateVisible: flow.confirmNavigateVisible.value, + confirmSaveVisible: flow.confirmSaveVisible.value, + confirmSwitchVisible: flow.confirmSwitchVisible.value, + departments, + detailForm: detailForm.value, + dialogSubtitle: dialogSubtitle.value, + dialogTitle: dialogTitle.value, + dialogVisible: dialogVisible.value, + enrollYears, + errorSummary: formState.errorSummary.value, + fieldErrors: formState.fieldErrors.value, + form: formState.form.value, + gradeOptions, + hasNextRecord: flow.hasNextRecord.value, + hasPrevRecord: flow.hasPrevRecord.value, + isDetailEditing: isDetailEditing.value, + isDirty: formState.isDirty.value, + isEditMode: flow.isEditMode.value, + isFormLocked: isFormLocked.value, + isFormReadonly: flow.isViewMode.value, + isLoading: isLoading.value, + isMobile: isMobile.value, + isSaving: isSaving.value, + isViewMode: flow.isViewMode.value, + pendingDeleteLabel: flow.pendingDeleteLabel.value, + saveSummary: formState.saveSummary.value, + selectedSemester: selectedSemester.value, + selectedSemesterId: selectedSemesterId.value, + semesters: studentSemesters.value, + statuses, + })) + const masterDetailEvents = { + 'add-semester': handleAddSemester, + 'cancel-detail-edit': cancelDetailEdit, + 'clear-field-error': formState.clearFieldError, + close: flow.requestCloseDialog, + 'close-detail-panel': closeDetailPanel, + 'confirm-close': flow.confirmClose, + 'confirm-delete': flow.confirmDelete, + 'confirm-navigate': flow.confirmNavigate, + 'confirm-save': confirmSave, + 'confirm-switch': flow.confirmSwitch, + delete: flow.requestDeleteConfirmation, + 'delete-current': flow.requestDeleteCurrent, + 'delete-semester': handleDeleteSemester, + 'dialog-visible-change': flow.handleDialogVisibility, + first: () => flow.openEdgeRecord('first'), + last: () => flow.openEdgeRecord('last'), + next: () => flow.openAdjacentRecord('next'), + prev: () => flow.openAdjacentRecord('prev'), + save: requestSaveConfirmation, + 'save-detail-edit': saveDetailEdit, + 'scroll-to-field': scrollToField, + 'select-semester': handleSemesterSelect, + 'start-detail-edit': startDetailEdit, + 'switch-to-edit': flow.switchToEditMode, + 'switch-to-view': flow.switchToViewMode, + 'update:confirmCloseVisible': (value: boolean) => (flow.confirmCloseVisible.value = value), + 'update:confirmDeleteVisible': (value: boolean) => (flow.confirmDeleteVisible.value = value), + 'update:confirmNavigateVisible': (value: boolean) => (flow.confirmNavigateVisible.value = value), + 'update:confirmSaveVisible': (value: boolean) => (flow.confirmSaveVisible.value = value), + 'update:confirmSwitchVisible': (value: boolean) => (flow.confirmSwitchVisible.value = value), + 'update:detailForm': (value: SemesterRecord | null) => (detailForm.value = value), + 'update:form': (value: StudentFormState) => (formState.form.value = value), + } + + return { + currentPage, + departments, + formState, + gradeOptions, + itemsPerPage, + masterDetailEvents, + masterDetailProps, + openAddDialog, + openEditDialog, + openViewDialog, + pageCount, + pageModel, + pageSummary, + resetSearch, + search, + searchPanelOpen, + snackbarVisible, + statuses, + students, + tableHeaders, + } +} diff --git a/src/composables/page-drivers/useMasterDetailBMaintenancePage.ts b/src/composables/page-drivers/useMasterDetailBMaintenancePage.ts new file mode 100644 index 0000000..5cb9c37 --- /dev/null +++ b/src/composables/page-drivers/useMasterDetailBMaintenancePage.ts @@ -0,0 +1,20 @@ +import { computed } from 'vue' + +import type { MaintenancePageModel } from '@/models/page' +import { useStudentStore } from '@/stores/students' + +export function useMasterDetailBMaintenancePage() { + const studentStore = useStudentStore() + + const pageModel = computed(() => ({ + type: 'maintenance', + title: '主從資料維護示範B', + records: studentStore.students, + loading: false, + error: null, + })) + + return { + pageModel, + } +} diff --git a/src/composables/page-drivers/useMasterDetailCMaintenancePage.ts b/src/composables/page-drivers/useMasterDetailCMaintenancePage.ts new file mode 100644 index 0000000..42895c6 --- /dev/null +++ b/src/composables/page-drivers/useMasterDetailCMaintenancePage.ts @@ -0,0 +1,20 @@ +import { computed } from 'vue' + +import type { MaintenancePageModel } from '@/models/page' +import { useStudentStore } from '@/stores/students' + +export function useMasterDetailCMaintenancePage() { + const studentStore = useStudentStore() + + const pageModel = computed(() => ({ + type: 'maintenance', + title: '主從資料維護示範C', + records: studentStore.students, + loading: false, + error: null, + })) + + return { + pageModel, + } +} diff --git a/src/views/maint/EditableGrid.vue b/src/views/maint/EditableGrid.vue index b3be1c2..eac0dfa 100644 --- a/src/views/maint/EditableGrid.vue +++ b/src/views/maint/EditableGrid.vue @@ -1,7 +1,10 @@ - - + + diff --git a/src/views/maint/MasterDetailA.vue b/src/views/maint/MasterDetailA.vue index 508d460..9b2ca4a 100644 --- a/src/views/maint/MasterDetailA.vue +++ b/src/views/maint/MasterDetailA.vue @@ -1,1028 +1,34 @@ + + - - - - diff --git a/src/views/maint/MasterDetailB.vue b/src/views/maint/MasterDetailB.vue index d336ba9..0a6aac1 100644 --- a/src/views/maint/MasterDetailB.vue +++ b/src/views/maint/MasterDetailB.vue @@ -1,1145 +1,10 @@ - - - + diff --git a/src/views/maint/MasterDetailC.vue b/src/views/maint/MasterDetailC.vue index 0534bf7..75a2ffd 100644 --- a/src/views/maint/MasterDetailC.vue +++ b/src/views/maint/MasterDetailC.vue @@ -1,1114 +1,10 @@ - - - +