feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities

This commit is contained in:
skytek_xinliang
2026-03-26 11:24:37 +08:00
parent 507afcc99c
commit 069141794e
116 changed files with 15247 additions and 107 deletions
+253
View File
@@ -0,0 +1,253 @@
<template>
<v-sheet class="bg-surface" v-bind="$attrs">
<v-card border elevation="0">
<!-- Top Action Bar -->
<SKTableActionBar
:create-btn-text="props.createBtnText"
:title="props.listTitle"
@create="emit('create')"
@refresh="emit('refresh')"
@settings="emit('settings')"
/>
<SKFormEditDialog
v-model="addSubDialogOpen"
:cancel-text="props.dialogCancelText"
:confirm-text="props.dialogConfirmText"
:item="addSubDraftItem"
:loading="props.dialogLoading"
:show-permission="false"
:status-label-text="props.dialogStatusLabelText"
:status-options="props.statusOptions"
:title-text="props.addSubDialogTitleText"
@submit="onAddSubSubmit"
>
<template #fields="{ form }">
<v-text-field
v-model="form.name"
density="comfortable"
hide-details
:label="props.dialogNameLabelText"
variant="outlined"
/>
<v-textarea
v-model="form.note"
density="comfortable"
hide-details
:label="props.dialogNoteLabelText"
rows="3"
variant="outlined"
/>
</template>
</SKFormEditDialog>
<SKFormEditDialog
v-model="editDialogOpen"
:cancel-text="props.dialogCancelText"
:confirm-text="props.dialogConfirmText"
:item="selectedItem"
:loading="props.dialogLoading"
:show-permission="false"
:status-label-text="props.dialogStatusLabelText"
:status-options="props.statusOptions"
:title-text="props.editDialogTitleText"
@submit="onEditSubmit"
>
<template #fields="{ form }">
<v-text-field
v-model="form.name"
density="comfortable"
hide-details
:label="props.dialogNameLabelText"
variant="outlined"
/>
<v-textarea
v-model="form.note"
density="comfortable"
hide-details
:label="props.dialogNoteLabelText"
rows="3"
variant="outlined"
/>
</template>
</SKFormEditDialog>
<!-- Tree Table -->
<SKTreeTable
:headers="formattedHeaders"
:items="props.items"
:loading="props.loading"
table-class="dept-table"
:tree-column-keys="['name']"
@toggle-expand="emit('toggle-expand', $event)"
>
<!-- Status Column -->
<template #[`item.status`]="{ item }">
<v-chip
:color="isEnabledStatus(item.status) ? 'success' : 'error'"
label
size="small"
variant="tonal"
>
{{ isEnabledStatus(item.status) ? props.statusEnabledText : props.statusDisabledText }}
</v-chip>
</template>
<!-- Actions Column -->
<template #[`item.actions`]="{ item }">
<v-btn class="px-1" color="primary" size="small" variant="text" @click="openAddSub(item)">
{{ props.addSubActionText }}
</v-btn>
<v-btn class="px-1" color="primary" size="small" variant="text" @click="openEdit(item)">
{{ props.editActionText }}
</v-btn>
<v-btn
class="px-1"
color="error"
size="small"
variant="text"
@click="emit('delete', item)"
>
{{ props.deleteActionText }}
</v-btn>
</template>
</SKTreeTable>
</v-card>
</v-sheet>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
import { computed, ref } from 'vue'
import SKFormEditDialog from './base/SKFormEditDialog.vue'
import SKTableActionBar from './base/SKTableActionBar.vue'
import SKTreeTable from './base/SKTreeTable.vue'
export interface DeptItem {
id: string | number
name: string
status: string | number
createTime: string
note: string
children?: DeptItem[]
[key: string]: unknown
}
const props = defineProps({
items: { type: Array as () => DeptItem[], default: () => [] },
loading: { type: Boolean, default: false },
// Text Props
listTitle: { type: String, default: '部門列表' },
createBtnText: { type: String, default: '新增部門' },
addSubActionText: { type: String, default: '新增下級' },
editActionText: { type: String, default: '修改' },
deleteActionText: { type: String, default: '刪除' },
statusEnabledText: { type: String, default: '已啟用' },
statusDisabledText: { type: String, default: '已禁用' },
// Dialog Props
addSubDialogTitleText: { type: String, default: '新增下級' },
editDialogTitleText: { type: String, default: '編輯' },
dialogStatusLabelText: { type: String, default: '狀態' },
dialogNameLabelText: { type: String, default: '部門名稱' },
dialogNoteLabelText: { type: String, default: '備註' },
dialogCancelText: { type: String, default: '取消' },
dialogConfirmText: { type: String, default: '確認' },
dialogLoading: { type: Boolean, default: false },
statusEnabledValue: { type: [String, Number] as PropType<string | number>, default: undefined },
statusOptions: {
type: Array as () => Array<string | number | { title: string; value: string | number }>,
default: () => [],
},
// Header Texts
nameHeader: { type: String, default: '部門名稱' },
statusHeader: { type: String, default: '狀態' },
createTimeHeader: { type: String, default: '創建時間' },
noteHeader: { type: String, default: '備註' },
actionsHeader: { type: String, default: '操作' },
})
const emit = defineEmits([
'create',
'add-sub',
'edit',
'delete',
'refresh',
'settings',
'toggle-expand',
])
function normalizeOptions (options: Array<string | number | { title: string; value: string | number }>) {
return options.map((o) => {
if (typeof o === 'string' || typeof o === 'number') {
return { title: String(o), value: o }
}
return o
})
}
const normalizedStatusOptions = computed(() => normalizeOptions(props.statusOptions))
const resolvedStatusEnabledValue = computed(() => {
if (props.statusEnabledValue !== undefined) return props.statusEnabledValue
return normalizedStatusOptions.value[0]?.value ?? 'enable'
})
const isEnabledStatus = (status: unknown) => status === resolvedStatusEnabledValue.value
const addSubDialogOpen = ref(false)
const editDialogOpen = ref(false)
const addSubParentItem = ref<DeptItem | null>(null)
const addSubDraftItem = ref<Record<string, unknown> | null>(null)
const selectedItem = ref<DeptItem | null>(null)
function openAddSub (item: DeptItem) {
addSubParentItem.value = item
addSubDraftItem.value = {
name: '',
note: '',
status: resolvedStatusEnabledValue.value,
}
addSubDialogOpen.value = true
}
function openEdit (item: DeptItem) {
selectedItem.value = item
editDialogOpen.value = true
}
function onAddSubSubmit (payload: Record<string, unknown>) {
if (!addSubParentItem.value) return
const newItem: DeptItem = {
id: Date.now(),
name: String(payload.name ?? ''),
note: String(payload.note ?? ''),
status: (payload.status as string | number | undefined) ?? resolvedStatusEnabledValue.value,
createTime: new Date().toISOString(),
children: [],
}
emit('add-sub', addSubParentItem.value, newItem)
}
function onEditSubmit (updated: Record<string, unknown>) {
emit('edit', updated)
}
// --- Table Config ---
const formattedHeaders = computed(() => [
{ title: props.nameHeader, key: 'name', align: 'start' as const, minWidth: '250px' },
{ title: props.statusHeader, key: 'status', align: 'center' as const, width: '100px' },
{ title: props.createTimeHeader, key: 'createTime', align: 'start' as const },
{ title: props.noteHeader, key: 'note', align: 'start' as const },
{
title: props.actionsHeader,
key: 'actions',
align: 'center' as const,
width: '250px',
sortable: false,
},
])
</script>