refactor: update breadcrumb bar visibility handling across components
This commit is contained in:
@@ -120,7 +120,7 @@
|
|||||||
:logout-label="logoutLabel"
|
:logout-label="logoutLabel"
|
||||||
:search-config="searchConfig"
|
:search-config="searchConfig"
|
||||||
:search-value="searchValue"
|
:search-value="searchValue"
|
||||||
:show-breadcrumb-bar="showBreadcrumbBar"
|
:breadcrumb-bar-visible="breadcrumbBarVisible"
|
||||||
:show-favorites-bar="showFavoritesBar"
|
:show-favorites-bar="showFavoritesBar"
|
||||||
:theme-toggle-label="themeToggleLabel"
|
:theme-toggle-label="themeToggleLabel"
|
||||||
:toolbar-actions="toolbarActions"
|
:toolbar-actions="toolbarActions"
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
@toggle-drawer="drawer = !drawer"
|
@toggle-drawer="drawer = !drawer"
|
||||||
@toggle-theme="toggleTheme"
|
@toggle-theme="toggleTheme"
|
||||||
@update:search-value="searchValue = $event"
|
@update:search-value="searchValue = $event"
|
||||||
@update:show-breadcrumb-bar="showBreadcrumbBar = $event"
|
@update:breadcrumb-bar-visible="breadcrumbBarVisible = $event"
|
||||||
@update:show-favorites-bar="showFavoritesBar = $event"
|
@update:show-favorites-bar="showFavoritesBar = $event"
|
||||||
>
|
>
|
||||||
<template v-if="$slots.actions" #actions>
|
<template v-if="$slots.actions" #actions>
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
:breadcrumb-items="breadcrumbItems"
|
:breadcrumb-items="breadcrumbItems"
|
||||||
:features="features"
|
:features="features"
|
||||||
:is-mobile="isMobile"
|
:is-mobile="isMobile"
|
||||||
:show-breadcrumb-bar="showBreadcrumbBar"
|
:breadcrumb-bar-visible="breadcrumbBarVisible"
|
||||||
:show-favorites-bar="showFavoritesBar"
|
:show-favorites-bar="showFavoritesBar"
|
||||||
@toggle-favorites-bar="toggleFavoritesBar"
|
@toggle-favorites-bar="toggleFavoritesBar"
|
||||||
>
|
>
|
||||||
@@ -389,7 +389,7 @@ const {
|
|||||||
mobileMenuLevels,
|
mobileMenuLevels,
|
||||||
openMobileFavoritesPanel,
|
openMobileFavoritesPanel,
|
||||||
opened,
|
opened,
|
||||||
showBreadcrumbBar,
|
breadcrumbBarVisible,
|
||||||
showFavoritesBar,
|
showFavoritesBar,
|
||||||
toggleFavoritesBar,
|
toggleFavoritesBar,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-col
|
<v-col
|
||||||
v-if="features.showBreadcrumb && showBreadcrumbBar && !isMobile"
|
v-if="features.showBreadcrumb && breadcrumbBarVisible && !isMobile"
|
||||||
class="d-flex align-center justify-space-between pr-2 pl-3 py-1 bg-surface">
|
class="d-flex align-center justify-space-between pr-2 pl-3 py-1 bg-surface">
|
||||||
<v-breadcrumbs class="pa-0" density="compact" :items="breadcrumbItems">
|
<v-breadcrumbs class="pa-0" density="compact" :items="breadcrumbItems">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
@@ -32,7 +32,7 @@ import { mdiChevronRight } from '@mdi/js'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
features?: AdminLayoutFeatures
|
features?: AdminLayoutFeatures
|
||||||
showBreadcrumbBar?: boolean
|
breadcrumbBarVisible?: boolean
|
||||||
isMobile?: boolean
|
isMobile?: boolean
|
||||||
breadcrumbItems?: AdminLayoutBreadcrumbItem[]
|
breadcrumbItems?: AdminLayoutBreadcrumbItem[]
|
||||||
showFavoritesBar?: boolean
|
showFavoritesBar?: boolean
|
||||||
@@ -47,7 +47,7 @@ withDefaults(defineProps<Props>(), {
|
|||||||
showToolbarActions: true,
|
showToolbarActions: true,
|
||||||
showUserInfo: true,
|
showUserInfo: true,
|
||||||
}),
|
}),
|
||||||
showBreadcrumbBar: true,
|
breadcrumbBarVisible: true,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
breadcrumbItems: () => [],
|
breadcrumbItems: () => [],
|
||||||
showFavoritesBar: true,
|
showFavoritesBar: true,
|
||||||
@@ -60,8 +60,12 @@ const emit = defineEmits<{
|
|||||||
function getBreadcrumbItem (item: unknown): AdminLayoutBreadcrumbItem | null {
|
function getBreadcrumbItem (item: unknown): AdminLayoutBreadcrumbItem | null {
|
||||||
if (typeof item !== 'object' || item === null) return null
|
if (typeof item !== 'object' || item === null) return null
|
||||||
|
|
||||||
|
if ('title' in item) {
|
||||||
|
return item as AdminLayoutBreadcrumbItem
|
||||||
|
}
|
||||||
|
|
||||||
const raw = (item as { raw?: unknown }).raw
|
const raw = (item as { raw?: unknown }).raw
|
||||||
if (typeof raw !== 'object' || raw === null) return null
|
if (typeof raw !== 'object' || raw === null || !('title' in raw)) return null
|
||||||
|
|
||||||
return raw as AdminLayoutBreadcrumbItem
|
return raw as AdminLayoutBreadcrumbItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
<v-btn v-if="isMobile" :icon="mdiMenu" size="small" variant="text" @click="emit('toggle-drawer')"></v-btn>
|
<v-btn v-if="isMobile" :icon="mdiMenu" size="small" variant="text" @click="emit('toggle-drawer')"></v-btn>
|
||||||
|
|
||||||
<div v-if="features.showSearch" class="search-input-wrapper">
|
<div v-if="features.showSearch" class="search-input-wrapper">
|
||||||
|
<span id="admin-layout-search-label" class="sr-only">{{ searchConfig.label }}</span>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="searchValueModel" :aria-label="searchConfig.label" class="search-input" density="compact"
|
v-model="searchValueModel" aria-labelledby="admin-layout-search-label" :aria-label="searchConfig.label"
|
||||||
hide-details :placeholder="searchConfig.placeholder" variant="outlined"
|
class="search-input" density="compact" hide-details name="layout-search"
|
||||||
|
:placeholder="searchConfig.placeholder" variant="outlined"
|
||||||
@keyup.enter="triggerSearch">
|
@keyup.enter="triggerSearch">
|
||||||
<template v-if="!isMobile" #prepend-inner>
|
<template v-if="!isMobile" #prepend-inner>
|
||||||
<v-icon size="small" :icon="mdiMagnify" />
|
<v-icon size="small" :icon="mdiMagnify" />
|
||||||
@@ -87,7 +89,7 @@ v-bind="{ ...menuProps, ...tooltipProps }" :aria-label="toolbarActions.settingsL
|
|||||||
</v-switch>
|
</v-switch>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<v-switch v-model="showBreadcrumbBarModel" color="primary" density="comfortable" hide-details>
|
<v-switch v-model="breadcrumbBarVisibleModel" color="primary" density="comfortable" hide-details>
|
||||||
<template #label>
|
<template #label>
|
||||||
<span class="text-body-2" style="width: 8ch;">路徑</span>
|
<span class="text-body-2" style="width: 8ch;">路徑</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -132,7 +134,7 @@ interface Props {
|
|||||||
logoutLabel?: string
|
logoutLabel?: string
|
||||||
themeToggleLabel?: string
|
themeToggleLabel?: string
|
||||||
showFavoritesBar?: boolean
|
showFavoritesBar?: boolean
|
||||||
showBreadcrumbBar?: boolean
|
breadcrumbBarVisible?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@@ -163,7 +165,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
logoutLabel: '',
|
logoutLabel: '',
|
||||||
themeToggleLabel: '',
|
themeToggleLabel: '',
|
||||||
showFavoritesBar: true,
|
showFavoritesBar: true,
|
||||||
showBreadcrumbBar: true,
|
breadcrumbBarVisible: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -174,7 +176,7 @@ const emit = defineEmits<{
|
|||||||
logout: []
|
logout: []
|
||||||
'toggle-theme': []
|
'toggle-theme': []
|
||||||
'update:showFavoritesBar': [value: boolean]
|
'update:showFavoritesBar': [value: boolean]
|
||||||
'update:showBreadcrumbBar': [value: boolean]
|
'update:breadcrumbBarVisible': [value: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const searchValueModel = computed({
|
const searchValueModel = computed({
|
||||||
@@ -187,9 +189,9 @@ const showFavoritesBarModel = computed({
|
|||||||
set: (value) => emit('update:showFavoritesBar', value),
|
set: (value) => emit('update:showFavoritesBar', value),
|
||||||
})
|
})
|
||||||
|
|
||||||
const showBreadcrumbBarModel = computed({
|
const breadcrumbBarVisibleModel = computed({
|
||||||
get: () => props.showBreadcrumbBar,
|
get: () => props.breadcrumbBarVisible,
|
||||||
set: (value) => emit('update:showBreadcrumbBar', value),
|
set: (value) => emit('update:breadcrumbBarVisible', value),
|
||||||
})
|
})
|
||||||
|
|
||||||
function triggerSearch () {
|
function triggerSearch () {
|
||||||
@@ -214,4 +216,16 @@ function triggerSearch () {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-list v-model:opened="openedModel" color="primary" density="compact" prepend-gap="8">
|
<v-list v-model:opened="openedModel" color="primary" density="compact" prepend-gap="8">
|
||||||
<template v-for="item in menuItems" :key="item.path ?? item.title">
|
<template v-for="item in menuItems" :key="item.path ?? item.title">
|
||||||
<v-list-group v-if="item.subItems?.length" class="menu-group" :value="`menu:${item.path ?? item.title}`">
|
<v-list-group v-if="item.subItems?.length" :id="getGroupId(item)" :value="getGroupValue(item)">
|
||||||
<template #activator="{ props: activatorProps }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-bind="isShrink ? undefined : activatorProps" :class="{ 'px-0': isShrink }"
|
v-bind="isShrink ? undefined : activatorProps" :class="{ 'px-0': isShrink }"
|
||||||
@@ -27,7 +27,8 @@ v-if="!isShrink && getItemCount(item) > 0" class="menu-count" color="secondary"
|
|||||||
<template v-for="subItem in item.subItems" :key="subItem.path ?? subItem.title">
|
<template v-for="subItem in item.subItems" :key="subItem.path ?? subItem.title">
|
||||||
<v-list-group
|
<v-list-group
|
||||||
v-if="subItem.subItems?.length"
|
v-if="subItem.subItems?.length"
|
||||||
class="menu-group" :value="`menu:${item.path ?? item.title}::${subItem.path ?? subItem.title}`">
|
:id="getGroupId(subItem, getGroupId(item))"
|
||||||
|
:value="getGroupValue(subItem, getGroupValue(item))">
|
||||||
<template #activator="{ props: subProps }">
|
<template #activator="{ props: subProps }">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-bind="subProps" :link="isNavigable(subItem)"
|
v-bind="subProps" :link="isNavigable(subItem)"
|
||||||
@@ -151,6 +152,37 @@ function getItemCount (item: AdminLayoutMenuItem) {
|
|||||||
}, 0)
|
}, 0)
|
||||||
return countLeaf(item.subItems)
|
return countLeaf(item.subItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGroupValue(item: AdminLayoutMenuItem, parentKey?: string) {
|
||||||
|
const rawKey = item.path ?? item.title ?? 'group'
|
||||||
|
const normalizedKey = Array.from(rawKey.trim())
|
||||||
|
.map((char) => {
|
||||||
|
if (/[a-z0-9]/i.test(char)) {
|
||||||
|
return char.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return `u${char.codePointAt(0)?.toString(16)}`
|
||||||
|
})
|
||||||
|
.join('-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
|
||||||
|
if (!parentKey) {
|
||||||
|
return `menu-${normalizedKey || 'group'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${parentKey}__${normalizedKey || 'group'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGroupId(item: AdminLayoutMenuItem, parentId?: string) {
|
||||||
|
const groupId = getGroupValue(item).replace(/^menu-/, 'group-')
|
||||||
|
|
||||||
|
if (!parentId) {
|
||||||
|
return `nav-${groupId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${parentId}__${groupId}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -92,11 +92,13 @@
|
|||||||
<span v-if="isFormReadonly">{{ course.credits }}</span>
|
<span v-if="isFormReadonly">{{ course.credits }}</span>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-else
|
v-else
|
||||||
|
:aria-label="`${semester.semesterName} ${course.name} 學分`"
|
||||||
density="compact"
|
density="compact"
|
||||||
:disabled="isFormLocked"
|
:disabled="isFormLocked"
|
||||||
hide-details
|
hide-details
|
||||||
hide-spin-buttons
|
hide-spin-buttons
|
||||||
:model-value="course.credits"
|
:model-value="course.credits"
|
||||||
|
:name="`semester-${semester.id}-course-${originalIndex}-credits`"
|
||||||
type="number"
|
type="number"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
@@ -111,11 +113,13 @@
|
|||||||
<span v-if="isFormReadonly">{{ course.score }}</span>
|
<span v-if="isFormReadonly">{{ course.score }}</span>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-else
|
v-else
|
||||||
|
:aria-label="`${semester.semesterName} ${course.name} 分數`"
|
||||||
density="compact"
|
density="compact"
|
||||||
:disabled="isFormLocked"
|
:disabled="isFormLocked"
|
||||||
hide-details
|
hide-details
|
||||||
hide-spin-buttons
|
hide-spin-buttons
|
||||||
:model-value="course.score"
|
:model-value="course.score"
|
||||||
|
:name="`semester-${semester.id}-course-${originalIndex}-score`"
|
||||||
type="number"
|
type="number"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
|
|||||||
@@ -50,16 +50,16 @@ class="border rounded" density="compact" :headers="headers" hide-default-footer
|
|||||||
<template #[`item.credits`]="slotProps">
|
<template #[`item.credits`]="slotProps">
|
||||||
<span v-if="isFormReadonly">{{ slotProps.item.credits }}</span>
|
<span v-if="isFormReadonly">{{ slotProps.item.credits }}</span>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-else density="compact" :disabled="isFormLocked" hide-details
|
v-else :aria-label="`${slotProps.item.semesterName} ${slotProps.item.name} 學分`" density="compact" :disabled="isFormLocked" hide-details
|
||||||
hide-spin-buttons :model-value="slotProps.item.credits" type="number" variant="outlined" @update:model-value="
|
hide-spin-buttons :model-value="slotProps.item.credits" :name="`semester-${slotProps.item.semesterId}-course-${slotProps.item.courseIndex}-credits`" type="number" variant="outlined" @update:model-value="
|
||||||
(value) => $emit('update-course', slotProps.item.semesterId, slotProps.item.courseIndex, { credits: Number(value) || 0 })
|
(value) => $emit('update-course', slotProps.item.semesterId, slotProps.item.courseIndex, { credits: Number(value) || 0 })
|
||||||
" />
|
" />
|
||||||
</template>
|
</template>
|
||||||
<template #[`item.score`]="slotProps">
|
<template #[`item.score`]="slotProps">
|
||||||
<span v-if="isFormReadonly">{{ slotProps.item.score }}</span>
|
<span v-if="isFormReadonly">{{ slotProps.item.score }}</span>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-else density="compact" :disabled="isFormLocked" hide-details
|
v-else :aria-label="`${slotProps.item.semesterName} ${slotProps.item.name} 分數`" density="compact" :disabled="isFormLocked" hide-details
|
||||||
hide-spin-buttons :model-value="slotProps.item.score" type="number" variant="outlined" @update:model-value="
|
hide-spin-buttons :model-value="slotProps.item.score" :name="`semester-${slotProps.item.semesterId}-course-${slotProps.item.courseIndex}-score`" type="number" variant="outlined" @update:model-value="
|
||||||
(value) => $emit('update-course', slotProps.item.semesterId, slotProps.item.courseIndex, { score: Number(value) || 0 })
|
(value) => $emit('update-course', slotProps.item.semesterId, slotProps.item.courseIndex, { score: Number(value) || 0 })
|
||||||
" />
|
" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function useAdminLayoutState(options: UseAdminLayoutStateOptions) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const showBreadcrumbBar = computed({
|
const breadcrumbBarVisible = computed({
|
||||||
get: () => options.breadcrumbBarVisible ?? localBreadcrumbBarVisible.value,
|
get: () => options.breadcrumbBarVisible ?? localBreadcrumbBarVisible.value,
|
||||||
set: (value: boolean) => {
|
set: (value: boolean) => {
|
||||||
if (options.breadcrumbBarVisible === null) {
|
if (options.breadcrumbBarVisible === null) {
|
||||||
@@ -203,7 +203,7 @@ export function useAdminLayoutState(options: UseAdminLayoutStateOptions) {
|
|||||||
mobileMenuLevels,
|
mobileMenuLevels,
|
||||||
openMobileFavoritesPanel,
|
openMobileFavoritesPanel,
|
||||||
opened,
|
opened,
|
||||||
showBreadcrumbBar,
|
breadcrumbBarVisible,
|
||||||
showFavoritesBar,
|
showFavoritesBar,
|
||||||
toggleFavoritesBar,
|
toggleFavoritesBar,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
|
|||||||
@@ -4,28 +4,28 @@
|
|||||||
@create="openAddDialog" @toggle-search="searchPanelOpen = !searchPanelOpen">
|
@create="openAddDialog" @toggle-search="searchPanelOpen = !searchPanelOpen">
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="search.studentId" density="compact" hide-details placeholder="例如:S2024001"
|
id="search-student-id" v-model="search.studentId" aria-labelledby="search-student-id-label" density="compact" hide-details name="searchStudentId" placeholder="例如:S2024001"
|
||||||
variant="outlined" />
|
variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
<div id="search-name-label" class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
||||||
<v-text-field v-model="search.name" density="compact" hide-details placeholder="例如:王小明" variant="outlined" />
|
<v-text-field id="search-name" v-model="search.name" aria-labelledby="search-name-label" density="compact" hide-details name="searchName" placeholder="例如:王小明" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
<div id="search-department-label" class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
||||||
<v-select v-model="search.department" density="compact" hide-details :items="departments" variant="outlined" />
|
<v-select id="search-department" v-model="search.department" aria-labelledby="search-department-label" density="compact" hide-details :items="departments" name="searchDepartment" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
<div id="search-grade-label" class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="search.grade" density="compact" hide-details item-title="title" item-value="value"
|
id="search-grade" v-model="search.grade" aria-labelledby="search-grade-label" density="compact" hide-details item-title="title" item-value="value"
|
||||||
:items="gradeOptions" variant="outlined" />
|
:items="gradeOptions" name="searchGrade" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
<div id="search-status-label" class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
||||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
<v-select id="search-status" v-model="search.status" aria-labelledby="search-status-label" density="compact" hide-details :items="statuses" name="searchStatus" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||||
@@ -34,8 +34,8 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
|||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
class="student-table" density="compact" fixed-header :headers="tableHeaders" height="100%"
|
v-model:page="currentPage" class="student-table" density="compact" fixed-header :headers="tableHeaders" height="100%"
|
||||||
:items="students" :items-per-page="10" items-per-page-text="每頁筆數" page-text="第 {0}-{1} 筆 / 共 {2} 筆"
|
hide-default-footer :items="students" :items-per-page="itemsPerPage"
|
||||||
:row-props="rowProps">
|
:row-props="rowProps">
|
||||||
<template #[`item.grade`]="{ item }">
|
<template #[`item.grade`]="{ item }">
|
||||||
{{ gradeLabel(item.grade) }}
|
{{ gradeLabel(item.grade) }}
|
||||||
@@ -60,6 +60,20 @@ color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<div class="d-flex align-center justify-space-between px-4 py-3">
|
||||||
|
<div class="text-body-2 text-medium-emphasis">
|
||||||
|
{{ pageSummary }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<v-btn :disabled="currentPage <= 1" size="small" variant="text" @click="currentPage = 1">第一頁</v-btn>
|
||||||
|
<v-btn :disabled="currentPage <= 1" size="small" variant="text" @click="currentPage -= 1">上一頁</v-btn>
|
||||||
|
<span class="text-body-2">{{ currentPage }} / {{ pageCount }}</span>
|
||||||
|
<v-btn :disabled="currentPage >= pageCount" size="small" variant="text" @click="currentPage += 1">下一頁</v-btn>
|
||||||
|
<v-btn :disabled="currentPage >= pageCount" size="small" variant="text" @click="currentPage = pageCount">最後頁</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</template>
|
</template>
|
||||||
</mnt-page-cards>
|
</mnt-page-cards>
|
||||||
@@ -209,7 +223,7 @@ v-if="!isViewMode" color="primary" :disabled="!isDirty || isLoading" :loading="i
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
||||||
@@ -270,6 +284,17 @@ const studentStore = useStudentStore()
|
|||||||
const semesterStore = useSemesterStore()
|
const semesterStore = useSemesterStore()
|
||||||
const students = computed(() => studentStore.students)
|
const students = computed(() => studentStore.students)
|
||||||
type StudentPayload = Omit<StudentRecord, 'id'>
|
type StudentPayload = Omit<StudentRecord, 'id'>
|
||||||
|
const itemsPerPage = 10
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageCount = computed(() => Math.max(1, Math.ceil(students.value.length / itemsPerPage)))
|
||||||
|
const pageSummary = computed(() => {
|
||||||
|
const total = students.value.length
|
||||||
|
if (total === 0) return '第 0-0 筆 / 共 0 筆'
|
||||||
|
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage + 1
|
||||||
|
const end = Math.min(currentPage.value * itemsPerPage, total)
|
||||||
|
return `第 ${start}-${end} 筆 / 共 ${total} 筆`
|
||||||
|
})
|
||||||
|
|
||||||
// 彈窗狀態與流程控制
|
// 彈窗狀態與流程控制
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
@@ -452,6 +477,12 @@ function resetSearch () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(pageCount, (value) => {
|
||||||
|
if (currentPage.value > value) {
|
||||||
|
currentPage.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 新增:開啟彈窗,使用預設值
|
// 新增:開啟彈窗,使用預設值
|
||||||
function openAddDialog () {
|
function openAddDialog () {
|
||||||
loadSequence.value += 1
|
loadSequence.value += 1
|
||||||
|
|||||||
@@ -4,28 +4,28 @@
|
|||||||
@create="openAddDialog" @toggle-search="searchPanelOpen = !searchPanelOpen">
|
@create="openAddDialog" @toggle-search="searchPanelOpen = !searchPanelOpen">
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="search.studentId" density="compact" hide-details placeholder="例如:S2024001"
|
id="search-student-id" v-model="search.studentId" aria-labelledby="search-student-id-label" density="compact" hide-details name="searchStudentId" placeholder="例如:S2024001"
|
||||||
variant="outlined" />
|
variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
<div id="search-name-label" class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
||||||
<v-text-field v-model="search.name" density="compact" hide-details placeholder="例如:王小明" variant="outlined" />
|
<v-text-field id="search-name" v-model="search.name" aria-labelledby="search-name-label" density="compact" hide-details name="searchName" placeholder="例如:王小明" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
<div id="search-department-label" class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
||||||
<v-select v-model="search.department" density="compact" hide-details :items="departments" variant="outlined" />
|
<v-select id="search-department" v-model="search.department" aria-labelledby="search-department-label" density="compact" hide-details :items="departments" name="searchDepartment" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
<div id="search-grade-label" class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="search.grade" density="compact" hide-details item-title="title" item-value="value"
|
id="search-grade" v-model="search.grade" aria-labelledby="search-grade-label" density="compact" hide-details item-title="title" item-value="value"
|
||||||
:items="gradeOptions" variant="outlined" />
|
:items="gradeOptions" name="searchGrade" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
<div id="search-status-label" class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
||||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
<v-select id="search-status" v-model="search.status" aria-labelledby="search-status-label" density="compact" hide-details :items="statuses" name="searchStatus" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||||
@@ -34,8 +34,8 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
|||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
class="student-table" density="compact" fixed-header :headers="tableHeaders" height="100%"
|
v-model:page="currentPage" class="student-table" density="compact" fixed-header :headers="tableHeaders" height="100%"
|
||||||
:items="students" :items-per-page="10" items-per-page-text="每頁筆數" page-text="第 {0}-{1} 筆 / 共 {2} 筆"
|
hide-default-footer :items="students" :items-per-page="itemsPerPage"
|
||||||
:row-props="rowProps">
|
:row-props="rowProps">
|
||||||
<template #[`item.grade`]="{ item }">
|
<template #[`item.grade`]="{ item }">
|
||||||
{{ gradeLabel(item.grade) }}
|
{{ gradeLabel(item.grade) }}
|
||||||
@@ -60,6 +60,20 @@ color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<div class="d-flex align-center justify-space-between px-4 py-3">
|
||||||
|
<div class="text-body-2 text-medium-emphasis">
|
||||||
|
{{ pageSummary }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<v-btn :disabled="currentPage <= 1" size="small" variant="text" @click="currentPage = 1">第一頁</v-btn>
|
||||||
|
<v-btn :disabled="currentPage <= 1" size="small" variant="text" @click="currentPage -= 1">上一頁</v-btn>
|
||||||
|
<span class="text-body-2">{{ currentPage }} / {{ pageCount }}</span>
|
||||||
|
<v-btn :disabled="currentPage >= pageCount" size="small" variant="text" @click="currentPage += 1">下一頁</v-btn>
|
||||||
|
<v-btn :disabled="currentPage >= pageCount" size="small" variant="text" @click="currentPage = pageCount">最後頁</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</template>
|
</template>
|
||||||
</mnt-page-cards>
|
</mnt-page-cards>
|
||||||
@@ -236,9 +250,10 @@ v-model.number="addCourseForm.score" density="comfortable" hide-spin-buttons lab
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiBookPlus, mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
import { mdiBookPlus, mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
import CommonConfirmDialog from '@/components/maintenance/CommonConfirmDialog.vue'
|
||||||
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
||||||
import MaintenanceStudentFormFields from '@/components/maintenance/MaintenanceStudentFormFields.vue'
|
import MaintenanceStudentFormFields from '@/components/maintenance/MaintenanceStudentFormFields.vue'
|
||||||
import MasterDetailBSemesterMobilePanel from '@/components/maintenance/master-detail-b/MasterDetailBSemesterMobilePanel.vue'
|
import MasterDetailBSemesterMobilePanel from '@/components/maintenance/master-detail-b/MasterDetailBSemesterMobilePanel.vue'
|
||||||
@@ -297,6 +312,17 @@ const studentStore = useStudentStore()
|
|||||||
const semesterStore = useSemesterStore()
|
const semesterStore = useSemesterStore()
|
||||||
const students = computed(() => studentStore.students)
|
const students = computed(() => studentStore.students)
|
||||||
type StudentPayload = Omit<StudentRecord, 'id'>
|
type StudentPayload = Omit<StudentRecord, 'id'>
|
||||||
|
const itemsPerPage = 10
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageCount = computed(() => Math.max(1, Math.ceil(students.value.length / itemsPerPage)))
|
||||||
|
const pageSummary = computed(() => {
|
||||||
|
const total = students.value.length
|
||||||
|
if (total === 0) return '第 0-0 筆 / 共 0 筆'
|
||||||
|
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage + 1
|
||||||
|
const end = Math.min(currentPage.value * itemsPerPage, total)
|
||||||
|
return `第 ${start}-${end} 筆 / 共 ${total} 筆`
|
||||||
|
})
|
||||||
|
|
||||||
// 彈窗狀態與流程控制
|
// 彈窗狀態與流程控制
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
@@ -531,6 +557,12 @@ function resetSearch () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(pageCount, (value) => {
|
||||||
|
if (currentPage.value > value) {
|
||||||
|
currentPage.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 新增:開啟彈窗,使用預設值
|
// 新增:開啟彈窗,使用預設值
|
||||||
function openAddDialog () {
|
function openAddDialog () {
|
||||||
loadSequence.value += 1
|
loadSequence.value += 1
|
||||||
|
|||||||
@@ -4,28 +4,28 @@
|
|||||||
@create="openAddDialog" @toggle-search="searchPanelOpen = !searchPanelOpen">
|
@create="openAddDialog" @toggle-search="searchPanelOpen = !searchPanelOpen">
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="search.studentId" density="compact" hide-details placeholder="例如:S2024001"
|
id="search-student-id" v-model="search.studentId" aria-labelledby="search-student-id-label" density="compact" hide-details name="searchStudentId" placeholder="例如:S2024001"
|
||||||
variant="outlined" />
|
variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
<div id="search-name-label" class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
||||||
<v-text-field v-model="search.name" density="compact" hide-details placeholder="例如:王小明" variant="outlined" />
|
<v-text-field id="search-name" v-model="search.name" aria-labelledby="search-name-label" density="compact" hide-details name="searchName" placeholder="例如:王小明" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
<div id="search-department-label" class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
||||||
<v-select v-model="search.department" density="compact" hide-details :items="departments" variant="outlined" />
|
<v-select id="search-department" v-model="search.department" aria-labelledby="search-department-label" density="compact" hide-details :items="departments" name="searchDepartment" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
<div id="search-grade-label" class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
||||||
<v-select
|
<v-select
|
||||||
v-model="search.grade" density="compact" hide-details item-title="title" item-value="value"
|
id="search-grade" v-model="search.grade" aria-labelledby="search-grade-label" density="compact" hide-details item-title="title" item-value="value"
|
||||||
:items="gradeOptions" variant="outlined" />
|
:items="gradeOptions" name="searchGrade" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
<div id="search-status-label" class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
||||||
<v-select v-model="search.status" density="compact" hide-details :items="statuses" variant="outlined" />
|
<v-select id="search-status" v-model="search.status" aria-labelledby="search-status-label" density="compact" hide-details :items="statuses" name="searchStatus" variant="outlined" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
<v-col class="d-flex justify-end align-end flex-grow-1 ga-2" cols="12" md="auto">
|
||||||
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
<v-btn :prepend-icon="mdiBroom" variant="text" @click="resetSearch">清除</v-btn>
|
||||||
@@ -34,8 +34,8 @@ v-model="search.grade" density="compact" hide-details item-title="title" item-va
|
|||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
class="student-table" density="compact" fixed-header :headers="tableHeaders" height="100%"
|
v-model:page="currentPage" class="student-table" density="compact" fixed-header :headers="tableHeaders" height="100%"
|
||||||
:items="students" :items-per-page="10" items-per-page-text="每頁筆數" page-text="第 {0}-{1} 筆 / 共 {2} 筆"
|
hide-default-footer :items="students" :items-per-page="itemsPerPage"
|
||||||
:row-props="rowProps">
|
:row-props="rowProps">
|
||||||
<template #[`item.grade`]="{ item }">
|
<template #[`item.grade`]="{ item }">
|
||||||
{{ gradeLabel(item.grade) }}
|
{{ gradeLabel(item.grade) }}
|
||||||
@@ -60,6 +60,20 @@ color="error" :prepend-icon="mdiDelete" size="small" variant="text"
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<div class="d-flex align-center justify-space-between px-4 py-3">
|
||||||
|
<div class="text-body-2 text-medium-emphasis">
|
||||||
|
{{ pageSummary }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<v-btn :disabled="currentPage <= 1" size="small" variant="text" @click="currentPage = 1">第一頁</v-btn>
|
||||||
|
<v-btn :disabled="currentPage <= 1" size="small" variant="text" @click="currentPage -= 1">上一頁</v-btn>
|
||||||
|
<span class="text-body-2">{{ currentPage }} / {{ pageCount }}</span>
|
||||||
|
<v-btn :disabled="currentPage >= pageCount" size="small" variant="text" @click="currentPage += 1">下一頁</v-btn>
|
||||||
|
<v-btn :disabled="currentPage >= pageCount" size="small" variant="text" @click="currentPage = pageCount">最後頁</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</template>
|
</template>
|
||||||
</mnt-page-cards>
|
</mnt-page-cards>
|
||||||
@@ -226,7 +240,7 @@ v-model.number="addCourseForm.score" density="comfortable" label="分數" type="
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil, mdiSchool } from '@mdi/js'
|
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil, mdiSchool } from '@mdi/js'
|
||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
||||||
@@ -287,6 +301,17 @@ const studentStore = useStudentStore()
|
|||||||
const semesterStore = useSemesterStore()
|
const semesterStore = useSemesterStore()
|
||||||
const students = computed(() => studentStore.students)
|
const students = computed(() => studentStore.students)
|
||||||
type StudentPayload = Omit<StudentRecord, 'id'>
|
type StudentPayload = Omit<StudentRecord, 'id'>
|
||||||
|
const itemsPerPage = 10
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageCount = computed(() => Math.max(1, Math.ceil(students.value.length / itemsPerPage)))
|
||||||
|
const pageSummary = computed(() => {
|
||||||
|
const total = students.value.length
|
||||||
|
if (total === 0) return '第 0-0 筆 / 共 0 筆'
|
||||||
|
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage + 1
|
||||||
|
const end = Math.min(currentPage.value * itemsPerPage, total)
|
||||||
|
return `第 ${start}-${end} 筆 / 共 ${total} 筆`
|
||||||
|
})
|
||||||
|
|
||||||
// 彈窗狀態與流程控制
|
// 彈窗狀態與流程控制
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
@@ -541,6 +566,12 @@ function resetSearch () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(pageCount, (value) => {
|
||||||
|
if (currentPage.value > value) {
|
||||||
|
currentPage.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 新增:開啟彈窗,使用預設值
|
// 新增:開啟彈窗,使用預設值
|
||||||
function openAddDialog () {
|
function openAddDialog () {
|
||||||
loadSequence.value += 1
|
loadSequence.value += 1
|
||||||
|
|||||||
@@ -7,54 +7,69 @@
|
|||||||
>
|
>
|
||||||
<template #search-fields>
|
<template #search-fields>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
<div id="search-student-id-label" class="text-body-2 text-medium-emphasis pl-2">學號</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
id="search-student-id"
|
||||||
v-model="search.studentId"
|
v-model="search.studentId"
|
||||||
|
aria-labelledby="search-student-id-label"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
|
name="searchStudentId"
|
||||||
placeholder="例如:S2024001"
|
placeholder="例如:S2024001"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
<div id="search-name-label" class="text-body-2 text-medium-emphasis pl-2">姓名</div>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
id="search-name"
|
||||||
v-model="search.name"
|
v-model="search.name"
|
||||||
|
aria-labelledby="search-name-label"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
|
name="searchName"
|
||||||
placeholder="例如:王小明"
|
placeholder="例如:王小明"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
<div id="search-department-label" class="text-body-2 text-medium-emphasis pl-2">系所</div>
|
||||||
<v-select
|
<v-select
|
||||||
|
id="search-department"
|
||||||
v-model="search.department"
|
v-model="search.department"
|
||||||
|
aria-labelledby="search-department-label"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
:items="departments"
|
:items="departments"
|
||||||
|
name="searchDepartment"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
<div id="search-grade-label" class="text-body-2 text-medium-emphasis pl-2">年級</div>
|
||||||
<v-select
|
<v-select
|
||||||
|
id="search-grade"
|
||||||
v-model="search.grade"
|
v-model="search.grade"
|
||||||
|
aria-labelledby="search-grade-label"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
item-title="title"
|
item-title="title"
|
||||||
item-value="value"
|
item-value="value"
|
||||||
:items="gradeOptions"
|
:items="gradeOptions"
|
||||||
|
name="searchGrade"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" md="2">
|
<v-col cols="12" md="2">
|
||||||
<div class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
<div id="search-status-label" class="text-body-2 text-medium-emphasis pl-2">狀態</div>
|
||||||
<v-select
|
<v-select
|
||||||
|
id="search-status"
|
||||||
v-model="search.status"
|
v-model="search.status"
|
||||||
|
aria-labelledby="search-status-label"
|
||||||
density="compact"
|
density="compact"
|
||||||
hide-details
|
hide-details
|
||||||
:items="statuses"
|
:items="statuses"
|
||||||
|
name="searchStatus"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -65,15 +80,15 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #table>
|
<template #table>
|
||||||
<v-data-table
|
<v-data-table
|
||||||
|
v-model:page="currentPage"
|
||||||
class="student-table"
|
class="student-table"
|
||||||
density="compact"
|
density="compact"
|
||||||
fixed-header
|
fixed-header
|
||||||
:headers="tableHeaders"
|
:headers="tableHeaders"
|
||||||
height="100%"
|
height="100%"
|
||||||
|
hide-default-footer
|
||||||
:items="students"
|
:items="students"
|
||||||
:items-per-page="10"
|
:items-per-page="itemsPerPage"
|
||||||
items-per-page-text="每頁筆數"
|
|
||||||
page-text="第 {0}-{1} 筆 / 共 {2} 筆"
|
|
||||||
:row-props="rowProps"
|
:row-props="rowProps"
|
||||||
>
|
>
|
||||||
<template #[`item.grade`]="{ item }">
|
<template #[`item.grade`]="{ item }">
|
||||||
@@ -115,6 +130,48 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<div class="d-flex align-center justify-space-between px-4 py-3">
|
||||||
|
<div class="text-body-2 text-medium-emphasis">
|
||||||
|
{{ pageSummary }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center ga-2">
|
||||||
|
<v-btn
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="currentPage = 1"
|
||||||
|
>
|
||||||
|
第一頁
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
:disabled="currentPage <= 1"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="currentPage -= 1"
|
||||||
|
>
|
||||||
|
上一頁
|
||||||
|
</v-btn>
|
||||||
|
<span class="text-body-2">{{ currentPage }} / {{ pageCount }}</span>
|
||||||
|
<v-btn
|
||||||
|
:disabled="currentPage >= pageCount"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="currentPage += 1"
|
||||||
|
>
|
||||||
|
下一頁
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
:disabled="currentPage >= pageCount"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="currentPage = pageCount"
|
||||||
|
>
|
||||||
|
最後頁
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</template>
|
</template>
|
||||||
</mnt-page-cards>
|
</mnt-page-cards>
|
||||||
@@ -399,7 +456,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
import { mdiBroom, mdiDelete, mdiEye, mdiMagnify, mdiPencil } from '@mdi/js'
|
||||||
import { computed, nextTick, ref } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
import MaintenanceCrudDialogs from '@/components/maintenance/MaintenanceCrudDialogs.vue'
|
||||||
import MntDialogCard from '@/components/maintenance/MntDialogCard.vue'
|
import MntDialogCard from '@/components/maintenance/MntDialogCard.vue'
|
||||||
@@ -472,6 +529,17 @@ const searchPanelOpen = ref(false)
|
|||||||
const studentStore = useStudentStore()
|
const studentStore = useStudentStore()
|
||||||
const students = computed(() => studentStore.students)
|
const students = computed(() => studentStore.students)
|
||||||
type StudentPayload = Omit<StudentRecord, 'id'>
|
type StudentPayload = Omit<StudentRecord, 'id'>
|
||||||
|
const itemsPerPage = 10
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageCount = computed(() => Math.max(1, Math.ceil(students.value.length / itemsPerPage)))
|
||||||
|
const pageSummary = computed(() => {
|
||||||
|
const total = students.value.length
|
||||||
|
if (total === 0) return '第 0-0 筆 / 共 0 筆'
|
||||||
|
|
||||||
|
const start = (currentPage.value - 1) * itemsPerPage + 1
|
||||||
|
const end = Math.min(currentPage.value * itemsPerPage, total)
|
||||||
|
return `第 ${start}-${end} 筆 / 共 ${total} 筆`
|
||||||
|
})
|
||||||
|
|
||||||
// 彈窗狀態與流程控制
|
// 彈窗狀態與流程控制
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
@@ -577,6 +645,12 @@ function resetSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(pageCount, (value) => {
|
||||||
|
if (currentPage.value > value) {
|
||||||
|
currentPage.value = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 新增:開啟彈窗,使用預設值
|
// 新增:開啟彈窗,使用預設值
|
||||||
function openAddDialog() {
|
function openAddDialog() {
|
||||||
loadSequence.value += 1
|
loadSequence.value += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user