实验课表

This commit is contained in:
2026-07-28 21:25:21 +08:00 Unverified
parent ec979f640d
commit 51c3b81e27
7 changed files with 687 additions and 28 deletions
+131 -16
View File
@@ -69,6 +69,10 @@ const planStatusLabels: Record<string, string> = {
Published: '已发布',
Archived: '已归档',
}
const experimentModeLabels: Record<string, string> = {
Centralized: '集中安排',
SelfScheduled: '自主预约',
}
const grades = computed(() =>
[...new Set(classes.value.map((item) => item.grade))].sort((a, b) => b - a),
@@ -128,9 +132,27 @@ const examEntries = computed(() => {
}]
})
})
const experimentEntries = computed(() => {
if (!termMonday.value) return []
return (timetable.value?.experimentEntries ?? []).flatMap((entry: any) => {
const experimentDate = parseDate(entry.experimentDate)
if (!experimentDate) return []
const week = Math.floor(
(experimentDate.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,
...experimentEntries.value,
])
const hasTimedEntries = computed(() => timedEntries.value.length > 0)
const maxPeriods = computed(() => {
@@ -174,6 +196,13 @@ const sortedExamEntries = computed(() =>
String(a.courseCode).localeCompare(String(b.courseCode)),
),
)
const sortedExperimentEntries = computed(() =>
experimentEntries.value.slice().sort((a: any, b: any) =>
String(a.experimentDate).localeCompare(String(b.experimentDate)) ||
a.startPeriod - b.startPeriod ||
String(a.experimentProjectCode).localeCompare(String(b.experimentProjectCode)),
),
)
const visibleGridEntries = computed(() =>
viewMode.value === 'overview'
? (timetable.value?.entries ?? [])
@@ -215,19 +244,31 @@ function weeks(entry: any) {
}
function entryKey(entry: any) {
return `${entry.isExam ? 'exam' : 'course'}-${entry.id}-${entry.teachingTaskId ?? ''}`
const kind = entry.isExperiment ? 'experiment' : entry.isExam ? 'exam' : 'course'
return `${kind}-${entry.id}-${entry.teachingTaskId ?? ''}`
}
function entryTiming(entry: any) {
const periods = `${entry.startPeriod}${entry.startPeriod + entry.periodCount - 1}`
if (entry.isExperiment) {
return `${formatActivityDate(entry.experimentDate, '实验日期待定')} · ${periods}`
}
return entry.isExam
? `${formatExamDate(entry.examDate)} · ${periods}`
? `${formatActivityDate(entry.examDate, '考试日期待定')} · ${periods}`
: `${weeks(entry)} · ${periods}`
}
function formatExamDate(value?: string) {
return formatActivityDate(value, '考试日期待定')
}
function formatActivityDate(value?: string, fallback = '日期待定') {
const date = parseDate(value)
return date ? `${date.getMonth() + 1}${date.getDate()}` : '考试日期待定'
return date ? `${date.getMonth() + 1}${date.getDate()}` : fallback
}
function experimentMode(entry: any) {
return experimentModeLabels[entry.experimentArrangementMode] ?? '实验安排'
}
function location(entry: any) {
@@ -890,9 +931,14 @@ onMounted(async () => {
<template v-if="examEntries.length">
· 已融入 {{ examEntries.length }} 场考试
</template>
<template v-if="experimentEntries.length">
· 已融入 {{ experimentEntries.length }} 场实验
</template>
</span>
<span v-else-if="examEntries.length">
固定课表尚未发布 · 已载入 {{ examEntries.length }} 场考试
<span v-else-if="examEntries.length || experimentEntries.length">
固定课表尚未发布
<template v-if="examEntries.length"> · 已载入 {{ examEntries.length }} 场考试</template>
<template v-if="experimentEntries.length"> · 已载入 {{ experimentEntries.length }} 场实验</template>
</span>
<span v-else>本学期课表尚未发布</span>
</div>
@@ -920,7 +966,9 @@ onMounted(async () => {
</section>
<div
v-if="(viewMode === 'week' && hasTimedEntries) || (viewMode === 'overview' && hasOverviewCourseEntries)"
v-if="(viewMode === 'week' && hasTimedEntries) ||
(viewMode === 'overview' &&
(hasOverviewCourseEntries || sortedExamEntries.length || sortedExperimentEntries.length))"
class="timetable-view-section"
>
<div class="view-context">
@@ -928,12 +976,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-else-if="!weekEntries.length">本周没有课程考试或实验安排</small>
</div>
<div class="timetable-scroll">
<div
@@ -963,14 +1011,21 @@ onMounted(async () => {
v-for="entry in visibleGridEntries"
:key="entryKey(entry)"
class="course-block"
:class="{ 'exam-block': entry.isExam }"
:class="{
'exam-block': entry.isExam,
'experiment-block': entry.isExperiment,
}"
:style="gridEntryStyle(entry)"
>
<strong>
<span v-if="entry.isExam" class="entry-kind">考试</span>
{{ entry.courseName }}
<span v-if="entry.isExperiment" class="entry-kind experiment-kind">实验</span>
{{ entry.isExperiment ? entry.experimentProjectName : entry.courseName }}
</strong>
<span v-if="entry.isExam && entry.examPlanName">{{ entry.examPlanName }}</span>
<span v-if="entry.isExperiment">
{{ entry.courseName }} · {{ experimentMode(entry) }}
</span>
<span>{{ entry.teacherNames.join('、') || '教师待定' }}</span>
<span>{{ location(entry) }}</span>
<small>{{ entryTiming(entry) }}</small>
@@ -1006,24 +1061,33 @@ onMounted(async () => {
v-for="entry in dayEntries"
:key="entryKey(entry)"
class="day-course-block"
:class="{ 'exam-block': entry.isExam }"
:class="{
'exam-block': entry.isExam,
'experiment-block': entry.isExperiment,
}"
:style="dayEntryStyle(entry)"
>
<strong>
<span v-if="entry.isExam" class="entry-kind">考试</span>
{{ entry.courseName }}
<span v-if="entry.isExperiment" class="entry-kind experiment-kind">实验</span>
{{ entry.isExperiment ? entry.experimentProjectName : entry.courseName }}
</strong>
<span v-if="entry.isExam && entry.examPlanName">{{ entry.examPlanName }}</span>
<span v-if="entry.isExperiment">
{{ entry.courseName }} · {{ experimentMode(entry) }}
</span>
<span>{{ entry.teacherNames.join('、') || '教师待定' }}</span>
<span>{{ location(entry) }}</span>
<small>{{ entryTiming(entry) }}</small>
</article>
<span v-if="!dayEntries.length" class="day-no-courses">当日无课程安排</span>
<span v-if="!dayEntries.length" class="day-no-courses">当日无教学安排</span>
</div>
</div>
<el-empty
v-else-if="timetable && !loading && !hasTimedEntries && !timetable.flexibleCourses?.length"
:description="timetable.plan ? '该课表暂时没有课程或考试安排' : '所选学期尚未发布课表或考试安排'"
:description="timetable.plan
? '该课表暂时没有课程、考试或实验安排'
: '所选学期尚未发布课表、考试或实验安排'"
/>
<section v-if="viewMode === 'overview' && sortedExamEntries.length" class="exam-overview">
@@ -1054,6 +1118,38 @@ onMounted(async () => {
</article>
</div>
</section>
<section
v-if="viewMode === 'overview' && sortedExperimentEntries.length"
class="exam-overview experiment-overview"
>
<header>
<div>
<span>LAB SESSIONS</span>
<strong>个人实验安排</strong>
</div>
<small>共 {{ sortedExperimentEntries.length }} 场 · 预约变化会自动同步</small>
</header>
<div class="exam-overview-list">
<article v-for="entry in sortedExperimentEntries" :key="entryKey(entry)">
<div class="exam-date">
<strong>{{ formatActivityDate(entry.experimentDate, '日期待定') }}</strong>
<span>{{ weekdays[entry.dayOfWeek] }}</span>
<small>第 {{ entry.startWeek }} 周</small>
</div>
<div class="exam-summary">
<span>{{ entry.experimentProjectCode }} · {{ experimentMode(entry) }}</span>
<strong>{{ entry.experimentProjectName }}</strong>
<p>{{ entry.courseName }} · {{ location(entry) }}</p>
<small>
第 {{ entry.startWeek }} 周 ·
第 {{ entry.startPeriod }}—{{ entry.startPeriod + entry.periodCount - 1 }} 节
</small>
<small>{{ entry.teacherNames.join('、') || '实验教师待定' }}</small>
</div>
</article>
</div>
</section>
</div>
</section>
@@ -1069,7 +1165,7 @@ onMounted(async () => {
<span>LIVE FEED</span>
<div>
<strong>订阅已启用</strong>
<small>课表考试安排变更后日历客户端会在下次同步时自动更新</small>
<small>课表考试和实验安排变更后日历客户端会在下次同步时自动更新。</small>
</div>
</div>
@@ -1132,6 +1228,7 @@ onMounted(async () => {
<ul>
<li>已发布课表中的固定课程和调停课后的最新安排</li>
<li>学生考试补考教师授课监考和补考监考</li>
<li>集中实验及学生本人已预约的自主实验场次</li>
<li>非排时课程会在学期首日生成全天提醒</li>
</ul>
<p>苹果日历可直接点击订阅Google 日历Outlook 可复制地址后选择通过 URL 添加服务需具备外网可访问的 HTTPS 地址才能跨设备同步</p>
@@ -1227,7 +1324,11 @@ onMounted(async () => {
.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; }
.course-block.experiment-block, .day-course-block.experiment-block { border-left-color: #168276; background: #e7f5f1; color: #315f59; }
.course-block.experiment-block strong, .day-course-block.experiment-block strong { color: #155b53; }
.course-block.experiment-block small, .day-course-block.experiment-block small { color: #527a74; }
.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; }
.entry-kind.experiment-kind { background: #168276; }
.exam-overview { margin-top: 20px; border: 1px solid #e2d6cf; background: #fffaf7; }
.exam-overview > header { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #eaded7; background: #fff5ef; }
.exam-overview > header > div { display: flex; align-items: baseline; gap: 10px; }
@@ -1245,6 +1346,20 @@ onMounted(async () => {
.exam-summary > strong { color: #533225; font-size: 15px; }
.exam-summary p { margin: 0; color: #6e584e; font-size: 12px; }
.exam-summary small { color: #8a7469; font-size: 11px; line-height: 1.5; }
.experiment-overview { border-color: #cfe4df; background: #f8fcfb; }
.experiment-overview > header { border-bottom-color: #d8ebe7; background: #eff9f6; }
.experiment-overview > header span { color: #168276; }
.experiment-overview > header strong { color: #174f49; }
.experiment-overview > header small { color: #63827d; }
.experiment-overview .exam-overview-list article { border-color: #d5e8e4; border-left-color: #168276; }
.experiment-overview .exam-date { border-right-color: #dbece8; background: #f0faf7; }
.experiment-overview .exam-date strong { color: #176b62; }
.experiment-overview .exam-date span { color: #4c706b; }
.experiment-overview .exam-date small { color: #718d88; }
.experiment-overview .exam-summary > span { color: #168276; }
.experiment-overview .exam-summary > strong { color: #174f49; }
.experiment-overview .exam-summary p { color: #506f6a; }
.experiment-overview .exam-summary small { color: #6c8581; }
.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; }