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
+125
View File
@@ -0,0 +1,125 @@
import { nextTick, type Ref } from 'vue'
interface UseCrudCommandsOptions<TRecord extends { id: number }, TPayload> {
clearAllErrors: () => void
dialogMode: Ref<'create' | 'edit' | 'view'>
dialogVisible: Ref<boolean>
editingId: Ref<number | null>
fieldErrors: Ref<Record<string, string[]>>
form: Ref<TPayload>
highlightedId: Ref<number | null>
isDirty: Readonly<Ref<boolean>>
isLoading: Ref<boolean>
isSaving: Ref<boolean>
isViewMode: Readonly<Ref<boolean>>
loadDelay?: number
loadSequence: Ref<number>
resetForm: () => void
saveDelay?: number
scrollToField: (field: string) => void
setForm: (payload: TPayload) => void
syncInitialForm: () => void
toFormPayload: (record: TRecord) => TPayload
toSavePayload: (form: TPayload) => TPayload
updateRecord: (id: number, payload: TPayload) => unknown
createRecord: (payload: TPayload) => number
validateForm: () => Array<{ field: string; message: string }>
}
export function useCrudCommands<TRecord extends { id: number }, TPayload>(
options: UseCrudCommandsOptions<TRecord, TPayload>
) {
function openAddDialog() {
options.loadSequence.value += 1
options.dialogMode.value = 'create'
options.editingId.value = null
options.resetForm()
options.isLoading.value = false
options.dialogVisible.value = true
}
function loadRecord(record: TRecord, mode: 'edit' | 'view') {
options.loadSequence.value += 1
const sequence = options.loadSequence.value
options.dialogMode.value = mode
options.editingId.value = record.id
options.dialogVisible.value = true
options.isLoading.value = true
options.clearAllErrors()
window.setTimeout(() => {
if (sequence !== options.loadSequence.value || !options.dialogVisible.value) return
options.setForm(options.toFormPayload(record))
options.syncInitialForm()
options.isLoading.value = false
}, options.loadDelay ?? 350)
}
function openEditDialog(record: TRecord) {
loadRecord(record, 'edit')
}
function openViewDialog(record: TRecord) {
loadRecord(record, 'view')
}
async function requestSaveConfirmation(confirmSaveVisible: Ref<boolean>) {
if (
options.isSaving.value ||
options.isLoading.value ||
!options.isDirty.value ||
options.isViewMode.value
) {
return
}
options.clearAllErrors()
const errors = options.validateForm()
if (errors.length > 0) {
for (const error of errors) {
options.fieldErrors.value[error.field] = [error.message]
}
await nextTick()
const firstError = errors[0]
if (firstError) options.scrollToField(firstError.field)
return
}
confirmSaveVisible.value = true
}
function confirmSave(confirmSaveVisible: Ref<boolean>) {
confirmSaveVisible.value = false
return saveRecord()
}
async function saveRecord() {
if (options.isSaving.value || options.isLoading.value) return
options.isSaving.value = true
await new Promise((resolve) => window.setTimeout(resolve, options.saveDelay ?? 450))
const payload = options.toSavePayload(options.form.value)
if (options.editingId.value) {
const updated = options.updateRecord(options.editingId.value, payload)
if (updated) options.highlightedId.value = options.editingId.value
} else {
const createdId = options.createRecord(payload)
options.highlightedId.value = createdId
}
options.syncInitialForm()
options.dialogVisible.value = false
options.isSaving.value = false
window.setTimeout(() => {
options.highlightedId.value = null
}, 1600)
}
return {
confirmSave,
openAddDialog,
openEditDialog,
openViewDialog,
requestSaveConfirmation,
saveRecord,
}
}
@@ -20,7 +20,7 @@ interface UseMaintenanceCrudFlowOptions<T extends { id: number }> {
onAfterDelete?: (deletedId: number) => void
}
interface UseMaintenanceCrudFlowResult<T extends { id: number }> {
export interface UseMaintenanceCrudFlowResult<T extends { id: number }> {
confirmCloseVisible: Ref<boolean>
confirmSaveVisible: Ref<boolean>
confirmDeleteVisible: Ref<boolean>
@@ -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,
}
}