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
+263
View File
@@ -0,0 +1,263 @@
<template>
<v-sheet class="bg-background" v-bind="$attrs">
<!-- Search Filter Section -->
<SKSearchFilter
v-if="showSearchFilter"
:collapse-btn-text="props.collapseBtnText"
:expand-btn-text="props.expandBtnText"
:fields="searchFields"
:reset-btn-text="props.resetBtnText"
:search-btn-text="props.searchBtnText"
:show-expand="true"
:visible-when-collapsed="['roleName', 'roleId']"
@reset="$emit('reset')"
@search="$emit('search', $event)"
/>
<!-- Data Table Section -->
<v-card class="bg-surface">
<SKTableActionBar
v-model:settings-selected-keys="visibleHeaderKeys"
:create-btn-text="props.createBtnText"
:search-visible="showSearchFilter"
:settings-items="headerSettingsItems"
:show-search-toggle="true"
:title="props.listTitle"
@create="emit('create')"
@refresh="emit('refresh')"
@settings="emit('settings')"
@toggle-search="showSearchFilter = !showSearchFilter"
/>
<v-divider></v-divider>
<v-data-table
class="role-table"
:headers="filteredHeaders"
hover
:items="props.roles"
:items-per-page="10"
:items-per-page-options="itemsPerPageOptions"
:items-per-page-text="props.itemsPerPageText"
:loading="props.loading"
>
<!-- Status Slot -->
<template #[`item.status`]="{ item }">
<v-switch
color="primary"
density="compact"
hide-details
:model-value="item.status"
@update:model-value="emit('update:status', item, $event)"
></v-switch>
</template>
<!-- Actions Slot -->
<template #[`item.actions`]="{ item }">
<v-btn
class="px-1"
color="primary"
size="small"
variant="text"
@click="emit('edit', item)"
>
{{ props.editActionText }}
</v-btn>
<v-divider
class="mx-1 d-inline-block"
style="height: 12px; vertical-align: middle"
vertical
></v-divider>
<v-btn
class="px-1"
color="error"
size="small"
variant="text"
@click="emit('delete', item)"
>
{{ props.deleteActionText }}
</v-btn>
</template>
</v-data-table>
</v-card>
</v-sheet>
</template>
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import SKSearchFilter from './base/SKSearchFilter.vue'
import SKTableActionBar from './base/SKTableActionBar.vue'
export interface RoleItem {
name: string
id: string
status: boolean
note: string
createTime: string
}
const showSearchFilter = ref(true)
const props = defineProps({
// Filter Labels & Placeholders
roleNameLabel: { type: String, default: '角色名稱' },
roleNamePlaceholder: { type: String, default: '請輸入' },
roleIdLabel: { type: String, default: '角色ID' },
roleIdPlaceholder: { type: String, default: '請輸入' },
statusLabel: { type: String, default: '狀態' },
statusPlaceholder: { type: String, default: '請選擇' },
statusOptions: { type: Array, default: () => ['已啟用', '已禁用'] },
noteLabel: { type: String, default: '備註' },
notePlaceholder: { type: String, default: '請輸入' },
createTimeLabel: { type: String, default: '建立時間' },
startDateLabel: { type: String, default: '開始日期' },
endDateLabel: { type: String, default: '結束日期' },
datePlaceholder: { type: String, default: '請選擇' },
// Button Texts
searchBtnText: { type: String, default: '搜尋' },
resetBtnText: { type: String, default: '重置' },
expandBtnText: { type: String, default: '展開' },
collapseBtnText: { type: String, default: '收起' },
createBtnText: { type: String, default: '新增角色' },
// Table Texts
listTitle: { type: String, default: '權限列表' },
roleNameHeader: { type: String, default: '角色名稱' },
roleIdHeader: { type: String, default: '角色ID' },
statusHeader: { type: String, default: '狀態' },
noteHeader: { type: String, default: '備註' },
createTimeHeader: { type: String, default: '建立時間' },
actionsHeader: { type: String, default: '操作' },
editActionText: { type: String, default: '修改' },
deleteActionText: { type: String, default: '刪除' },
// Data Table Footer Texts
itemsPerPageText: { type: String, default: '每頁筆數:' },
itemsPerPageAllText: { type: String, default: '全部' },
// Data
roles: {
type: Array as () => RoleItem[],
default: () => [],
},
loading: { type: Boolean, default: false },
})
const emit = defineEmits([
'search',
'reset',
'create',
'edit',
'delete',
'update:status',
'refresh',
'settings',
])
// --- Search Fields Configuration ---
const searchFields = computed(() => [
{
key: 'roleName',
type: 'text' as const,
label: props.roleNameLabel,
placeholder: props.roleNamePlaceholder,
meta: {
cols: 12,
md: 4,
lg: 3,
},
},
{
key: 'roleId',
type: 'text' as const,
label: props.roleIdLabel,
placeholder: props.roleIdPlaceholder,
meta: {
cols: 12,
md: 4,
lg: 3,
},
},
{
key: 'status',
type: 'select' as const,
label: props.statusLabel,
placeholder: props.statusPlaceholder,
items: props.statusOptions,
meta: {
cols: 12,
md: 4,
lg: 3,
},
},
{
key: 'note',
type: 'text' as const,
label: props.noteLabel,
placeholder: props.notePlaceholder,
meta: {
cols: 12,
md: 4,
lg: 3,
},
},
{
type: 'date' as const,
key: 'startDate',
label: props.startDateLabel,
placeholder: props.datePlaceholder,
meta: {
cols: 12,
md: 4,
lg: 3,
},
},
{
type: 'date' as const,
key: 'endDate',
label: props.endDateLabel,
placeholder: props.datePlaceholder,
meta: {
cols: 12,
md: 4,
lg: 3,
},
},
])
// --- Table Config ---
const formattedHeaders = computed(() => [
{ title: props.roleNameHeader, key: 'name', align: 'start' as const },
{ title: props.roleIdHeader, key: 'id', align: 'start' as const },
{ title: props.statusHeader, key: 'status', align: 'start' as const },
{ title: props.noteHeader, key: 'note', align: 'start' as const },
{ title: props.createTimeHeader, key: 'createTime', align: 'start' as const },
{ title: props.actionsHeader, key: 'actions', align: 'end' as const, sortable: false },
])
const visibleHeaderKeys = ref<string[]>([])
watchEffect(() => {
if (visibleHeaderKeys.value.length > 0) {
return
}
visibleHeaderKeys.value = formattedHeaders.value.map((h) => String(h.key))
})
const headerSettingsItems = computed(() =>
formattedHeaders.value
.filter((h) => h.key !== 'actions')
.map((h) => ({ key: String(h.key), label: String(h.title) }))
)
const filteredHeaders = computed(() =>
formattedHeaders.value.filter((h) => visibleHeaderKeys.value.includes(String(h.key)))
)
const itemsPerPageOptions = computed(() =>
[10, 25, 50, 100, -1].map((value) => ({
value,
title: value === -1 ? props.itemsPerPageAllText : String(value),
}))
)
</script>