Files
skt-vuetify-templates/src/components/maintenance/master-detail/MasterDetailSemesterPanel.vue
T

353 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 ? mdiArrowLeft : mdiClose" 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="mdiDelete"
size="small"
variant="text"
@click="$emit('delete', selectedSemester.id)"
>
刪除
</v-btn>
<v-btn
v-if="selectedSemester && !isViewMode"
color="primary"
:prepend-icon="mdiPencil"
size="small"
variant="text"
@click="$emit('start-edit')"
>
編輯
</v-btn>
</v-toolbar>
<v-toolbar v-else density="compact" flat>
<v-btn :icon="isMobile ? mdiArrowLeft : mdiClose" 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
:model-value="detailForm.semesterName"
density="compact"
hide-details="auto"
label="學期名稱"
variant="outlined"
@update:model-value="updateDetailFormField('semesterName', $event)"
/>
</v-col>
<v-col :cols="isMobile ? 12 : 6">
<v-text-field
:model-value="detailForm.rank"
density="compact"
hide-details="auto"
label="班級排名"
type="number"
variant="outlined"
@update:model-value="updateDetailFormField('rank', toNumber($event))"
/>
</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="mdiPlus" size="small" variant="text" @click="addCourse">
加入課程
</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="mdiDelete" size="small" variant="text" @click="removeCourse(idx)" />
</div>
<div class="d-flex flex-column ga-3">
<v-text-field
:model-value="course.name"
density="compact"
hide-details
label="課程名稱"
variant="outlined"
@update:model-value="updateCourseField(idx, 'name', $event)"
/>
<v-text-field
:model-value="course.code"
density="compact"
hide-details
label="代碼"
variant="outlined"
@update:model-value="updateCourseField(idx, 'code', $event)"
/>
<v-text-field
:model-value="course.credits"
density="compact"
hide-details
label="學分"
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'credits', toNumber($event))"
/>
<v-text-field
:model-value="course.score"
density="compact"
hide-details
label="分數"
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'score', toNumber($event))"
/>
</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="mdiDelete" size="small" variant="text" @click="removeCourse(idx)" />
</td>
<td class="py-2">
<v-text-field
:model-value="course.name"
class="mb-1"
density="compact"
hide-details
label="課程名稱"
variant="underlined"
@update:model-value="updateCourseField(idx, 'name', $event)"
/>
<v-text-field
:model-value="course.code"
density="compact"
hide-details
label="代碼"
style="font-size: 0.85em"
variant="underlined"
@update:model-value="updateCourseField(idx, 'code', $event)"
/>
</td>
<td class="align-top py-2">
<v-text-field
:model-value="course.credits"
density="compact"
hide-details
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'credits', toNumber($event))"
/>
</td>
<td class="align-top py-2">
<v-text-field
:model-value="course.score"
density="compact"
hide-details
type="number"
variant="outlined"
@update:model-value="updateCourseField(idx, 'score', toNumber($event))"
/>
</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 { mdiArrowLeft, mdiClose, mdiDelete, mdiPencil, mdiPlus } from '@mdi/js'
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
}>()
const emit = 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
(event: 'update:detail-form', value: SemesterRecord | null): void
}>()
function cloneDetailForm (form: SemesterRecord): SemesterRecord {
return {
...form,
courses: form.courses.map((course) => ({ ...course })),
}
}
function emitDetailFormUpdate (updater: (draft: SemesterRecord) => void) {
if (!props.detailForm) return
const nextForm = cloneDetailForm(props.detailForm)
updater(nextForm)
emit('update:detail-form', nextForm)
}
function updateDetailFormField<K extends keyof SemesterRecord> (key: K, value: SemesterRecord[K]) {
emitDetailFormUpdate((draft) => {
draft[key] = value
})
}
function updateCourseField<K extends keyof SemesterRecord['courses'][number]>(
index: number,
key: K,
value: SemesterRecord['courses'][number][K],
) {
emitDetailFormUpdate((draft) => {
const course = draft.courses[index]
if (!course) return
course[key] = value
})
}
function toNumber (value: unknown): number {
if (typeof value === 'number') return value
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
}
function addCourse () {
emitDetailFormUpdate((draft) => {
draft.courses.push({
code: '',
name: '',
credits: 3,
score: 0,
})
})
}
function removeCourse (index: number) {
emitDetailFormUpdate((draft) => {
draft.courses.splice(index, 1)
})
}
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>