docs: reorganize architecture strategy documentation

Split current project diagnostics into a dedicated analysis document and
trim the main architecture strategy to focus on core guidance. This makes
the documentation easier to navigate and separates observed issues from
recommended architectural principles.docs: reorganize architecture strategy documentation

Split current project diagnostics into a dedicated analysis document and
trim the main architecture strategy to focus on core guidance. This makes
the documentation easier to navigate and separates observed issues from
recommended architectural principles.
This commit is contained in:
skytek_xinliang
2026-05-19 14:13:10 +08:00
parent 9ae91418e0
commit 96b96bcaaa
12 changed files with 1373 additions and 1077 deletions
@@ -0,0 +1,258 @@
import { computed, ref, watch } from 'vue'
import { useDisplay } from 'vuetify'
import { useCrudCommands } from '@/composables/commands/useCrudCommands'
import { useMaintenanceCrudFlow } from '@/composables/maint/useMaintenanceCrudFlow'
import {
type StudentFormState,
useStudentMaintenanceForm,
} from '@/composables/maint/useStudentMaintenanceForm'
import type { MaintenancePageModel } from '@/models/page'
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<StudentRecord, 'id'>
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 useSingleRecordMaintenancePage() {
const studentStore = useStudentStore()
const students = computed(() => studentStore.students)
const { smAndUp } = useDisplay()
const pageModel = computed<MaintenancePageModel>(() => ({
type: 'maintenance',
title: '單筆資料維護示範',
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<number | null>(null)
const dialogMode = ref<'create' | 'edit' | 'view'>('create')
const isLoading = ref(false)
const isSaving = ref(false)
const highlightedId = ref<number | null>(null)
const loadSequence = ref(0)
const snackbarVisible = ref(false)
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 resetSearch() {
search.value = { studentId: '', name: '', department: '', grade: null, status: '' }
}
function scrollToField(field: string) {
const target = document.getElementById(`field-${field}`)
if (!target) return
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
const commands = useCrudCommands<StudentRecord, StudentFormState>({
...formState,
dialogMode,
dialogVisible,
editingId,
highlightedId,
isLoading,
isSaving,
isViewMode: computed(() => dialogMode.value === 'view'),
loadSequence,
scrollToField,
toFormPayload,
toSavePayload,
updateRecord: (id, payload) => studentStore.updateStudent(id, payload),
createRecord: (payload) => studentStore.addStudent(payload),
})
const flow = useMaintenanceCrudFlow<StudentRecord>({
records: students,
editingId,
dialogMode,
dialogVisible,
isLoading,
isSaving,
isDirty: formState.isDirty,
clearAllErrors: formState.clearAllErrors,
resetForm: formState.resetForm,
openEditDialog: commands.openEditDialog,
openViewDialog: commands.openViewDialog,
removeRecord: (id) => studentStore.removeStudent(id),
describeRecord: (student) => `${student.studentId} ${student.name}`,
})
const dialogTitle = computed(() => {
if (dialogMode.value === 'view') return '檢視資料示範'
if (dialogMode.value === 'edit') return '修改資料示範'
return '新增資料示範'
})
const dialogSubtitle = computed(() => (!editingId.value ? '' : `${formState.form.value.studentId || '未填學號'}${formState.form.value.name || '未填姓名'}`))
watch(pageCount, (value) => {
if (currentPage.value > value) currentPage.value = value
})
async function requestSaveConfirmation() {
await commands.requestSaveConfirmation(flow.confirmSaveVisible)
}
async function confirmSave() {
await commands.confirmSave(flow.confirmSaveVisible)
snackbarVisible.value = true
}
const formPanelProps = computed(() => ({
confirmCloseVisible: flow.confirmCloseVisible.value,
confirmDeleteVisible: flow.confirmDeleteVisible.value,
confirmNavigateVisible: flow.confirmNavigateVisible.value,
confirmSaveVisible: flow.confirmSaveVisible.value,
confirmSwitchVisible: flow.confirmSwitchVisible.value,
departments,
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,
isDirty: formState.isDirty.value,
isEditMode: flow.isEditMode.value,
isFormLocked: isFormLocked.value,
isFormReadonly: flow.isViewMode.value,
isLoading: isLoading.value,
isSaving: isSaving.value,
isViewMode: flow.isViewMode.value,
pendingDeleteLabel: flow.pendingDeleteLabel.value,
saveSummary: formState.saveSummary.value,
statuses,
}))
const formPanelEvents = {
'clear-field-error': formState.clearFieldError,
close: flow.requestCloseDialog,
'confirm-close': flow.confirmClose,
'confirm-delete': flow.confirmDelete,
'confirm-navigate': flow.confirmNavigate,
'confirm-save': confirmSave,
'confirm-switch': flow.confirmSwitch,
'delete-current': flow.requestDeleteCurrent,
'dialog-visible-change': flow.handleDialogVisibility,
first: () => flow.openEdgeRecord('first'),
last: () => flow.openEdgeRecord('last'),
next: () => flow.openAdjacentRecord('next'),
prev: () => flow.openAdjacentRecord('prev'),
save: requestSaveConfirmation,
'scroll-to-field': scrollToField,
'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:form': (value: StudentFormState) => (formState.form.value = value),
}
return {
commands,
currentPage,
departments,
dialogMode,
dialogSubtitle,
dialogTitle,
dialogVisible,
enrollYears,
flow,
formState,
formPanelEvents,
formPanelProps,
gradeOptions,
isFormLocked,
isFormReadonly: flow.isViewMode,
isLoading,
isSaving,
itemsPerPage,
pageCount,
pageModel,
pageSummary,
requestSaveConfirmation,
confirmSave,
resetSearch,
scrollToField,
search,
searchPanelOpen,
snackbarVisible,
statuses,
students,
tableHeaders,
}
}