feat: add SingleRecord component for student maintenance with CRUD functionality
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<v-card border class="d-flex flex-column h-100 rounded-0" flat width="100%">
|
||||
<v-toolbar color="transparent" density="compact" flat>
|
||||
<v-btn :icon="mdiArrowLeft" size="small" variant="text" @click="$emit('close')" />
|
||||
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
|
||||
{{ semester?.semesterName || '課程明細' }}
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0 flex-grow-1 overflow-y-auto bg-grey-lighten-5">
|
||||
<div v-if="semester" class="pa-4 d-flex flex-column ga-4">
|
||||
<div class="d-flex flex-column ga-3">
|
||||
<v-card class="py-2 px-3" variant="outlined">
|
||||
<div class="text-caption text-medium-emphasis">學期平均</div>
|
||||
<div class="text-h6 font-weight-bold text-primary">{{ semester.average }}</div>
|
||||
</v-card>
|
||||
<v-card class="py-2 px-3" variant="outlined">
|
||||
<div class="text-caption text-medium-emphasis">班級排名</div>
|
||||
<div class="text-h6 font-weight-bold">{{ semester.rank }}</div>
|
||||
</v-card>
|
||||
<v-card class="py-2 px-3" variant="outlined">
|
||||
<div class="text-caption text-medium-emphasis">總學分</div>
|
||||
<div class="text-h6 font-weight-bold">{{ totalCredits }}</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<div>
|
||||
<div class="text-subtitle-2 font-weight-bold">課程列表</div>
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
手機版改用卡片式維護,不使用扁平表格
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="!isViewMode"
|
||||
color="primary"
|
||||
:disabled="isFormLocked"
|
||||
:prepend-icon="mdiPlus"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('add-course', semester.id)"
|
||||
>
|
||||
加入課程
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<template v-if="semester.courses.length > 0">
|
||||
<v-card
|
||||
v-for="(course, idx) in semester.courses"
|
||||
:key="`${course.code}-${idx}`"
|
||||
class="pa-3"
|
||||
variant="outlined"
|
||||
>
|
||||
<template v-if="isViewMode">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<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"
|
||||
:disabled="isFormLocked"
|
||||
:icon="mdiDelete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('delete-course', semester.id, idx)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column ga-3">
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="課程名稱"
|
||||
:model-value="course.name"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateCourse(idx, { name: String(value) })"
|
||||
/>
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="代碼"
|
||||
:model-value="course.code"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateCourse(idx, { code: String(value) })"
|
||||
/>
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="學分"
|
||||
:model-value="course.credits"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@update:model-value="
|
||||
(value) => updateCourse(idx, { credits: Number(value) || 0 })
|
||||
"
|
||||
/>
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="分數"
|
||||
:model-value="course.score"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateCourse(idx, { score: Number(value) || 0 })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<div v-else class="text-center text-medium-emphasis py-6 border border-dashed rounded">
|
||||
尚無課程資料
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
|
||||
import { mdiArrowLeft, mdiDelete, mdiPlus } from '@mdi/js'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
semester: SemesterRecord | null
|
||||
isViewMode: boolean
|
||||
isFormLocked: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'close'): 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): void
|
||||
}>()
|
||||
|
||||
const totalCredits = computed(
|
||||
() => props.semester?.courses.reduce((sum, course) => sum + course.credits, 0) ?? 0
|
||||
)
|
||||
|
||||
function updateCourse(courseIndex: number, payload: Partial<CourseRecord>) {
|
||||
if (!props.semester) return
|
||||
emit('update-course', props.semester.id, courseIndex, payload)
|
||||
}
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="text-subtitle-1 font-weight-bold mb-3 d-flex align-center">
|
||||
<v-icon start :icon="mdiSchool" />
|
||||
子檔資料示範 ({{ semesters.length }})
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="!isViewMode"
|
||||
color="primary"
|
||||
:prepend-icon="mdiPlus"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('add')"
|
||||
>
|
||||
新增學期
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-row density="compact">
|
||||
<v-col v-for="semester in semesters" :key="semester.id" cols="12" :md="isMobile ? 12 : 6">
|
||||
<v-card
|
||||
class="cursor-pointer mb-2"
|
||||
:class="{ 'border-opacity-100': selectedSemesterId === semester.id }"
|
||||
:color="selectedSemesterId === semester.id ? 'primary' : undefined"
|
||||
variant="outlined"
|
||||
@click="$emit('select', semester.id)"
|
||||
>
|
||||
<v-list-item density="compact">
|
||||
<v-list-item-title class="text-body-2 font-weight-bold">{{
|
||||
semester.semesterName
|
||||
}}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption">
|
||||
平均: {{ semester.average }} ・ 排名: {{ semester.rank }}
|
||||
</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<v-icon size="small" :icon="mdiChevronRight" />
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<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 { SemesterRecord } from '@/stores/semesters'
|
||||
import { mdiChevronRight, mdiPlus, mdiSchool } from '@mdi/js'
|
||||
|
||||
defineProps<{
|
||||
semesters: SemesterRecord[]
|
||||
selectedSemesterId: number | null
|
||||
isViewMode: boolean
|
||||
isMobile: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(event: 'select', id: number): void
|
||||
(event: 'add'): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<v-card border class="d-flex flex-column h-100 rounded-0" flat width="100%">
|
||||
<v-toolbar color="transparent" density="compact" flat>
|
||||
<v-btn :icon="mdiArrowLeft" size="small" variant="text" @click="$emit('close')" />
|
||||
<v-toolbar-title class="text-subtitle-1 font-weight-bold">
|
||||
{{ semester?.semesterName || '學期明細' }}
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0 flex-grow-1 overflow-y-auto bg-grey-lighten-5">
|
||||
<div v-if="semester" class="pa-4 d-flex flex-column ga-4">
|
||||
<div class="d-flex flex-column ga-3">
|
||||
<v-card class="py-2 px-3" variant="outlined">
|
||||
<div class="text-caption text-medium-emphasis">學期平均</div>
|
||||
<div class="text-h6 font-weight-bold text-primary">{{ semester.average }}</div>
|
||||
</v-card>
|
||||
<v-card class="py-2 px-3" variant="outlined">
|
||||
<div class="text-caption text-medium-emphasis">班級排名</div>
|
||||
<div class="text-h6 font-weight-bold">{{ semester.rank }}</div>
|
||||
</v-card>
|
||||
<v-card class="py-2 px-3" variant="outlined">
|
||||
<div class="text-caption text-medium-emphasis">總學分</div>
|
||||
<div class="text-h6 font-weight-bold">{{ totalCredits }}</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<template v-if="isViewMode">
|
||||
<v-card
|
||||
v-for="course in semester.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="semester.courses.length === 0"
|
||||
class="text-center text-medium-emphasis py-6 border border-dashed rounded"
|
||||
>
|
||||
尚無課程資料
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details="auto"
|
||||
label="學期名稱"
|
||||
:model-value="semester.semesterName"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateSemester({ semesterName: String(value) })"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details="auto"
|
||||
label="班級排名"
|
||||
:model-value="semester.rank"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateSemester({ rank: Number(value) || 0 })"
|
||||
/>
|
||||
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
label="平均分數"
|
||||
:model-value="semester.average"
|
||||
readonly
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center">
|
||||
<div class="text-subtitle-2 font-weight-bold">課程列表</div>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="isFormLocked"
|
||||
:prepend-icon="mdiPlus"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('add-course', semester.id)"
|
||||
>
|
||||
加入課程
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-card
|
||||
v-for="(course, idx) in semester.courses"
|
||||
:key="`${course.code}-${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"
|
||||
:disabled="isFormLocked"
|
||||
:icon="mdiDelete"
|
||||
size="small"
|
||||
variant="text"
|
||||
@click="$emit('delete-course', semester.id, idx, course.name)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column ga-3">
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="課程名稱"
|
||||
:model-value="course.name"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateCourse(idx, { name: String(value) })"
|
||||
/>
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="代碼"
|
||||
:model-value="course.code"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateCourse(idx, { code: String(value) })"
|
||||
/>
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="學分"
|
||||
:model-value="course.credits"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateCourse(idx, { credits: Number(value) || 0 })"
|
||||
/>
|
||||
<v-text-field
|
||||
density="comfortable"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
label="分數"
|
||||
:model-value="course.score"
|
||||
type="number"
|
||||
variant="outlined"
|
||||
@update:model-value="(value) => updateCourse(idx, { score: Number(value) || 0 })"
|
||||
/>
|
||||
</div>
|
||||
</v-card>
|
||||
|
||||
<div
|
||||
v-if="semester.courses.length === 0"
|
||||
class="text-center text-medium-emphasis py-6 border border-dashed rounded"
|
||||
>
|
||||
尚無課程資料
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CourseRecord, SemesterRecord } from '@/stores/semesters'
|
||||
import { mdiArrowLeft, mdiDelete, mdiPlus } from '@mdi/js'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
semester: SemesterRecord | null
|
||||
isViewMode: boolean
|
||||
isFormLocked: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'close'): void
|
||||
(event: 'add-course', semesterId: number): void
|
||||
(event: 'update-semester', semesterId: number, payload: Partial<SemesterRecord>): void
|
||||
(
|
||||
event: 'update-course',
|
||||
semesterId: number,
|
||||
courseIndex: number,
|
||||
payload: Partial<CourseRecord>
|
||||
): void
|
||||
(event: 'delete-course', semesterId: number, courseIndex: number, courseName: string): void
|
||||
}>()
|
||||
|
||||
const totalCredits = computed(
|
||||
() => props.semester?.courses.reduce((sum, course) => sum + course.credits, 0) ?? 0
|
||||
)
|
||||
|
||||
function updateSemester(payload: Partial<SemesterRecord>) {
|
||||
if (!props.semester) return
|
||||
emit('update-semester', props.semester.id, payload)
|
||||
}
|
||||
|
||||
function updateCourse(courseIndex: number, payload: Partial<CourseRecord>) {
|
||||
if (!props.semester) return
|
||||
emit('update-course', props.semester.id, courseIndex, payload)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,398 @@
|
||||
<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 density="compact">
|
||||
<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 type { SemesterRecord } from '@/stores/semesters'
|
||||
import { mdiArrowLeft, mdiClose, mdiDelete, mdiPencil, mdiPlus } from '@mdi/js'
|
||||
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>
|
||||
@@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<div class="text-subtitle-1 font-weight-bold my-3 d-flex align-center">
|
||||
<v-icon start :icon="mdiSchool" />
|
||||
子檔資料示範
|
||||
</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" :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-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="mdiPlus"
|
||||
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" :icon="getSortIcon(semester.id, 'name')" />
|
||||
</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" :icon="getSortIcon(semester.id, 'credits')" />
|
||||
</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" :icon="getSortIcon(semester.id, 'score')" />
|
||||
</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
|
||||
:aria-label="`${semester.semesterName} ${course.name} 學分`"
|
||||
density="compact"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
hide-spin-buttons
|
||||
:model-value="course.credits"
|
||||
:name="`semester-${semester.id}-course-${originalIndex}-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
|
||||
:aria-label="`${semester.semesterName} ${course.name} 分數`"
|
||||
density="compact"
|
||||
:disabled="isFormLocked"
|
||||
hide-details
|
||||
hide-spin-buttons
|
||||
:model-value="course.score"
|
||||
:name="`semester-${semester.id}-course-${originalIndex}-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="mdiDeleteOutline"
|
||||
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" :icon="mdiBookOpenOutline" />
|
||||
<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 {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiBookOpenOutline,
|
||||
mdiChevronRight,
|
||||
mdiDeleteOutline,
|
||||
mdiPlus,
|
||||
mdiSchool,
|
||||
mdiSwapVertical,
|
||||
} from '@mdi/js'
|
||||
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 mdiSwapVertical
|
||||
return current.order === 'asc' ? mdiArrowUp : mdiArrowDown
|
||||
}
|
||||
|
||||
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