feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user