feat: Implement detailed semester management components
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<div class="text-subtitle-1 font-weight-bold my-3 d-flex align-center">
|
||||
<v-icon start :icon="mdiSchool" />
|
||||
子檔資料示範
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="!isMobile && !isFormReadonly && !isFormLocked"
|
||||
color="primary"
|
||||
:prepend-icon="mdiPlus"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="$emit('add-course')"
|
||||
>
|
||||
新增成績
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="isMobile" class="d-flex flex-column ga-3">
|
||||
<v-card
|
||||
v-for="semester in semesters"
|
||||
:key="semester.id"
|
||||
class="cursor-pointer"
|
||||
:class="{ 'border-opacity-100': selectedSemesterId === semester.id }"
|
||||
:color="selectedSemesterId === semester.id ? 'primary' : undefined"
|
||||
variant="outlined"
|
||||
@click="$emit('select-semester', semester.id)"
|
||||
>
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-start justify-space-between ga-3">
|
||||
<div>
|
||||
<div class="text-body-1 font-weight-bold">{{ semester.semesterName }}</div>
|
||||
<div class="text-caption text-medium-emphasis">點擊查看課程與成績</div>
|
||||
</div>
|
||||
<v-icon size="small" :icon="mdiChevronRight" />
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap ga-2 mt-3">
|
||||
<v-chip color="primary" size="small" variant="tonal">平均 {{ semester.average }}</v-chip>
|
||||
<v-chip size="small" variant="tonal">排名 {{ semester.rank }}</v-chip>
|
||||
<v-chip size="small" variant="tonal">課程 {{ semester.courses.length }}</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex-grow-1" style="min-height: 0">
|
||||
<div class="d-flex flex-column ga-3 overflow-y-auto pr-1 h-100">
|
||||
<v-card class="flex-shrink-0" variant="flat">
|
||||
<v-card-text class="pa-2">
|
||||
<v-data-table
|
||||
class="border rounded"
|
||||
density="compact"
|
||||
:headers="headers"
|
||||
hide-default-footer
|
||||
:items="flattenedCourses"
|
||||
:items-per-page="-1"
|
||||
>
|
||||
<template #[`item.semesterName`]="slotProps">
|
||||
{{ slotProps.item.semesterName }}
|
||||
</template>
|
||||
<template #[`item.name`]="slotProps">
|
||||
{{ slotProps.item.name }}
|
||||
</template>
|
||||
<template #[`item.credits`]="slotProps">
|
||||
<span v-if="isFormReadonly">{{ slotProps.item.credits }}</span>
|
||||
<v-text-field
|
||||
v-else
|
||||
:aria-label="`${slotProps.item.semesterName} ${slotProps.item.name} 學分`"
|
||||
density="compact"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
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,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.score`]="slotProps">
|
||||
<span v-if="isFormReadonly">{{ slotProps.item.score }}</span>
|
||||
<v-text-field
|
||||
v-else
|
||||
:aria-label="`${slotProps.item.semesterName} ${slotProps.item.name} 分數`"
|
||||
density="compact"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
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,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<template #[`item.actions`]="slotProps">
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="isFormLocked"
|
||||
:icon="mdiDelete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="
|
||||
$emit('delete-course', slotProps.item.semesterId, slotProps.item.courseIndex)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="semesters.length === 0"
|
||||
class="text-caption text-center py-6 text-medium-emphasis border border-dashed rounded"
|
||||
>
|
||||
尚無成績資料
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
|
||||
import { mdiChevronRight, mdiDelete, mdiPlus, mdiSchool } from '@mdi/js'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
semesters: SemesterRecord[]
|
||||
isMobile: boolean
|
||||
isFormReadonly: boolean
|
||||
isFormLocked: boolean
|
||||
selectedSemesterId: number | null
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(event: 'select-semester', semesterId: number): void
|
||||
(event: 'add-course'): void
|
||||
(
|
||||
event: 'update-course',
|
||||
semesterId: number,
|
||||
courseIndex: number,
|
||||
payload: Partial<CourseRecord>
|
||||
): void
|
||||
(event: 'delete-course', semesterId: number, courseIndex: number): void
|
||||
}>()
|
||||
|
||||
const headers = computed(() => {
|
||||
const baseHeaders: Array<{ title: string; key: string; sortable: boolean; width?: string }> = [
|
||||
{ title: '學年學期', key: 'semesterName', sortable: true },
|
||||
{ title: '課程名稱', key: 'name', sortable: true },
|
||||
{ title: '學分', key: 'credits', sortable: true, width: '100' },
|
||||
{ title: '分數', key: 'score', sortable: true, width: '100' },
|
||||
]
|
||||
|
||||
if (!props.isFormReadonly) {
|
||||
baseHeaders.push({ title: '操作', key: 'actions', sortable: false, width: '60' })
|
||||
}
|
||||
|
||||
return baseHeaders
|
||||
})
|
||||
|
||||
const flattenedCourses = computed(() => {
|
||||
const result: Array<{
|
||||
semesterId: number
|
||||
courseIndex: number
|
||||
semesterName: string
|
||||
name: string
|
||||
credits: number
|
||||
score: number
|
||||
}> = []
|
||||
|
||||
for (const semester of props.semesters) {
|
||||
for (const [courseIndex, course] of semester.courses.entries()) {
|
||||
result.push({
|
||||
semesterId: semester.id,
|
||||
courseIndex,
|
||||
semesterName: semester.semesterName,
|
||||
name: course.name,
|
||||
credits: course.credits,
|
||||
score: course.score,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user