This commit is contained in:
2026-07-27 08:59:02 +08:00 Unverified
parent 22321abe23
commit 2f5c6af37c
12 changed files with 6281 additions and 212 deletions
+206 -65
View File
@@ -20,11 +20,19 @@ const currentAdj = ref<any>(null)
const rejectComment = ref('')
const terms = ref<any[]>([])
const tasks = ref<any[]>([])
const sessionOptions = ref<any[]>([])
const classrooms = ref<any[]>([])
const periods = ref<any[]>([])
const sessionTask = ref<any>(null)
const sessionLoading = ref(false)
const termId = ref<string>()
const tab = ref(isManager.value ? 'reviews' : 'mine')
const form = reactive({
teachingTaskId: undefined as string | undefined,
sourceKey: undefined as string | undefined,
sourceScheduleEntryId: undefined as string | undefined,
sourceWeek: undefined as number | undefined,
type: 'Reschedule' as string,
reason: '',
submit: true,
@@ -41,11 +49,36 @@ const form = reactive({
const typeLabels: Record<string, string> = { Reschedule: '调课', Cancel: '停课', Makeup: '补课', Substitute: '代课' }
const statusLabels: Record<string, string> = { Draft: '草稿', Submitted: '待审核', Approved: '已通过', Rejected: '已退回' }
const statusColors: Record<string, 'info' | 'warning' | 'success' | 'danger'> = { Draft: 'info', Submitted: 'warning', Approved: 'success', Rejected: 'danger' }
const sourceRequired = computed(() => ['Reschedule', 'Cancel', 'Makeup'].includes(form.type))
const selectedSession = computed(() =>
sessionOptions.value.find(item => sessionKey(item) === form.sourceKey))
function periodOptions() {
if (periods.value.length) {
return periods.value.map(item => ({
value: item.periodNumber,
label: `${item.periodNumber} 节 · ${item.name} ${item.startsAt}${item.endsAt}`,
}))
}
return Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: `${i + 1}` }))
}
function sessionKey(session: any) {
return `${session.scheduleEntryId}:${session.week}`
}
function weekdayLabel(day: number) {
return ['','一','二','三','四','五','六','日'][day]
}
function sessionLabel(session: any) {
const end = session.startPeriod + session.periodCount - 1
const room = session.classroomName
? ` · ${session.buildingName ?? ''}${session.classroomName}`
: ' · 无固定教室'
return `${session.week} 周 · ${session.date}${weekdayLabel(session.dayOfWeek)} · 第 ${session.startPeriod}${end}${room}`
}
async function load() {
loading.value = true
try {
@@ -66,6 +99,9 @@ async function load() {
function openCreate() {
Object.assign(form, {
teachingTaskId: undefined,
sourceKey: undefined,
sourceScheduleEntryId: undefined,
sourceWeek: undefined,
type: 'Reschedule',
reason: '',
submit: true,
@@ -81,12 +117,51 @@ function openCreate() {
dialog.value = true
}
async function loadSessionOptions() {
sessionOptions.value = []
classrooms.value = []
periods.value = []
sessionTask.value = null
form.sourceKey = undefined
form.sourceScheduleEntryId = undefined
form.sourceWeek = undefined
if (!form.teachingTaskId) return
sessionLoading.value = true
try {
const data = (await http.get(
`/course-adjustments/tasks/${form.teachingTaskId}/session-options`,
)).data
sessionTask.value = data.task
sessionOptions.value = data.sessions
classrooms.value = data.classrooms
periods.value = data.periods
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
sessionLoading.value = false
}
}
function selectSession() {
const session = selectedSession.value
if (!session) return
form.sourceScheduleEntryId = session.scheduleEntryId
form.sourceWeek = session.week
form.periodCount = session.periodCount
form.classroomId = session.classroomId ?? undefined
}
async function submit() {
if (!form.teachingTaskId) { ElMessage.warning('请选择教学班'); return }
if (sourceRequired.value && !selectedSession.value) { ElMessage.warning('请选择要处理的原课次'); return }
if (selectedSession.value?.hasExistingAdjustment) { ElMessage.warning('该课次已有调停课记录,请选择其他课次'); return }
if (['Reschedule', 'Makeup'].includes(form.type) && !form.targetDate) { ElMessage.warning('请选择目标日期'); return }
if (['Reschedule', 'Makeup'].includes(form.type) && !form.startPeriod) { ElMessage.warning('请选择目标节次'); return }
if (!form.reason.trim()) { ElMessage.warning('请填写申请原因'); return }
try {
await http.post('/course-adjustments', {
...form,
sourceKey: undefined,
dayOfWeek: form.dayOfWeek || null,
startPeriod: form.startPeriod || null,
classroomId: form.classroomId || null,
@@ -159,12 +234,15 @@ onMounted(async () => {
await load()
})
function showTypeFields(type: string) {
function changeType() {
form.targetDate = ''
form.startPeriod = undefined
form.classroomId = selectedSession.value?.classroomId ?? undefined
}
function showTarget(type: string) {
return type === 'Reschedule' || type === 'Makeup'
}
function showDate(type: string) { return type === 'Makeup' }
function showSub(type: string) { return type === 'Substitute' }
function showCancel(type: string) { return type === 'Cancel' }
</script>
<template>
@@ -209,14 +287,18 @@ function showCancel(type: string) { return type === 'Cancel' }
</header>
<div class="adj-body">
<p v-if="adj.reason"><b>原因</b>{{ adj.reason }}</p>
<div v-if="adj.sourceDate" class="adjustment-route">
<div><small>原课次</small><b> {{ adj.sourceWeek }} · {{ adj.sourceDate }} · {{ adj.sourceStartPeriod }}{{ adj.sourceStartPeriod + adj.sourcePeriodCount - 1 }} </b><span>{{ adj.sourceClassroomName || '无固定教室' }}</span></div>
<i>{{ adj.type === 'Cancel' ? '×' : '→' }}</i>
<div v-if="adj.type !== 'Cancel'"><small>{{ adj.type === 'Makeup' ? '新增补课' : '调整至' }}</small><b>{{ adj.targetDate }} · {{ adj.startPeriod }}{{ adj.startPeriod + adj.periodCount - 1 }} </b><span>{{ adj.classroomName ? `${adj.buildingName} ${adj.classroomName}` : adj.sourceClassroomName || '无固定教室' }}</span></div>
<div v-else><small>处理结果</small><b>仅停掉本次课程</b><span>其他周次保持不变</span></div>
</div>
<div class="adj-details">
<span v-if="adj.targetDate"><b>日期</b>{{ adj.targetDate }}</span>
<span v-if="adj.dayOfWeek"><b>星期</b>{{ ['','一','二','三','四','五','六','日'][adj.dayOfWeek] }}</span>
<span v-if="adj.startPeriod"><b>节次</b>{{ adj.startPeriod }}-{{ adj.startPeriod + adj.periodCount - 1 }}</span>
<span v-if="adj.classroomName"><b>教室</b>{{ adj.buildingName }} {{ adj.classroomName }}</span>
<span v-if="!adj.sourceDate && adj.targetDate"><b>日期</b>{{ adj.targetDate }}</span>
<span v-if="!adj.sourceDate && adj.dayOfWeek"><b>星期</b>{{ weekdayLabel(adj.dayOfWeek) }}</span>
<span v-if="!adj.sourceDate && adj.startPeriod"><b>节次</b>{{ adj.startPeriod }}{{ adj.startPeriod + adj.periodCount - 1 }}</span>
<span v-if="!adj.sourceDate && adj.cancelWeek"><b>停课周次</b>{{ adj.cancelWeek }}</span>
<span v-if="adj.substituteTeacherName"><b>代课教师</b>{{ adj.substituteTeacherName }}</span>
<span v-if="adj.cancelWeek"><b>停课周次</b>{{ adj.cancelWeek }}</span>
<span v-if="adj.cancelDate"><b>停课日期</b>{{ adj.cancelDate }}</span>
</div>
<el-alert v-if="adj.reviewComment" :title="`审核意见:${adj.reviewComment}`" type="warning" :closable="false" show-icon />
</div>
@@ -242,14 +324,18 @@ function showCancel(type: string) { return type === 'Cancel' }
</header>
<div class="adj-body">
<p><b>原因</b>{{ adj.reason }}</p>
<div v-if="adj.sourceDate" class="adjustment-route">
<div><small>原课次</small><b> {{ adj.sourceWeek }} · {{ adj.sourceDate }} · {{ adj.sourceStartPeriod }}{{ adj.sourceStartPeriod + adj.sourcePeriodCount - 1 }} </b><span>{{ adj.sourceClassroomName || '无固定教室' }}</span></div>
<i>{{ adj.type === 'Cancel' ? '×' : '→' }}</i>
<div v-if="adj.type !== 'Cancel'"><small>{{ adj.type === 'Makeup' ? '新增补课' : '调整至' }}</small><b>{{ adj.targetDate }} · {{ adj.startPeriod }}{{ adj.startPeriod + adj.periodCount - 1 }} </b><span>{{ adj.classroomName ? `${adj.buildingName} ${adj.classroomName}` : adj.sourceClassroomName || '无固定教室' }}</span></div>
<div v-else><small>处理结果</small><b>仅停掉本次课程</b><span>其他周次保持不变</span></div>
</div>
<div class="adj-details">
<span v-if="adj.targetDate"><b>日期</b>{{ adj.targetDate }}</span>
<span v-if="adj.dayOfWeek"><b>星期</b>{{ ['','一','二','三','四','五','六','日'][adj.dayOfWeek] }}</span>
<span v-if="adj.startPeriod"><b>节次</b>{{ adj.startPeriod }}-{{ adj.startPeriod + adj.periodCount - 1 }}</span>
<span v-if="adj.classroomName"><b>教室</b>{{ adj.buildingName }} {{ adj.classroomName }}</span>
<span v-if="!adj.sourceDate && adj.targetDate"><b>日期</b>{{ adj.targetDate }}</span>
<span v-if="!adj.sourceDate && adj.dayOfWeek"><b>星期</b>{{ weekdayLabel(adj.dayOfWeek) }}</span>
<span v-if="!adj.sourceDate && adj.startPeriod"><b>节次</b>{{ adj.startPeriod }}{{ adj.startPeriod + adj.periodCount - 1 }}</span>
<span v-if="!adj.sourceDate && adj.cancelWeek"><b>停课周次</b>{{ adj.cancelWeek }}</span>
<span v-if="adj.substituteTeacherName"><b>代课教师</b>{{ adj.substituteTeacherName }}</span>
<span v-if="adj.cancelWeek"><b>停课周次</b>{{ adj.cancelWeek }}</span>
<span v-if="adj.cancelDate"><b>停课日期</b>{{ adj.cancelDate }}</span>
</div>
<p class="adj-meta"><b>提交时间</b>{{ new Date(adj.submittedAt).toLocaleString('zh-CN') }}</p>
</div>
@@ -262,69 +348,91 @@ function showCancel(type: string) { return type === 'Cancel' }
</section>
<!-- Create dialog -->
<el-dialog v-model="dialog" title="提交调停课申请" width="680px" top="5vh">
<el-form label-position="top">
<el-form-item label="教学班" required>
<el-select v-model="form.teachingTaskId" filterable placeholder="选择教学班">
<el-option v-for="t in tasks" :key="t.id" :label="`${t.taskNumber} · ${t.courseName}`" :value="t.id" />
</el-select>
</el-form-item>
<el-form-item label="申请类型" required>
<el-segmented v-model="form.type" :options="[
{ label: '课', value: 'Reschedule' },
{ label: '停课', value: 'Cancel' },
{ label: '补课', value: 'Makeup' },
{ label: '代课', value: 'Substitute' },
]" />
</el-form-item>
<template v-if="showDate(form.type)">
<el-form-item label="补课日期" required>
<el-date-picker v-model="form.targetDate" type="date" value-format="YYYY-MM-DD" placeholder="选择补课日期" />
<el-dialog v-model="dialog" title="发起单次课程调整" width="760px" top="4vh" class="adjustment-dialog">
<el-form label-position="top" class="adjustment-form">
<section class="adjustment-step">
<header><b>1</b><div><h3>选择业务类型</h3><p>每次申请只处理一个具体课次</p></div></header>
<el-form-item label="申请类型" required>
<el-segmented v-model="form.type" :options="[
{ label: '调课', value: 'Reschedule' },
{ label: '停课', value: 'Cancel' },
{ label: '补课', value: 'Makeup' },
{ label: '课', value: 'Substitute' },
]" @change="changeType" />
</el-form-item>
</template>
<el-alert
:title="form.type === 'Reschedule' ? '把原课次移到另一个日期和节次,原时间不再上课。' : form.type === 'Cancel' ? '只停掉选中的这一次课,不影响其他周次。' : form.type === 'Makeup' ? '保留原课次,另外增加一次补课。' : '为教学班增加代课教师。'"
type="info" :closable="false"
/>
</section>
<template v-if="showTypeFields(form.type)">
<div class="form-grid three">
<el-form-item label="星期">
<el-select v-model="form.dayOfWeek" placeholder="选择">
<el-option v-for="d in 7" :key="d" :label="['','周一','周二','周三','周四','周五','周六','周日'][d]" :value="d" />
</el-select>
<section class="adjustment-step">
<header><b>2</b><div><h3>锁定原课次</h3><p>从已发布课表中选择不再手填周次和星期</p></div></header>
<el-form-item label="教学班" required>
<el-select v-model="form.teachingTaskId" filterable placeholder="选择本人承担的教学班" @change="loadSessionOptions">
<el-option v-for="t in tasks" :key="t.id" :label="`${t.taskNumber} · ${t.courseName}`" :value="t.id" />
</el-select>
</el-form-item>
<el-form-item v-if="sourceRequired" label="原课次" required>
<el-select
v-model="form.sourceKey" filterable :loading="sessionLoading"
placeholder="选择第几周、哪一天、哪一节课" @change="selectSession"
>
<el-option
v-for="session in sessionOptions" :key="sessionKey(session)"
:label="sessionLabel(session)" :value="sessionKey(session)"
:disabled="session.hasExistingAdjustment"
>
<span>{{ sessionLabel(session) }}</span>
<small v-if="session.hasExistingAdjustment" class="session-used">已有申请</small>
</el-option>
</el-select>
</el-form-item>
<el-empty v-if="form.teachingTaskId && sourceRequired && !sessionLoading && !sessionOptions.length" description="该教学班没有可用的已发布课次" :image-size="56" />
</section>
<section v-if="showTarget(form.type)" class="adjustment-step target-step">
<header><b>3</b><div><h3>{{ form.type === 'Reschedule' ? '安排调课后课次' : '安排补课课次' }}</h3><p>课程时长沿用原课次系统会在审批时检查教师班级和教室冲突</p></div></header>
<div class="source-target-strip" v-if="selectedSession">
<div><span>原课次</span><strong>{{ sessionLabel(selectedSession) }}</strong></div>
<i></i>
<div><span>目标课次</span><strong>{{ form.targetDate || '待选日期' }} · {{ form.startPeriod ? `${form.startPeriod} 节起` : '待选节次' }}</strong></div>
</div>
<div class="form-grid two">
<el-form-item label="目标日期" required>
<el-date-picker
v-model="form.targetDate" type="date" value-format="YYYY-MM-DD"
:disabled-date="(date: Date) => sessionTask && (date < new Date(`${sessionTask.termStartDate}T00:00:00`) || date > new Date(`${sessionTask.termEndDate}T23:59:59`))"
placeholder="选择本学期内日期"
/>
</el-form-item>
<el-form-item label="起始节次">
<el-select v-model="form.startPeriod">
<el-form-item label="目标起始节次" required>
<el-select v-model="form.startPeriod" placeholder="选择节次">
<el-option v-for="o in periodOptions()" :key="o.value" :label="o.label" :value="o.value" />
</el-select>
</el-form-item>
<el-form-item label="持续节数">
<el-input-number v-model="form.periodCount" :min="1" :max="6" />
</el-form-item>
</div>
<el-form-item label="教室(可选)">
<el-select v-model="form.classroomId" clearable filterable placeholder="留空不调整教室">
<el-form-item label="目标教室">
<el-select v-model="form.classroomId" clearable filterable placeholder="留空则沿用原教室">
<el-option
v-for="room in classrooms" :key="room.id"
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity}`"
:value="room.id"
/>
</el-select>
</el-form-item>
</template>
</section>
<template v-if="showSub(form.type)">
<el-form-item label="代课教师" required>
<el-select v-model="form.substituteTeacherId" filterable placeholder="选择代课教师">
</el-select>
</el-form-item>
<section class="adjustment-step">
<header><b>3</b><div><h3>选择代课教师</h3><p>代课教师必须为在职教师。</p></div></header>
<el-form-item label="代课教师" required>
<el-select v-model="form.substituteTeacherId" filterable placeholder="选择代课教师" />
</el-form-item>
</section>
</template>
<template v-if="showCancel(form.type)">
<div class="form-grid two">
<el-form-item label="停课周次">
<el-input-number v-model="form.cancelWeek" :min="1" :max="20" placeholder="第几周" />
</el-form-item>
<el-form-item label="停课日期">
<el-date-picker v-model="form.cancelDate" type="date" value-format="YYYY-MM-DD" placeholder="或选择日期" />
</el-form-item>
</div>
</template>
<el-form-item label="申请原因" required>
<el-form-item label="申请原因" required class="reason-field">
<el-input v-model="form.reason" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请详细说明调停课原因" />
</el-form-item>
<el-form-item>
@@ -370,5 +478,38 @@ function showCancel(type: string) { return type === 'Cancel' }
.adj-card.submitted { border-left: 4px solid #e6a23c; }
.adj-card.approved { border-left: 4px solid #67c23a; }
.adj-card.rejected { border-left: 4px solid #f56c6c; }
.adjustment-route {
display: grid; grid-template-columns: minmax(0, 1fr) 34px minmax(0, 1fr);
align-items: stretch; gap: 8px; margin: 12px 0; padding: 10px;
background: #f4f7f9; border: 1px solid #dfe7eb;
}
.adjustment-route > div { display: grid; gap: 4px; padding: 9px 11px; background: white; border-left: 3px solid var(--teal); }
.adjustment-route small { color: var(--muted); font-size: 10px; }
.adjustment-route b { font-size: 12px; color: var(--ink); }
.adjustment-route span { color: var(--muted); font-size: 11px; }
.adjustment-route > i { align-self: center; color: var(--indigo); font: 700 20px/1 Consolas, monospace; text-align: center; }
.adjustment-form { display: grid; gap: 14px; }
.adjustment-step { padding: 16px; border: 1px solid var(--line); background: #fbfcfd; }
.adjustment-step > header { display: flex; align-items: flex-start; gap: 11px; margin-bottom: 14px; }
.adjustment-step > header > b {
display: grid; place-items: center; width: 26px; height: 26px; flex: 0 0 26px;
color: white; background: var(--indigo); font: 700 12px/1 Consolas, monospace;
}
.adjustment-step > header h3 { margin: 1px 0 3px; font-size: 14px; }
.adjustment-step > header p { margin: 0; color: var(--muted); font-size: 11px; }
.adjustment-step .el-form-item:last-child { margin-bottom: 0; }
.target-step { border-color: #b9d9d4; background: #f4faf8; }
.source-target-strip { display: grid; grid-template-columns: 1fr 34px 1fr; align-items: stretch; gap: 8px; margin-bottom: 14px; }
.source-target-strip > div { display: grid; gap: 5px; padding: 11px; background: white; border: 1px solid #d9e3e6; }
.source-target-strip span { color: var(--muted); font-size: 10px; }
.source-target-strip strong { font-size: 11px; line-height: 1.55; }
.source-target-strip > i { align-self: center; color: var(--teal); font: 700 20px/1 Consolas, monospace; text-align: center; }
.session-used { float: right; color: #b34e48; }
.reason-field { padding: 14px 16px 0; border-top: 1px solid var(--line); }
@media (max-width: 680px) {
.adjustment-route, .source-target-strip { grid-template-columns: 1fr; }
.adjustment-route > i, .source-target-strip > i { transform: rotate(90deg); padding: 2px; }
.adjustment-step { padding: 13px; }
}
</style>
+113 -23
View File
@@ -9,6 +9,7 @@ import {
Plus,
Promotion,
Refresh,
Search,
Upload,
} from '@element-plus/icons-vue'
import http, { apiErrorMessage } from '../api/http'
@@ -31,6 +32,13 @@ const createDialog = ref(false)
const returnDialog = ref(false)
const termId = ref<string | undefined>()
const status = ref<string | undefined>()
const taskKeyword = ref('')
const taskPage = ref(1)
const taskPageSize = 12
const taskTotal = ref(0)
const recordKeyword = ref('')
const recordPage = ref(1)
const recordPageSize = 50
const createForm = reactive({
regularWeight: 30,
finalWeight: 70,
@@ -67,8 +75,7 @@ const itemsWeight = computed(() =>
const weightSum = computed(() =>
createForm.regularWeight + createForm.finalWeight + itemsWeight.value)
const completedCount = computed(() =>
detail.value?.records.filter((record: any) =>
record.totalScore != null || record.examStatus !== 'Normal').length ?? 0)
selectedTask.value?.sheet?.completedCount ?? 0)
const averageScore = computed(() => {
const scores = (detail.value?.records ?? [])
.map((record: any) => Number(record.totalScore))
@@ -78,11 +85,9 @@ const averageScore = computed(() => {
.toFixed(1)
})
const passRate = computed(() => {
const scores = (detail.value?.records ?? [])
.map((record: any) => Number(record.totalScore))
.filter((value: number) => Number.isFinite(value))
if (!scores.length) return '—'
return `${Math.round(scores.filter((value: number) => value >= 60).length / scores.length * 100)}%`
const completed = Number(selectedTask.value?.sheet?.completedCount ?? 0)
if (!completed) return '—'
return `${Math.round(Number(selectedTask.value?.sheet?.passedCount ?? 0) / completed * 100)}%`
})
const transcriptGpa = computed(() => {
const numeric = transcript.value.records.filter((record: any) => record.gradePoint != null)
@@ -121,7 +126,7 @@ function removeItem(index: number) {
createForm.items.splice(index, 1)
}
async function load() {
async function load(resetPage = false) {
loading.value = true
try {
if (isStudent.value) {
@@ -130,16 +135,23 @@ async function load() {
})).data
return
}
tasks.value = (await http.get('/grades/sheets', {
if (resetPage) taskPage.value = 1
const response = (await http.get('/grades/sheets', {
params: {
academicTermId: termId.value,
status: status.value,
keyword: taskKeyword.value.trim() || undefined,
page: taskPage.value,
pageSize: taskPageSize,
},
})).data
tasks.value = response.items
taskTotal.value = response.total
const preferred = tasks.value.find((item) => item.id === selectedTask.value?.id)
?? tasks.value[0]
if (preferred) await selectTask(preferred)
else {
if (preferred) {
selectedTask.value = preferred
if (preferred.sheet) await loadDetail()
} else {
selectedTask.value = null
detail.value = null
}
@@ -152,11 +164,24 @@ async function load() {
async function selectTask(task: any) {
selectedTask.value = task
recordPage.value = 1
recordKeyword.value = ''
detail.value = null
if (!task.sheet) return
await loadDetail()
}
async function loadDetail() {
if (!selectedTask.value?.sheet) return
detailLoading.value = true
try {
const response = (await http.get(`/grades/sheets/${task.sheet.id}`)).data
const response = (await http.get(`/grades/sheets/${selectedTask.value.sheet.id}`, {
params: {
recordPage: recordPage.value,
recordPageSize,
studentKeyword: recordKeyword.value.trim() || undefined,
},
})).data
detail.value = response.sheet
detail.value.canEdit = response.canEdit
detail.value.canReview = response.canReview
@@ -168,6 +193,16 @@ async function selectTask(task: any) {
}
}
async function changeTaskPage(page: number) {
taskPage.value = page
await load()
}
async function searchRecords() {
recordPage.value = 1
await loadDetail()
}
function openCreate() {
createForm.regularWeight = 30
createForm.finalWeight = 70
@@ -213,7 +248,7 @@ async function saveRecords() {
})),
})
ElMessage.success('成绩已保存并重新计算总评')
await selectTask(selectedTask.value)
await loadDetail()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
@@ -257,7 +292,7 @@ async function handleImport(event: Event) {
file,
)
ElMessage.success(`导入完成:已更新 ${result.data.updated} 条成绩记录`)
await selectTask(selectedTask.value)
await loadDetail()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
} finally {
@@ -369,7 +404,7 @@ onMounted(async () => {
<p v-if="isStudent">查看学校已正式发布的课程成绩学分与绩点</p>
<p v-else>从教师登记学院复核到校级发布支持自定义平时实验实习等分项</p>
</div>
<el-button :icon="Refresh" @click="load">刷新</el-button>
<el-button :icon="Refresh" @click="load()">刷新</el-button>
</section>
<template v-if="isStudent">
@@ -381,7 +416,7 @@ onMounted(async () => {
</div>
<label>
<span>查看学期</span>
<el-select v-model="termId" clearable placeholder="全部学期" @change="load">
<el-select v-model="termId" clearable placeholder="全部学期" @change="load()">
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
</el-select>
</label>
@@ -421,13 +456,22 @@ onMounted(async () => {
<template v-else>
<section class="grade-toolbar">
<el-select v-model="termId" clearable placeholder="全部学期" @change="load">
<el-input
v-model="taskKeyword"
clearable
:prefix-icon="Search"
placeholder="搜索课程、课程号或教学班号"
@keyup.enter="load(true)"
@clear="load(true)"
/>
<el-select v-model="termId" clearable placeholder="全部学期" @change="load(true)">
<el-option v-for="term in terms" :key="term.id" :label="academicTermLabel(term)" :value="term.id" :class="academicTermOptionClass(term)" />
</el-select>
<el-select v-model="status" clearable placeholder="全部状态" @change="load">
<el-select v-model="status" clearable placeholder="全部状态" @change="load(true)">
<el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" />
</el-select>
<span> {{ tasks.length }} 个可管理教学班</span>
<el-button :icon="Search" @click="load(true)">查询</el-button>
<span> {{ taskTotal }} 个可管理教学班</span>
</section>
<section class="grade-workspace" v-loading="loading">
@@ -447,10 +491,20 @@ onMounted(async () => {
</i>
</button>
<el-empty v-if="!tasks.length" description="没有可管理的教学班" />
<el-pagination
v-if="taskTotal > taskPageSize"
class="grade-task-pagination"
small
layout="prev, pager, next"
:current-page="taskPage"
:page-size="taskPageSize"
:total="taskTotal"
@current-change="changeTaskPage"
/>
</aside>
<main class="grade-register" v-loading="detailLoading">
<el-empty v-if="!selectedTask" description="请选择教学班" />
<el-empty v-if="!selectedTask" description="从左侧选择一个教学班,进入对应成绩册" />
<section v-else-if="!selectedTask.sheet" class="grade-create-empty">
<el-icon><DocumentChecked /></el-icon>
<h3>{{ selectedTask.courseName }}</h3>
@@ -474,9 +528,9 @@ onMounted(async () => {
</header>
<section class="grade-register-ruler">
<div><span>学生数</span><b>{{ detail.records.length }}</b></div>
<div><span>学生数</span><b>{{ detail.recordTotal }}</b></div>
<div><span>已完成</span><b>{{ completedCount }}</b></div>
<div><span>平均</span><b>{{ averageScore }}</b></div>
<div><span>本页平均</span><b>{{ averageScore }}</b></div>
<div><span>及格率</span><b>{{ passRate }}</b></div>
<p>
平时 {{ detail.regularWeight }}%
@@ -493,6 +547,19 @@ onMounted(async () => {
show-icon
/>
<section class="grade-record-toolbar">
<el-input
v-model="recordKeyword"
clearable
:prefix-icon="Search"
placeholder="搜索姓名、学号或行政班"
@keyup.enter="searchRecords"
@clear="searchRecords"
/>
<el-button :icon="Search" @click="searchRecords">查找学生</el-button>
<span>每页最多显示 {{ recordPageSize }} 当前第 {{ detail.recordPage }} </span>
</section>
<div class="grade-table-scroll">
<el-table :data="detail.records" class="data-table grade-entry-table">
<el-table-column label="学生" fixed min-width="165">
@@ -565,6 +632,16 @@ onMounted(async () => {
</el-table-column>
</el-table>
</div>
<el-pagination
v-if="detail.recordTotal > recordPageSize"
class="grade-record-pagination"
background
layout="total, prev, pager, next"
:current-page="recordPage"
:page-size="recordPageSize"
:total="detail.recordTotal"
@current-change="(page: number) => { recordPage = page; loadDetail() }"
/>
<input ref="importFileInput" type="file" accept=".xlsx" style="display:none" @change="handleImport" />
@@ -688,4 +765,17 @@ onMounted(async () => {
.workflow-status .college-hint { color: #6b4e16; font-weight: 600; }
.grade-actions { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 16px; border-top: 1px solid #e4e7ed; background: #fafbfc; }
.grade-actions-buttons { display: flex; gap: 8px; flex-wrap: wrap; }
.grade-toolbar > .el-input { width: min(320px, 100%); }
.grade-task-pagination { justify-content: center; padding: 14px 8px; border-top: 1px solid var(--line); }
.grade-record-toolbar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 12px 16px; border-bottom: 1px solid var(--line); background: #f7f9fb;
}
.grade-record-toolbar .el-input { width: min(300px, 100%); }
.grade-record-toolbar > span { margin-left: auto; color: var(--muted); font-size: 11px; }
.grade-record-pagination { justify-content: flex-end; padding: 14px 16px; border-top: 1px solid var(--line); }
@media (max-width: 760px) {
.grade-record-toolbar > span { width: 100%; margin-left: 0; }
.grade-record-pagination { justify-content: center; }
}
</style>