实验成绩计算

This commit is contained in:
2026-08-09 22:40:21 +08:00 Unverified
parent d4abdd3db4
commit c25f16ec20
12 changed files with 7653 additions and 290 deletions
+98 -207
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Calendar, Check, Plus, Refresh, Tickets, User } from '@element-plus/icons-vue'
import { Calendar, Check, Plus, Refresh, User } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
@@ -665,7 +665,7 @@ onMounted(async () => {
</div>
</section>
<section class="lab-rail" aria-label="实验安排方式说明">
<section v-if="!isStudent" class="lab-rail" aria-label="实验安排方式说明">
<article class="rail-lane centralized">
<span class="rail-code">FIXED / 集中</span>
<b>统一课次</b>
@@ -707,7 +707,7 @@ onMounted(async () => {
<el-option label="已关闭" value="Closed" />
</el-select>
</template>
<span class="result-note"> {{ total }} 个教学班课程</span>
<span class="result-note">{{ isStudent ? `${projectGroups.length} 门课程、${total} 个实验项目` : `${total} 个教学班课程` }}</span>
</section>
<section v-if="!isStudent" class="experiment-batch-bar">
@@ -760,6 +760,16 @@ onMounted(async () => {
<div v-else-if="row.sessions.length" class="table-schedule">
<b>{{ row.sessions.length }} 个开放场次</b>
<small>{{ row.sessions.map((session: any) => formatSessionTime(session)).join('') }}</small>
<el-popover placement="bottom" :width="520" trigger="click">
<template #reference><el-button size="small" text :icon="User">查看场次与名单</el-button></template>
<div class="student-session-menu">
<div v-for="session in row.sessions" :key="session.id" class="student-session-option">
<div><b>{{ formatSessionTime(session) }}</b><span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</span></div>
<el-button size="small" text @click="showParticipants(row, session)">预约名单</el-button>
<el-button v-if="session.status !== 'Cancelled'" size="small" text type="danger" @click="cancelSession(row, session)">{{ row.status === 'Draft' ? '删除' : '取消' }}</el-button>
</div>
</div>
</el-popover>
</div>
<span v-else class="table-muted">尚未安排</span>
</template>
@@ -793,211 +803,77 @@ onMounted(async () => {
</template>
</el-table-column>
</el-table>
<article
v-else
v-for="project in group.projects"
:key="project.id"
class="project-card"
:class="[
project.arrangementMode === 'Centralized' ? 'is-centralized' : 'is-flexible',
`is-${project.status.toLowerCase()}`,
]"
>
<header class="project-head">
<div class="project-identity">
<span class="project-code">{{ project.courseCode }} · {{ project.code }}</span>
<h3>{{ project.name }}</h3>
<p>{{ project.arrangementMode === 'Centralized' ? '统一到场实验' : '开放预约实验' }}</p>
</div>
<div class="project-tags">
<el-checkbox
v-if="!isStudent && project.status === 'Draft'"
:model-value="selectedProjectIds.includes(project.id)"
@change="toggleProjectSelection(project.id, $event)"
aria-label="选择实验项目"
/>
<el-tag
:type="project.arrangementMode === 'Centralized' ? 'primary' : 'success'"
effect="plain"
>
{{ modeMeta[project.arrangementMode].label }}
</el-tag>
<el-tag
:type="project.status === 'Published' ? 'success' : project.status === 'Closed' ? 'info' : 'warning'"
>
{{ statusLabels[project.status] }}
</el-tag>
</div>
</header>
<div class="project-context">
<span><b>开放日期</b>{{ project.startDate }} {{ project.endDate }}</span>
<span><b>任课教师</b>{{ project.teacherNames.join('、') || '待定' }}</span>
<span v-if="!isStudent"><b>面向班级</b>{{ project.classNames.join('、') || '选课学生' }}</span>
</div>
<p v-if="project.description" class="project-description">{{ project.description }}</p>
<div v-if="project.requirements" class="requirement-strip">
<b>实验要求</b>
<span>{{ project.requirements }}</span>
</div>
<div class="session-board">
<div class="session-board-head">
<div>
<span>{{ project.arrangementMode === 'Centralized' ? '统一实验课次' : '可选实验场次' }}</span>
<b>{{ modeMeta[project.arrangementMode].note }}</b>
</div>
<el-button
v-if="!isStudent && project.status !== 'Closed' && project.arrangementMode === 'SelfScheduled'"
size="small"
:icon="Calendar"
@click="openSession(project)"
>
安排场次
</el-button>
</div>
<div v-if="project.arrangementMode === 'Centralized' && project.scheduleEntry" class="session-grid">
<article class="session-ticket">
<div class="ticket-date"><strong>课表</strong><span>固定</span></div>
<div class="ticket-body">
<b> {{ project.scheduleEntry.projectWeek ?? `${project.scheduleEntry.startWeek}${project.scheduleEntry.endWeek}` }} · 星期 {{ project.scheduleEntry.dayOfWeek }} · {{ project.scheduleEntry.startPeriod }}{{ project.scheduleEntry.startPeriod + project.scheduleEntry.periodCount - 1 }} </b>
<span>{{ project.scheduleEntry.campusName }} · {{ project.scheduleEntry.buildingName }} {{ project.scheduleEntry.classroomName }}</span>
</div>
<div class="ticket-action"><el-tag type="primary" size="small" effect="plain">课表已安排</el-tag></div>
</article>
</div>
<div v-else-if="project.sessions.length" class="session-grid">
<article
v-for="session in project.sessions"
:key="session.id"
class="session-ticket"
:class="{
'is-selected': project.myBooking?.experimentSessionId === session.id,
'is-cancelled': session.status === 'Cancelled',
}"
>
<div class="ticket-date">
<strong>{{ session.sessionDate.slice(8, 10) }}</strong>
<span>{{ session.sessionDate.slice(5, 7) }} </span>
</div>
<div class="ticket-body">
<b> {{ session.startPeriod }}{{ session.startPeriod + session.periodCount - 1 }} </b>
<span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</span>
<small v-if="session.notes">{{ session.notes }}</small>
</div>
<div class="ticket-action">
<template v-if="project.arrangementMode === 'SelfScheduled'">
<el-tag
v-if="session.status !== 'Cancelled'"
:type="sessionTagType(session)"
size="small"
effect="plain"
>
{{ session.capacity - session.reservedCount }} / {{ session.capacity }}
</el-tag>
<el-tag v-else type="info" size="small">已取消</el-tag>
<el-table v-else :data="group.projects" class="project-table student-project-table" size="small">
<el-table-column type="expand" width="44">
<template #default="{ row }">
<div class="student-project-detail">
<p v-if="row.description"><b>项目说明</b>{{ row.description }}</p>
<p v-if="row.requirements"><b>实验要求</b>{{ row.requirements }}</p>
<p><b>开放日期</b>{{ row.startDate }} {{ row.endDate }}</p>
<p><b>任课教师</b>{{ row.teacherNames.join('、') || '待定' }}</p>
</div>
</template>
</el-table-column>
<el-table-column label="实验项目" min-width="210">
<template #default="{ row }">
<div class="table-project-name">
<b>{{ row.code }} · {{ row.name }}</b>
<small>{{ row.description || modeMeta[row.arrangementMode].note }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="实验安排" min-width="300">
<template #default="{ row }">
<div v-if="row.arrangementMode === 'Centralized' && row.scheduleEntry" class="table-schedule">
<b> {{ row.scheduleEntry.projectWeek ?? `${row.scheduleEntry.startWeek}${row.scheduleEntry.endWeek}` }} · 星期 {{ row.scheduleEntry.dayOfWeek }} · {{ row.scheduleEntry.startPeriod }}{{ row.scheduleEntry.startPeriod + row.scheduleEntry.periodCount - 1 }} </b>
<small>{{ row.scheduleEntry.campusName }} · {{ row.scheduleEntry.buildingName }} {{ row.scheduleEntry.classroomName }}</small>
</div>
<div v-else-if="row.myBooking && selectedSession(row)" class="table-schedule is-booked">
<b>{{ formatSessionTime(selectedSession(row)) }}</b>
<small>{{ selectedSession(row).campusName }} · {{ selectedSession(row).buildingName }} {{ selectedSession(row).classroomName }}</small>
</div>
<div v-else-if="row.sessions.length" class="table-schedule">
<b>{{ row.sessions.length }} 个可选场次</b>
<small>尚未预约请在右侧选择实验时间</small>
</div>
<span v-else class="table-muted">暂无有效实验安排</span>
</template>
</el-table-column>
<el-table-column label="方式" width="100" align="center">
<template #default="{ row }"><el-tag :type="row.arrangementMode === 'Centralized' ? 'primary' : 'success'" effect="plain" size="small">{{ modeMeta[row.arrangementMode].label }}</el-tag></template>
</el-table-column>
<el-table-column label="状态" width="88" align="center">
<template #default="{ row }"><el-tag :type="row.status === 'Published' ? 'success' : 'info'" size="small">{{ statusLabels[row.status] }}</el-tag></template>
</el-table-column>
<el-table-column label="预约操作" width="190" fixed="right" align="right">
<template #default="{ row }">
<template v-if="row.arrangementMode === 'SelfScheduled'">
<el-popover placement="bottom-end" :width="540" trigger="click">
<template #reference>
<el-button size="small" :type="row.myBooking ? 'success' : 'primary'" plain>
{{ row.myBooking ? '查看场次' : '选择场次' }}
</el-button>
</template>
<div class="student-session-menu">
<header><b>{{ row.code }} · {{ row.name }}</b><span>每个实验项目限约一个场次</span></header>
<div v-for="session in row.sessions" :key="session.id" class="student-session-option" :class="{ 'is-selected': row.myBooking?.experimentSessionId === session.id }">
<div>
<b>{{ formatSessionTime(session) }}</b>
<span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</span>
</div>
<el-tag :type="sessionTagType(session)" size="small" effect="plain"> {{ session.remainingCount }} / {{ session.capacity }}</el-tag>
<el-button v-if="row.myBooking?.experimentSessionId === session.id" size="small" type="success" :icon="Check" disabled>已预约</el-button>
<el-button v-else size="small" type="primary" plain :disabled="row.status !== 'Published' || !!row.myBooking || session.remainingCount <= 0" @click="book(row, session)">预约</el-button>
</div>
<el-empty v-if="!row.sessions.length" :image-size="42" description="暂无有效实验场次" />
</div>
</el-popover>
<el-button v-if="row.myBooking && row.status === 'Published'" size="small" text type="danger" @click="cancelBooking(row)">取消</el-button>
</template>
<el-tag v-else type="primary" size="small" effect="plain">
统一到场
</el-tag>
<template v-if="isStudent && project.arrangementMode === 'SelfScheduled'">
<el-button
v-if="project.myBooking?.experimentSessionId === session.id"
size="small"
type="success"
:icon="Check"
disabled
>
已预约
</el-button>
<el-button
v-else
size="small"
type="primary"
plain
:disabled="project.status !== 'Published' || !!project.myBooking || session.remainingCount <= 0"
@click="book(project, session)"
>
预约此场
</el-button>
</template>
<template v-if="!isStudent">
<el-button
size="small"
text
:icon="User"
@click="showParticipants(project, session)"
>
{{ project.arrangementMode === 'Centralized' ? '应到名单' : '预约名单' }}
</el-button>
<el-button
v-if="session.status !== 'Cancelled'"
size="small"
text
type="danger"
@click="cancelSession(project, session)"
>
{{ project.status === 'Draft' ? '删除' : '取消' }}
</el-button>
</template>
</div>
</article>
</div>
<el-empty
v-else
:image-size="58"
:description="project.arrangementMode === 'Centralized' ? '尚未绑定已发布课表中的实验课' : (project.status === 'Draft' ? '尚未开放场次,安排后才能发布' : '暂无有效实验场次')"
/>
</div>
<footer v-if="!isStudent" class="project-actions">
<template v-if="project.status === 'Draft'">
<el-button size="small" @click="openEditProject(project)">编辑项目</el-button>
<el-button size="small" type="danger" plain @click="deleteProject(project)">删除</el-button>
<el-button
size="small"
type="primary"
:disabled="project.arrangementMode === 'Centralized'
? !project.scheduleEntry
: !project.sessions.some((item: any) => item.status !== 'Cancelled')"
@click="publishProject(project)"
>
发布给学生
</el-button>
</template>
<el-button
v-else-if="project.status === 'Published'"
size="small"
@click="closeProject(project)"
>
关闭项目
</el-button>
</footer>
<footer
v-else-if="project.arrangementMode === 'SelfScheduled' && project.myBooking"
class="student-booking-summary"
>
<div>
<Tickets />
<span>
<b>当前预约</b>
{{ formatSessionTime(selectedSession(project)) }}
</span>
</div>
<el-button
v-if="project.status === 'Published'"
size="small"
type="danger"
plain
@click="cancelBooking(project)"
>
取消并重选
</el-button>
</footer>
</article>
<span v-else class="table-muted">无需预约</span>
</template>
</el-table-column>
</el-table>
</div>
</section>
@@ -1390,11 +1266,25 @@ onMounted(async () => {
.project-table { --el-table-border-color: #dce5ea; --el-table-header-bg-color: #f7fafb; --el-table-row-hover-bg-color: #f6fafb; width: 100%; }
.project-table :deep(th.el-table__cell) { padding: 8px 0; color: #617986; font-size: 11px; font-weight: 650; }
.project-table :deep(td.el-table__cell) { padding: 9px 0; vertical-align: top; }
.student-project-table :deep(td.el-table__cell) { vertical-align: middle; }
.table-project-name, .table-schedule { display: grid; gap: 4px; min-width: 0; }
.table-project-name b { color: var(--lab-ink); font-size: 13px; }
.table-project-name small, .table-schedule small { overflow: hidden; color: #667f8d; font-size: 11px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; }
.table-schedule b { color: #365364; font-size: 12px; font-weight: 600; }
.table-schedule.is-booked b { color: var(--lab-teal); }
.table-muted { color: #8b9aa4; font-size: 12px; }
.student-project-detail { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 7px 20px; padding: 11px 18px 13px; background: #f7fafb; }
.student-project-detail p { margin: 0; display: grid; grid-template-columns: 70px 1fr; gap: 8px; color: #5f7682; font-size: 11px; line-height: 1.55; }
.student-project-detail b { color: #304d5d; }
.student-session-menu { display: grid; gap: 7px; }
.student-session-menu > header { display: flex; justify-content: space-between; gap: 12px; padding-bottom: 8px; border-bottom: 1px solid #dfe7ea; }
.student-session-menu > header b { color: var(--lab-ink); font-size: 12px; }
.student-session-menu > header span { color: #71858e; font-size: 10px; }
.student-session-option { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 9px; padding: 8px 9px; border: 1px solid #e0e7ea; background: #fff; }
.student-session-option.is-selected { border-color: #79afa5; box-shadow: inset 3px 0 0 var(--lab-teal); background: #f3faf8; }
.student-session-option > div { display: grid; gap: 2px; min-width: 0; }
.student-session-option > div b { color: #304d5d; font-size: 11px; }
.student-session-option > div span { overflow: hidden; color: #6a808b; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.project-card {
overflow: hidden;
border: 0;
@@ -1521,6 +1411,7 @@ onMounted(async () => {
.session-ticket { grid-template-columns: 54px minmax(0, 1fr); }
.ticket-action { grid-column: 1 / -1; justify-content: flex-start; padding: 0 10px 10px; }
.project-actions, .student-booking-summary { padding-inline: 14px; }
.student-project-detail { grid-template-columns: 1fr; }
.mode-choice { grid-template-columns: 1fr; }
.intro-actions { flex-wrap: wrap; justify-content: flex-end; }
}