feat: add SingleRecordMnt component for student record maintenance with search, add, edit, view, and delete functionalities
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="text-subtitle-1 font-weight-bold my-3 d-flex align-center">
|
||||
<v-icon start>mdi-school</v-icon>
|
||||
子檔資料示範
|
||||
</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.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">mdi-chevron-right</v-icon>
|
||||
</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-expansion-panels>
|
||||
<v-expansion-panel v-for="semester in semesters" :key="semester.id">
|
||||
<v-expansion-panel-title color="background">
|
||||
<div class="d-flex align-center ga-3 w-100 pr-2">
|
||||
<span class="font-weight-medium">{{ semester.semesterName }}</span>
|
||||
</div>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<span class="text-subtitle-1 font-weight-bold text-medium-emphasis">課程列表</span>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="!isFormReadonly"
|
||||
color="primary"
|
||||
:disabled="isFormLocked"
|
||||
prepend-icon="mdi-plus"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
@click="$emit('add-course', semester.id)"
|
||||
>
|
||||
加入課程
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-table class="border rounded" density="compact" fixed-header>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cursor-pointer" width="50%" @click="toggleSort(semester.id, 'name')">
|
||||
<div class="d-flex align-center ga-1">
|
||||
<span>課程名稱</span>
|
||||
<v-icon size="small">{{ getSortIcon(semester.id, 'name') }}</v-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th class="cursor-pointer" @click="toggleSort(semester.id, 'credits')">
|
||||
<div class="d-flex align-center ga-1">
|
||||
<span>學分</span>
|
||||
<v-icon size="small">{{ getSortIcon(semester.id, 'credits') }}</v-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th class="cursor-pointer" @click="toggleSort(semester.id, 'score')">
|
||||
<div class="d-flex align-center ga-1">
|
||||
<span>分數</span>
|
||||
<v-icon size="small">{{ getSortIcon(semester.id, 'score') }}</v-icon>
|
||||
</div>
|
||||
</th>
|
||||
<th v-if="!isFormReadonly" width="52"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="{ course, originalIndex } in getSortedCourses(semester)"
|
||||
:key="`${semester.id}-${originalIndex}`"
|
||||
>
|
||||
<td class="py-0">{{ course.name }}</td>
|
||||
<td class="align-top py-0">
|
||||
<span v-if="isFormReadonly">{{ course.credits }}</span>
|
||||
<v-text-field
|
||||
v-else
|
||||
density="compact"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
hide-spin-buttons
|
||||
:model-value="course.credits"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('update-course', semester.id, originalIndex, {
|
||||
credits: Number(value) || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
<td class="align-top py-0">
|
||||
<span v-if="isFormReadonly">{{ course.score }}</span>
|
||||
<v-text-field
|
||||
v-else
|
||||
density="compact"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
hide-spin-buttons
|
||||
:model-value="course.score"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@update:model-value="
|
||||
(value) =>
|
||||
$emit('update-course', semester.id, originalIndex, {
|
||||
score: Number(value) || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
<td v-if="!isFormReadonly" class="px-1 text-center">
|
||||
<v-btn
|
||||
color="error"
|
||||
:disabled="isFormLocked"
|
||||
icon="mdi-delete-outline"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('delete-course', semester.id, originalIndex, course.name)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="semester.courses.length === 0">
|
||||
<td
|
||||
class="text-center text-medium-emphasis py-6"
|
||||
:colspan="isFormReadonly ? 3 : 4"
|
||||
>
|
||||
<div class="d-flex flex-column align-center ga-2">
|
||||
<v-icon color="medium-emphasis" size="24">mdi-book-open-outline</v-icon>
|
||||
<span class="text-caption">尚無課程,點擊「加入課程」新增</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</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 { ref } from 'vue'
|
||||
|
||||
type CourseSortKey = 'name' | 'credits' | 'score'
|
||||
|
||||
interface CourseSortState {
|
||||
key: CourseSortKey
|
||||
order: 'asc' | 'desc'
|
||||
}
|
||||
|
||||
interface SortedCourseRow {
|
||||
course: CourseRecord
|
||||
originalIndex: number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
semesters: SemesterRecord[]
|
||||
isMobile: boolean
|
||||
isFormReadonly: boolean
|
||||
isFormLocked: boolean
|
||||
selectedSemesterId: number | null
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(event: 'select', semesterId: number): void
|
||||
(event: 'add-course', semesterId: number): void
|
||||
(
|
||||
event: 'update-course',
|
||||
semesterId: number,
|
||||
courseIndex: number,
|
||||
payload: Partial<CourseRecord>
|
||||
): void
|
||||
(event: 'delete-course', semesterId: number, courseIndex: number, courseName: string): void
|
||||
}>()
|
||||
|
||||
const semesterSortStates = ref<Record<number, CourseSortState>>({})
|
||||
|
||||
const getSortState = (semesterId: number) => semesterSortStates.value[semesterId]
|
||||
|
||||
function toggleSort (semesterId: number, key: CourseSortKey) {
|
||||
const current = getSortState(semesterId)
|
||||
|
||||
semesterSortStates.value[semesterId] =
|
||||
current?.key === key
|
||||
? {
|
||||
key,
|
||||
order: current.order === 'asc' ? 'desc' : 'asc',
|
||||
}
|
||||
: {
|
||||
key,
|
||||
order: 'asc',
|
||||
}
|
||||
}
|
||||
|
||||
function getSortIcon (semesterId: number, key: CourseSortKey) {
|
||||
const current = getSortState(semesterId)
|
||||
|
||||
if (current?.key !== key) return 'mdi-swap-vertical'
|
||||
return current.order === 'asc' ? 'mdi-arrow-up' : 'mdi-arrow-down'
|
||||
}
|
||||
|
||||
function compareCourseValue (left: CourseRecord, right: CourseRecord, key: CourseSortKey) {
|
||||
if (key === 'name') return left.name.localeCompare(right.name, 'zh-Hant')
|
||||
return left[key] - right[key]
|
||||
}
|
||||
|
||||
function getSortedCourses (semester: SemesterRecord): SortedCourseRow[] {
|
||||
const rows = semester.courses.map((course, originalIndex) => ({
|
||||
course,
|
||||
originalIndex,
|
||||
}))
|
||||
const sortState = getSortState(semester.id)
|
||||
|
||||
if (!sortState) return rows
|
||||
|
||||
return [...rows].sort((left, right) => {
|
||||
const result = compareCourseValue(left.course, right.course, sortState.key)
|
||||
|
||||
if (result === 0) return left.originalIndex - right.originalIndex
|
||||
return sortState.order === 'asc' ? result : -result
|
||||
})
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user