考试按实际日期、周次和节次融入周/日/总视图,并用橙色“考试”卡片区分。
即使固定课表尚未发布,只要考试已发布,也能正常显示。 考试计划、考场、监考教师和日期都会显示。 Excel 导出同步包含考试安排。
This commit is contained in:
@@ -100,14 +100,6 @@ const selectedResourceId = computed(() => {
|
||||
if (resourceType.value === 'Classroom') return classroomId.value
|
||||
return classId.value
|
||||
})
|
||||
const maxPeriods = computed(() => {
|
||||
const slotMaximum = Math.max(0, ...((timetable.value?.slots ?? []).map((x: any) => x.periodNumber)))
|
||||
const entryMaximum = Math.max(
|
||||
0,
|
||||
...((timetable.value?.entries ?? []).map((x: any) => x.startPeriod + x.periodCount - 1)),
|
||||
)
|
||||
return Math.max(8, slotMaximum, entryMaximum)
|
||||
})
|
||||
const slotMap = computed<Map<number, any>>(() =>
|
||||
new Map<number, any>(
|
||||
(timetable.value?.slots ?? []).map((item: any) => [item.periodNumber, item]),
|
||||
@@ -119,6 +111,36 @@ const termMonday = computed(() => {
|
||||
const offset = start.getDay() === 0 ? 6 : start.getDay() - 1
|
||||
return addDays(start, -offset)
|
||||
})
|
||||
const examEntries = computed(() => {
|
||||
if (!termMonday.value) return []
|
||||
return (timetable.value?.examEntries ?? []).flatMap((entry: any) => {
|
||||
const examDate = parseDate(entry.examDate)
|
||||
if (!examDate) return []
|
||||
const week = Math.floor(
|
||||
(examDate.getTime() - termMonday.value!.getTime()) / (7 * 86400000),
|
||||
) + 1
|
||||
if (week < 1) return []
|
||||
return [{
|
||||
...entry,
|
||||
startWeek: week,
|
||||
endWeek: week,
|
||||
weekPattern: 'All',
|
||||
}]
|
||||
})
|
||||
})
|
||||
const timedEntries = computed(() => [
|
||||
...(timetable.value?.entries ?? []),
|
||||
...examEntries.value,
|
||||
])
|
||||
const hasTimedEntries = computed(() => timedEntries.value.length > 0)
|
||||
const maxPeriods = computed(() => {
|
||||
const slotMaximum = Math.max(0, ...((timetable.value?.slots ?? []).map((x: any) => x.periodNumber)))
|
||||
const entryMaximum = Math.max(
|
||||
0,
|
||||
...(timedEntries.value.map((x: any) => x.startPeriod + x.periodCount - 1)),
|
||||
)
|
||||
return Math.max(8, slotMaximum, entryMaximum)
|
||||
})
|
||||
const totalWeeks = computed(() => {
|
||||
const end = parseDate(timetable.value?.term?.endDate)
|
||||
if (!termMonday.value || !end) return 1
|
||||
@@ -126,7 +148,7 @@ const totalWeeks = computed(() => {
|
||||
(7 * 86400000))
|
||||
const entryWeeks = Math.max(
|
||||
1,
|
||||
...((timetable.value?.entries ?? []).map((entry: any) => entry.endWeek)),
|
||||
...(timedEntries.value.map((entry: any) => entry.endWeek)),
|
||||
)
|
||||
return Math.max(1, dateWeeks, entryWeeks)
|
||||
})
|
||||
@@ -139,7 +161,7 @@ const currentTermWeek = computed(() => {
|
||||
return week >= 1 && week <= totalWeeks.value ? week : null
|
||||
})
|
||||
const weekEntries = computed(() =>
|
||||
(timetable.value?.entries ?? []).filter((entry: any) =>
|
||||
timedEntries.value.filter((entry: any) =>
|
||||
occursInWeek(entry, selectedWeek.value)),
|
||||
)
|
||||
const dayEntries = computed(() =>
|
||||
@@ -147,7 +169,7 @@ const dayEntries = computed(() =>
|
||||
)
|
||||
const visibleGridEntries = computed(() =>
|
||||
viewMode.value === 'overview'
|
||||
? (timetable.value?.entries ?? [])
|
||||
? timedEntries.value
|
||||
: weekEntries.value,
|
||||
)
|
||||
const selectedWeekLabel = computed(() => weekLabel(selectedWeek.value))
|
||||
@@ -182,6 +204,22 @@ function weeks(entry: any) {
|
||||
return `${entry.startWeek}—${entry.endWeek}周${pattern === '每周' ? '' : ` · ${pattern}`}`
|
||||
}
|
||||
|
||||
function entryKey(entry: any) {
|
||||
return `${entry.isExam ? 'exam' : 'course'}-${entry.id}-${entry.teachingTaskId ?? ''}`
|
||||
}
|
||||
|
||||
function entryTiming(entry: any) {
|
||||
const periods = `第 ${entry.startPeriod}—${entry.startPeriod + entry.periodCount - 1} 节`
|
||||
return entry.isExam
|
||||
? `${formatExamDate(entry.examDate)} · ${periods}`
|
||||
: `${weeks(entry)} · ${periods}`
|
||||
}
|
||||
|
||||
function formatExamDate(value?: string) {
|
||||
const date = parseDate(value)
|
||||
return date ? `${date.getMonth() + 1}月${date.getDate()}日` : '考试日期待定'
|
||||
}
|
||||
|
||||
function location(entry: any) {
|
||||
return [entry.campusName, entry.buildingName, entry.classroomName].filter(Boolean).join(' · ')
|
||||
}
|
||||
@@ -255,7 +293,7 @@ function laneFor(entry: any, entries: any[]) {
|
||||
return { item, index }
|
||||
})
|
||||
const count = Math.max(1, laneEnds.length)
|
||||
placed.forEach(({ item, index }) => layouts.set(String(item.id), { index, count }))
|
||||
placed.forEach(({ item, index }) => layouts.set(entryKey(item), { index, count }))
|
||||
}
|
||||
|
||||
sameDay.forEach((item: any) => {
|
||||
@@ -269,7 +307,7 @@ function laneFor(entry: any, entries: any[]) {
|
||||
clusterEnd = Math.max(clusterEnd, itemEnd)
|
||||
})
|
||||
placeCluster()
|
||||
return layouts.get(String(entry.id)) ?? { index: 0, count: 1 }
|
||||
return layouts.get(entryKey(entry)) ?? { index: 0, count: 1 }
|
||||
}
|
||||
|
||||
function withLaneStyle(entry: any, entries: any[], base: Record<string, string>) {
|
||||
@@ -670,7 +708,7 @@ onMounted(async () => {
|
||||
<p v-else-if="isMine">行政班课程与本人已选课程统一展示,仅采用教务处已发布课表。</p>
|
||||
<p v-else-if="isTeacherView">查看指定教师本学期的授课安排。</p>
|
||||
<p v-else-if="isManager">查询班级、教师与场地课表,可切换草稿或正式发布版本。</p>
|
||||
<p v-else>按年级、学院、专业和班级分类查询正式发布的课程安排。</p>
|
||||
<p v-else>按年级、学院、专业和班级分类查询正式发布的课程与考试安排。</p>
|
||||
</div>
|
||||
<div class="timetable-filters">
|
||||
<el-select v-model="termId" filterable placeholder="选择学期" @change="onTermChanged">
|
||||
@@ -839,6 +877,12 @@ onMounted(async () => {
|
||||
<template v-if="timetable.plan.publishedAt">
|
||||
· 发布于 {{ new Date(timetable.plan.publishedAt).toLocaleString('zh-CN') }}
|
||||
</template>
|
||||
<template v-if="examEntries.length">
|
||||
· 已融入 {{ examEntries.length }} 场考试
|
||||
</template>
|
||||
</span>
|
||||
<span v-else-if="examEntries.length">
|
||||
固定课表尚未发布 · 已载入 {{ examEntries.length }} 场考试
|
||||
</span>
|
||||
<span v-else>本学期课表尚未发布</span>
|
||||
</div>
|
||||
@@ -866,7 +910,7 @@ onMounted(async () => {
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="(viewMode === 'overview' || viewMode === 'week') && timetable?.plan && timetable.entries.length"
|
||||
v-if="(viewMode === 'overview' || viewMode === 'week') && hasTimedEntries"
|
||||
class="timetable-view-section"
|
||||
>
|
||||
<div class="view-context">
|
||||
@@ -874,12 +918,12 @@ onMounted(async () => {
|
||||
<strong>{{ viewMode === 'overview' ? '全学期总览' : selectedWeekLabel }}</strong>
|
||||
<span>
|
||||
{{ viewMode === 'overview'
|
||||
? '同时段但不同周次的课程并列显示'
|
||||
: `本周共 ${weekEntries.length} 项课程安排` }}
|
||||
? '固定课程与已发布考试按节次合并显示'
|
||||
: `本周共 ${weekEntries.length} 项课程、考试安排` }}
|
||||
</span>
|
||||
</div>
|
||||
<small v-if="viewMode === 'overview'">课程卡片保留起止周与单双周信息</small>
|
||||
<small v-else-if="!weekEntries.length">本周没有课程安排</small>
|
||||
<small v-if="viewMode === 'overview'">考试卡片显示具体日期,课程卡片保留周次信息</small>
|
||||
<small v-else-if="!weekEntries.length">本周没有课程或考试安排</small>
|
||||
</div>
|
||||
<div class="timetable-scroll">
|
||||
<div
|
||||
@@ -907,19 +951,24 @@ onMounted(async () => {
|
||||
</template>
|
||||
<article
|
||||
v-for="entry in visibleGridEntries"
|
||||
:key="entry.id"
|
||||
:key="entryKey(entry)"
|
||||
class="course-block"
|
||||
:class="{ 'exam-block': entry.isExam }"
|
||||
:style="gridEntryStyle(entry)"
|
||||
>
|
||||
<strong>{{ entry.courseName }}</strong>
|
||||
<strong>
|
||||
<span v-if="entry.isExam" class="entry-kind">考试</span>
|
||||
{{ entry.courseName }}
|
||||
</strong>
|
||||
<span v-if="entry.isExam && entry.examPlanName">{{ entry.examPlanName }}</span>
|
||||
<span>{{ entry.teacherNames.join('、') || '教师待定' }}</span>
|
||||
<span>{{ location(entry) }}</span>
|
||||
<small>{{ weeks(entry) }} · 第 {{ entry.startPeriod }}—{{ entry.startPeriod + entry.periodCount - 1 }} 节</small>
|
||||
<small>{{ entryTiming(entry) }}</small>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="viewMode === 'day' && timetable?.plan" class="day-view">
|
||||
<div v-else-if="viewMode === 'day' && hasTimedEntries" class="day-view">
|
||||
<header>
|
||||
<div>
|
||||
<strong>{{ weekdays[selectedDay] }} · {{ selectedDayDate }}</strong>
|
||||
@@ -945,21 +994,26 @@ onMounted(async () => {
|
||||
</template>
|
||||
<article
|
||||
v-for="entry in dayEntries"
|
||||
:key="entry.id"
|
||||
:key="entryKey(entry)"
|
||||
class="day-course-block"
|
||||
:class="{ 'exam-block': entry.isExam }"
|
||||
:style="dayEntryStyle(entry)"
|
||||
>
|
||||
<strong>{{ entry.courseName }}</strong>
|
||||
<strong>
|
||||
<span v-if="entry.isExam" class="entry-kind">考试</span>
|
||||
{{ entry.courseName }}
|
||||
</strong>
|
||||
<span v-if="entry.isExam && entry.examPlanName">{{ entry.examPlanName }}</span>
|
||||
<span>{{ entry.teacherNames.join('、') || '教师待定' }}</span>
|
||||
<span>{{ location(entry) }}</span>
|
||||
<small>{{ weeks(entry) }} · 第 {{ entry.startPeriod }}—{{ entry.startPeriod + entry.periodCount - 1 }} 节</small>
|
||||
<small>{{ entryTiming(entry) }}</small>
|
||||
</article>
|
||||
<span v-if="!dayEntries.length" class="day-no-courses">当日无课程安排</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-else-if="timetable && !loading && !timetable.flexibleCourses?.length"
|
||||
:description="timetable.plan ? '该课表暂时没有课程安排' : '所选学期尚未发布课表'"
|
||||
:description="timetable.plan ? '该课表暂时没有课程或考试安排' : '所选学期尚未发布课表或考试安排'"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1131,6 +1185,10 @@ onMounted(async () => {
|
||||
.course-block strong { color: #123a4b; font-size: 14px; }
|
||||
.course-block span { font-size: 12px; }
|
||||
.course-block small { margin-top: auto; color: #5f7885; font-size: 11px; }
|
||||
.course-block.exam-block, .day-course-block.exam-block { border-left-color: #b65b32; background: #fff1e8; color: #70442f; }
|
||||
.course-block.exam-block strong, .day-course-block.exam-block strong { color: #78391e; }
|
||||
.course-block.exam-block small, .day-course-block.exam-block small { color: #8b5b43; }
|
||||
.entry-kind { display: inline-block; margin-right: 5px; padding: 1px 5px; border-radius: 2px; background: #b65b32; color: #fff; font-size: 10px !important; line-height: 1.5; vertical-align: 1px; }
|
||||
.day-view { border: 1px solid #dce4eb; }
|
||||
.day-view > header { padding: 14px 16px; display: flex; align-items: baseline; justify-content: space-between; background: #173e72; color: #fff; }
|
||||
.day-view > header > div { display: grid; gap: 3px; }
|
||||
|
||||
Reference in New Issue
Block a user