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,253 @@
<template>
<v-card border class="d-flex flex-column h-100 rounded-0" flat width="100%">
<v-toolbar v-if="!isDetailEditing" color="transparent" density="compact" flat>
<v-btn :icon="isMobile ? 'mdi-arrow-left' : 'mdi-close'" size="small" variant="text" @click="$emit('close')" />
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
{{ selectedSemester ? selectedSemester.semesterName : '學期明細' }}
</v-toolbar-title>
<v-spacer />
<v-btn
v-if="selectedSemester && !isViewMode"
color="error"
prepend-icon="mdi-delete"
size="small"
variant="text"
@click="$emit('delete', selectedSemester.id)"
>
刪除
</v-btn>
<v-btn
v-if="selectedSemester && !isViewMode"
color="primary"
prepend-icon="mdi-pencil"
size="small"
variant="text"
@click="$emit('start-edit')"
>
編輯
</v-btn>
</v-toolbar>
<v-toolbar v-else density="compact" flat>
<v-btn :icon="isMobile ? 'mdi-arrow-left' : 'mdi-close'" size="small" variant="text" @click="$emit('cancel-edit')" />
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
編輯學期
</v-toolbar-title>
<v-spacer />
<v-btn variant="text" @click="$emit('save-edit')">儲存</v-btn>
</v-toolbar>
<v-divider />
<v-card-text v-if="!isDetailEditing" class="pa-0 flex-grow-1 overflow-y-auto bg-grey-lighten-5">
<div v-if="selectedSemester" class="h-100 d-flex flex-column">
<div :class="statsClass">
<v-card class="py-2 px-3" :class="statCardClass" variant="outlined">
<div class="text-caption text-medium-emphasis">學期平均</div>
<div class="text-h6 font-weight-bold text-primary">{{ selectedSemester.average }}</div>
</v-card>
<v-card class="py-2 px-3" :class="statCardClass" variant="outlined">
<div class="text-caption text-medium-emphasis">班級排名</div>
<div class="text-h6 font-weight-bold">{{ selectedSemester.rank }}</div>
</v-card>
<v-card class="py-2 px-3" :class="statCardClass" variant="outlined">
<div class="text-caption text-medium-emphasis">總學分</div>
<div class="text-h6 font-weight-bold">{{ totalCredits }}</div>
</v-card>
</div>
<v-divider />
<div v-if="isMobile" class="pa-3 d-flex flex-column ga-3">
<v-card v-for="course in selectedSemester.courses" :key="course.code" class="pa-3" variant="outlined">
<div class="d-flex align-start justify-space-between ga-3">
<div>
<div class="text-body-1 font-weight-medium">{{ course.name }}</div>
<div class="text-caption text-medium-emphasis">{{ course.code }}</div>
</div>
<v-chip :color="course.score < 60 ? 'error' : 'success'" size="small" variant="tonal">
{{ course.score }}
</v-chip>
</div>
<div class="d-flex ga-4 mt-3 text-body-2">
<div>學分 {{ course.credits }}</div>
</div>
</v-card>
<div v-if="selectedSemester.courses.length === 0" class="text-center text-medium-emphasis py-6">
尚無課程資料
</div>
</div>
<v-table v-else class="flex-grow-1">
<thead>
<tr>
<th class="text-left bg-grey-lighten-4">課程名稱</th>
<th class="text-center bg-grey-lighten-4" width="80">學分</th>
<th class="text-right bg-grey-lighten-4" width="80">分數</th>
</tr>
</thead>
<tbody>
<tr v-for="course in selectedSemester.courses" :key="course.code">
<td>
<div class="text-body-2 font-weight-medium">{{ course.name }}</div>
<div class="text-caption text-medium-emphasis">{{ course.code }}</div>
</td>
<td class="text-center">{{ course.credits }}</td>
<td class="text-right font-weight-bold" :class="course.score < 60 ? 'text-error' : 'text-success'">
{{ course.score }}
</td>
</tr>
</tbody>
</v-table>
</div>
</v-card-text>
<v-card-text v-else class="pa-0 flex-grow-1 overflow-y-auto bg-surface">
<div v-if="detailForm" class="pa-4 d-flex flex-column ga-4">
<v-row dense>
<v-col cols="12">
<v-text-field
v-model="detailForm.semesterName"
density="compact"
hide-details="auto"
label="學期名稱"
variant="outlined"
/>
</v-col>
<v-col :cols="isMobile ? 12 : 6">
<v-text-field
v-model.number="detailForm.rank"
density="compact"
hide-details="auto"
label="班級排名"
type="number"
variant="outlined"
/>
</v-col>
<v-col :cols="isMobile ? 12 : 6">
<v-text-field
density="compact"
hide-details="auto"
label="平均分數 (自動計算)"
:model-value="detailForm.average"
readonly
variant="filled"
/>
</v-col>
</v-row>
<div class="d-flex align-center mt-2 mb-1">
<div class="text-subtitle-2 font-weight-bold">課程列表</div>
<v-spacer />
<v-btn color="primary" prepend-icon="mdi-plus" size="small" variant="text" @click="$emit('add-course')">
加入課程
</v-btn>
</div>
<div v-if="isMobile" class="d-flex flex-column ga-3">
<v-card v-for="(course, idx) in detailForm.courses" :key="idx" class="pa-3" variant="outlined">
<div class="d-flex align-center mb-3">
<div class="text-subtitle-2 font-weight-bold">課程 {{ idx + 1 }}</div>
<v-spacer />
<v-btn color="error" icon="mdi-delete" size="small" variant="text" @click="$emit('remove-course', idx)" />
</div>
<div class="d-flex flex-column ga-3">
<v-text-field v-model="course.name" density="compact" hide-details label="課程名稱" variant="outlined" />
<v-text-field v-model="course.code" density="compact" hide-details label="代碼" variant="outlined" />
<v-text-field
v-model.number="course.credits"
density="compact"
hide-details
label="學分"
type="number"
variant="outlined"
/>
<v-text-field
v-model.number="course.score"
density="compact"
hide-details
label="分數"
type="number"
variant="outlined"
/>
</div>
</v-card>
<div v-if="detailForm.courses.length === 0" class="text-center text-medium-emphasis py-4 border border-dashed rounded">
暫無課程請點擊上方按鈕新增
</div>
</div>
<v-table v-else class="border rounded">
<thead>
<tr>
<th width="40"></th>
<th>課程資訊</th>
<th width="90">學分</th>
<th width="90">分數</th>
</tr>
</thead>
<tbody>
<tr v-for="(course, idx) in detailForm.courses" :key="idx">
<td class="px-0 text-center">
<v-btn color="error" icon="mdi-delete" size="small" variant="text" @click="$emit('remove-course', idx)" />
</td>
<td class="py-2">
<v-text-field v-model="course.name" class="mb-1" density="compact" hide-details label="課程名稱" variant="underlined" />
<v-text-field v-model="course.code" density="compact" hide-details label="代碼" style="font-size: 0.85em" variant="underlined" />
</td>
<td class="align-top py-2">
<v-text-field
v-model.number="course.credits"
density="compact"
hide-details
type="number"
variant="outlined"
/>
</td>
<td class="align-top py-2">
<v-text-field v-model.number="course.score" density="compact" hide-details type="number" variant="outlined" />
</td>
</tr>
<tr v-if="detailForm.courses.length === 0">
<td class="text-center text-medium-emphasis py-4" colspan="4">暫無課程請點擊上方按鈕新增</td>
</tr>
</tbody>
</v-table>
</div>
</v-card-text>
</v-card>
</template>
<script setup lang="ts">
import type { SemesterRecord } from '@/stores/semesters'
import { computed } from 'vue'
const props = defineProps<{
selectedSemester: SemesterRecord | null
detailForm: SemesterRecord | null
isViewMode: boolean
isDetailEditing: boolean
isMobile: boolean
}>()
defineEmits<{
(event: 'close'): void
(event: 'start-edit'): void
(event: 'delete', id: number): void
(event: 'cancel-edit'): void
(event: 'save-edit'): void
(event: 'add-course'): void
(event: 'remove-course', index: number): void
}>()
const totalCredits = computed(() =>
props.selectedSemester?.courses.reduce((sum, course) => sum + course.credits, 0) ?? 0,
)
const statsClass = computed(() =>
props.isMobile ? 'pa-3 d-flex flex-column ga-3 bg-surface' : 'pa-3 d-flex ga-3 bg-surface',
)
const statCardClass = computed(() => (props.isMobile ? '' : 'flex-grow-1'))
</script>