feat: add SingleRecord component for student maintenance with CRUD functionality

This commit is contained in:
skytek_xinliang
2026-03-30 11:03:01 +08:00
parent 00a7150757
commit 20b093ff73
22 changed files with 51 additions and 51 deletions
@@ -0,0 +1,244 @@
import { computed, type ComputedRef, ref, type Ref } from 'vue'
type DialogMode = 'create' | 'edit' | 'view'
interface UseMaintenanceCrudFlowOptions<T extends { id: number }> {
records: ComputedRef<T[]>
editingId: Ref<number | null>
dialogMode: Ref<DialogMode>
dialogVisible: Ref<boolean>
isLoading: Ref<boolean>
isSaving: Ref<boolean>
isDirty: Readonly<Ref<boolean>> | ComputedRef<boolean>
clearAllErrors: () => void
resetForm: () => void
openEditDialog: (record: T) => void
openViewDialog: (record: T) => void
removeRecord: (id: number) => void
describeRecord: (record: T) => string
onCloseReset?: () => void
onAfterDelete?: (deletedId: number) => void
}
interface UseMaintenanceCrudFlowResult<T extends { id: number }> {
confirmCloseVisible: Ref<boolean>
confirmSaveVisible: Ref<boolean>
confirmDeleteVisible: Ref<boolean>
confirmSwitchVisible: Ref<boolean>
confirmNavigateVisible: Ref<boolean>
pendingDelete: Ref<T | null>
pendingDeleteLabel: ComputedRef<string>
currentRecordIndex: ComputedRef<number>
currentEditingRecord: ComputedRef<T | null>
hasPrevRecord: ComputedRef<boolean>
hasNextRecord: ComputedRef<boolean>
isEditMode: ComputedRef<boolean>
isViewMode: ComputedRef<boolean>
openAdjacentRecord: (direction: 'prev' | 'next') => void
openEdgeRecord: (position: 'first' | 'last') => void
switchToEditMode: () => void
switchToViewMode: () => void
confirmSwitch: () => void
confirmNavigate: () => void
requestDeleteConfirmation: (record: T) => void
requestDeleteCurrent: () => void
confirmDelete: () => void
requestCloseDialog: () => void
confirmClose: () => void
closeDialog: () => void
handleDialogVisibility: (nextValue: boolean) => void
}
export function useMaintenanceCrudFlow<T extends { id: number }>(
options: UseMaintenanceCrudFlowOptions<T>
): UseMaintenanceCrudFlowResult<T> {
const confirmCloseVisible = ref(false)
const confirmSaveVisible = ref(false)
const confirmDeleteVisible = ref(false)
const confirmSwitchVisible = ref(false)
const confirmNavigateVisible = ref(false)
const pendingDelete = ref<T | null>(null) as Ref<T | null>
const pendingSwitchTarget = ref<T | null>(null)
const pendingNavigateTarget = ref<T | null>(null)
const isEditMode = computed(() => options.dialogMode.value === 'edit')
const isViewMode = computed(() => options.dialogMode.value === 'view')
const currentRecordIndex = computed(() =>
options.records.value.findIndex((item) => item.id === options.editingId.value)
)
const currentEditingRecord = computed(
() => options.records.value.find((item) => item.id === options.editingId.value) || null
)
const hasPrevRecord = computed(() => currentRecordIndex.value > 0)
const hasNextRecord = computed(
() =>
currentRecordIndex.value >= 0 && currentRecordIndex.value < options.records.value.length - 1
)
const pendingDeleteLabel = computed(() => {
if (!pendingDelete.value) return '這筆資料'
return options.describeRecord(pendingDelete.value)
})
function openAdjacentRecord(direction: 'prev' | 'next') {
if (!isViewMode.value && !isEditMode.value) return
const index = currentRecordIndex.value
if (index < 0) return
const targetIndex = direction === 'prev' ? index - 1 : index + 1
const target = options.records.value[targetIndex]
if (!target) return
if (isEditMode.value) {
if (options.isDirty.value) {
pendingNavigateTarget.value = target
confirmNavigateVisible.value = true
return
}
options.openEditDialog(target)
return
}
options.openViewDialog(target)
}
function openEdgeRecord(position: 'first' | 'last') {
if (!isViewMode.value && !isEditMode.value) return
if (options.records.value.length === 0) return
const target = position === 'first' ? options.records.value[0] : options.records.value.at(-1)
if (!target) return
if (isEditMode.value) {
if (options.isDirty.value) {
pendingNavigateTarget.value = target
confirmNavigateVisible.value = true
return
}
options.openEditDialog(target)
return
}
options.openViewDialog(target)
}
function switchToEditMode() {
if (!isViewMode.value) return
const current = currentEditingRecord.value
if (!current) return
options.openEditDialog(current)
}
function switchToViewMode() {
if (!isEditMode.value) return
const current = currentEditingRecord.value
if (!current) return
if (options.isDirty.value) {
pendingSwitchTarget.value = current
confirmSwitchVisible.value = true
return
}
options.openViewDialog(current)
}
function confirmSwitch() {
const target = pendingSwitchTarget.value
pendingSwitchTarget.value = null
confirmSwitchVisible.value = false
if (!target) return
options.openViewDialog(target)
}
function confirmNavigate() {
const target = pendingNavigateTarget.value
pendingNavigateTarget.value = null
confirmNavigateVisible.value = false
if (!target) return
options.openEditDialog(target)
}
function requestDeleteConfirmation(record: T) {
pendingDelete.value = record
confirmDeleteVisible.value = true
}
function requestDeleteCurrent() {
const current = currentEditingRecord.value
if (!current) return
requestDeleteConfirmation(current)
}
function closeDialog() {
options.dialogVisible.value = false
options.isLoading.value = false
options.isSaving.value = false
confirmCloseVisible.value = false
confirmSaveVisible.value = false
confirmDeleteVisible.value = false
confirmSwitchVisible.value = false
confirmNavigateVisible.value = false
pendingDelete.value = null
pendingSwitchTarget.value = null
pendingNavigateTarget.value = null
options.dialogMode.value = 'create'
options.editingId.value = null
options.clearAllErrors()
options.resetForm()
options.onCloseReset?.()
}
function confirmDelete() {
if (!pendingDelete.value) return
const deletedId = pendingDelete.value.id
options.removeRecord(deletedId)
pendingDelete.value = null
confirmDeleteVisible.value = false
options.onAfterDelete?.(deletedId)
if (options.editingId.value === deletedId) {
closeDialog()
}
}
function requestCloseDialog() {
if (options.isDirty.value && !options.isSaving.value) {
confirmCloseVisible.value = true
return
}
closeDialog()
}
function confirmClose() {
confirmCloseVisible.value = false
closeDialog()
}
function handleDialogVisibility(nextValue: boolean) {
if (nextValue) {
options.dialogVisible.value = true
return
}
requestCloseDialog()
}
return {
confirmCloseVisible,
confirmSaveVisible,
confirmDeleteVisible,
confirmSwitchVisible,
confirmNavigateVisible,
pendingDelete,
pendingDeleteLabel,
currentRecordIndex,
currentEditingRecord,
hasPrevRecord,
hasNextRecord,
isEditMode,
isViewMode,
openAdjacentRecord,
openEdgeRecord,
switchToEditMode,
switchToViewMode,
confirmSwitch,
confirmNavigate,
requestDeleteConfirmation,
requestDeleteCurrent,
confirmDelete,
requestCloseDialog,
confirmClose,
closeDialog,
handleDialogVisibility,
}
}