252 lines
9.0 KiB
Vue
252 lines
9.0 KiB
Vue
<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
|
|
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="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 { mdiArrowDown, mdiArrowUp, mdiBookOpenOutline, mdiChevronRight, mdiDeleteOutline, mdiPlus, mdiSchool, mdiSwapVertical } from '@mdi/js'
|
|
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 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>
|