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
@@ -0,0 +1,184 @@
<template>
<v-col class="d-flex align-center bg-surface pr-2 pl-2 pl-m-3 py-1">
<v-btn v-if="isMobile" icon="mdi-menu" size="small" variant="text" @click="emit('toggle-drawer')"></v-btn>
<div v-if="features.showSearch" class="search-input-wrapper">
<v-text-field
v-model="searchValueModel" :aria-label="searchConfig.label" class="search-input" density="compact"
hide-details :placeholder="searchConfig.placeholder" variant="outlined"
@keyup.enter="triggerSearch">
<template v-if="!isMobile" #prepend-inner>
<v-icon size="small">mdi-magnify</v-icon>
</template>
<template #append-inner>
<v-btn :aria-label="searchConfig.label" color="primary" size="small" variant="text" @click="triggerSearch">
開始搜尋
</v-btn>
</template>
</v-text-field>
</div>
<div v-if="features.showToolbarActions" class="top-actions">
<slot name="actions">
<!-- 通知 -->
<v-tooltip location="bottom" :text="toolbarActions.notificationsLabel">
<template #activator="{ props }">
<v-btn
v-bind="props" :aria-label="toolbarActions.notificationsLabel" icon size="small" variant="text"
@click="emit('action', 'notifications')">
<v-badge
v-if="toolbarCounts.notifications" color="error" :content="toolbarCounts.notifications"
offset-x="4" offset-y="-2">
<v-icon>mdi-bell-outline</v-icon>
</v-badge>
<v-icon v-else>mdi-bell-outline</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- 訊息 -->
<v-tooltip location="bottom" :text="toolbarActions.messagesLabel">
<template #activator="{ props }">
<v-btn
v-bind="props" :aria-label="toolbarActions.messagesLabel" icon size="small" variant="text"
@click="emit('action', 'messages')">
<v-badge
v-if="toolbarCounts.messages" color="warning" :content="toolbarCounts.messages" offset-x="4"
offset-y="-2">
<v-icon>mdi-message-text-outline</v-icon>
</v-badge>
<v-icon v-else>mdi-message-text-outline</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- 說明 -->
<v-tooltip v-if="false" location="bottom" :text="toolbarActions.helpLabel">
<template #activator="{ props }">
<v-btn
v-bind="props" :aria-label="toolbarActions.helpLabel" icon size="small" variant="text"
@click="emit('action', 'help')">
<v-icon>mdi-help</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- 設定 -->
<v-menu :close-on-content-click="false" location="bottom end">
<template #activator="{ props: menuProps }">
<v-tooltip location="bottom" :text="toolbarActions.settingsLabel">
<template #activator="{ props: tooltipProps }">
<v-btn
v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsLabel" icon size="small"
variant="text">
<v-icon>mdi-cog-outline</v-icon>
</v-btn>
</template>
</v-tooltip>
</template>
<v-list density="compact" width="180">
<v-list-subheader class="text-subtitle-1 py-2">顯示設定</v-list-subheader>
<v-list-item>
<v-switch v-model="showFavoritesBarModel" color="primary" density="comfortable" hide-details>
<template #label>
<span class="text-body-2" style="width: 8ch;">常用功能</span>
</template>
</v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="showBreadcrumbBarModel" color="primary" density="comfortable" hide-details>
<template #label>
<span class="text-body-2" style="width: 8ch;">路徑</span>
</template>
</v-switch>
</v-list-item>
</v-list>
</v-menu>
<!-- 登出 -->
<v-tooltip location="bottom" :text="logoutLabel">
<template #activator="{ props }">
<v-btn v-bind="props" :aria-label="logoutLabel" icon size="small" variant="text" @click="emit('logout')">
<v-icon>mdi-logout</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip v-if="features.showThemeToggle" location="bottom" :text="themeToggleLabel">
<template #activator="{ props }">
<v-btn v-bind="props" :aria-label="themeToggleLabel" icon variant="text" @click="emit('toggle-theme')">
<v-icon>mdi-palette-outline</v-icon>
</v-btn>
</template>
</v-tooltip>
</slot>
</div>
</v-col>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
isMobile: { type: Boolean, default: false },
features: { type: Object, default: () => ({}) },
searchValue: { type: String, default: '' },
searchConfig: { type: Object, default: () => ({}) },
toolbarActions: { type: Object, default: () => ({}) },
toolbarCounts: { type: Object, default: () => ({}) },
logoutLabel: { type: String, default: '' },
themeToggleLabel: { type: String, default: '' },
showFavoritesBar: { type: Boolean, default: true },
showBreadcrumbBar: { type: Boolean, default: true },
})
const emit = defineEmits([
'toggle-drawer',
'update:searchValue',
'search',
'action',
'logout',
'toggle-theme',
'update:showFavoritesBar',
'update:showBreadcrumbBar',
])
const searchValueModel = computed({
get: () => props.searchValue,
set: (value) => emit('update:searchValue', value),
})
const showFavoritesBarModel = computed({
get: () => props.showFavoritesBar,
set: (value) => emit('update:showFavoritesBar', value),
})
const showBreadcrumbBarModel = computed({
get: () => props.showBreadcrumbBar,
set: (value) => emit('update:showBreadcrumbBar', value),
})
function triggerSearch () {
emit('search')
}
</script>
<style scoped>
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
}
:deep(.search-input-wrapper .v-field--appended) {
padding-inline-end: 4px;
}
.top-actions {
display: flex;
align-items: center;
gap: 8px;
}
</style>