@@ -0,0 +1,834 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import {
Check ,
EditPen ,
Plus ,
Promotion ,
Refresh ,
Setting ,
} from '@element-plus/icons-vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import http , { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
import {
academicTermLabel ,
academicTermOptionClass ,
defaultAcademicTermId ,
} from '../utils/academicTerms'
const auth = useAuthStore ( )
const isStudent = computed ( ( ) => auth . user ? . roles . includes ( 'Student' ) ? ? false )
const loading = ref ( false )
const detailLoading = ref ( false )
const saving = ref ( false )
const terms = ref < any [ ] > ( [ ] )
const termId = ref ( '' )
const statusFilter = ref ( '' )
const projects = ref < any [ ] > ( [ ] )
const studentResults = ref < any [ ] > ( [ ] )
const detailDrawer = ref ( false )
const detail = ref < any | null > ( null )
const recordPage = ref ( 1 )
const recordPageSize = ref ( 50 )
const recordKeyword = ref ( '' )
const schemeDialog = ref ( false )
const editingScheme = ref ( false )
const schemeProject = ref < any | null > ( null )
const schemeForm = reactive ( {
contributionWeight : 1 ,
passScore : 60 ,
items : [ ] as Array < { name : string ; kind : string ; weight : number } > ,
} )
const statusLabels : Record < string , string > = {
Draft : '录入中' ,
Submitted : '待审核' ,
Approved : '审核通过' ,
Published : '已发布' ,
Returned : '已退回' ,
}
const statusTypes : Record < string , string > = {
Draft : 'info' ,
Submitted : 'warning' ,
Approved : 'success' ,
Published : 'primary' ,
Returned : 'danger' ,
}
const participationLabels : Record < string , string > = {
Pending : '待登记' ,
Completed : '已完成' ,
Absent : '缺席' ,
Excused : '请假' ,
Makeup : '补做' ,
Exempt : '免做' ,
}
const kindLabels : Record < string , string > = {
Operation : '实验操作' ,
Report : '实验报告' ,
Result : '实验结果' ,
Defense : '答辩' ,
Attendance : '到场情况' ,
Safety : '安全规范' ,
Other : '其他' ,
}
const modeLabels : Record < string , string > = {
Centralized : '集中安排' ,
SelfScheduled : '自主预约' ,
}
const schemeWeightTotal = computed ( ( ) =>
schemeForm . items . reduce ( ( sum , item ) => sum + Number ( item . weight || 0 ) , 0 ) ,
)
const detailAverage = computed ( ( ) => {
const values = ( detail . value ? . records ? ? [ ] )
. map ( ( record : any ) => previewTotal ( record ) )
. filter ( ( score : number | null ) => score != null ) as number [ ]
if ( ! values . length ) return '—'
return ( values . reduce ( ( sum , score ) => sum + score , 0 ) / values . length ) . toFixed ( 1 )
} )
const detailPassed = computed ( ( ) =>
( detail . value ? . records ? ? [ ] ) . filter ( ( record : any ) => {
const score = previewTotal ( record )
return score != null && score >= Number ( detail . value . passScore ) && ! record . safetyViolation
} ) . length ,
)
function resetScheme ( ) {
schemeForm . contributionWeight = 1
schemeForm . passScore = 60
schemeForm . items = [
{ name : '实验操作' , kind : 'Operation' , weight : 35 } ,
{ name : '实验结果' , kind : 'Result' , weight : 20 } ,
{ name : '实验报告' , kind : 'Report' , weight : 35 } ,
{ name : '到场与规范' , kind : 'Attendance' , weight : 10 } ,
]
}
function addSchemeItem ( ) {
schemeForm . items . push ( { name : '' , kind : 'Other' , weight : 0 } )
}
function openCreate ( project : any ) {
editingScheme . value = false
schemeProject . value = project
resetScheme ( )
schemeDialog . value = true
}
function openSettings ( ) {
if ( ! detail . value ) return
editingScheme . value = true
schemeProject . value = {
id : detail . value . experimentProjectId ,
name : detail . value . projectName ,
sheet : { id : detail . value . id } ,
}
schemeForm . contributionWeight = Number ( detail . value . contributionWeight )
schemeForm . passScore = Number ( detail . value . passScore )
schemeForm . items = detail . value . items . map ( ( item : any ) => ( {
name : item . name ,
kind : item . kind ,
weight : Number ( item . weight ) ,
} ) )
schemeDialog . value = true
}
async function saveScheme ( ) {
if ( ! schemeProject . value ) return
if ( ! schemeForm . items . length ||
schemeForm . items . some ( ( item ) => ! item . name . trim ( ) || Number ( item . weight ) <= 0 ) ||
schemeWeightTotal . value !== 100 ) {
ElMessage . warning ( '请完整填写评分项,且权重合计必须为 100%' )
return
}
try {
const payload = {
contributionWeight : schemeForm . contributionWeight ,
passScore : schemeForm . passScore ,
items : schemeForm . items . map ( ( item ) => ( {
name : item . name . trim ( ) ,
kind : item . kind ,
weight : item . weight ,
} ) ) ,
}
if ( editingScheme . value ) {
await http . put ( ` /experiment-grades/sheets/ ${ schemeProject . value . sheet . id } /settings ` , payload )
ElMessage . success ( '实验评分方案已更新,原分项成绩已清空' )
await loadDetail ( )
} else {
await http . post ( '/experiment-grades/sheets' , {
experimentProjectId : schemeProject . value . id ,
... payload ,
} )
ElMessage . success ( '实验成绩单已建立' )
await load ( )
}
schemeDialog . value = false
} catch ( error ) {
ElMessage . error ( apiErrorMessage ( error ) )
}
}
async function load ( ) {
loading . value = true
try {
if ( isStudent . value ) {
studentResults . value = ( await http . get ( '/experiment-grades/mine' ) ) . data
} else {
projects . value = ( await http . get ( '/experiment-grades/management' , {
params : {
academicTermId : termId . value || undefined ,
status : statusFilter . value || undefined ,
} ,
} ) ) . data
}
} catch ( error ) {
ElMessage . error ( apiErrorMessage ( error ) )
} finally {
loading . value = false
}
}
async function openDetail ( project : any ) {
detailDrawer . value = true
detail . value = {
id : project . sheet . id ,
items : [ ] ,
records : [ ] ,
recordTotal : 0 ,
}
recordPage . value = 1
recordKeyword . value = ''
await loadDetail ( )
}
async function loadDetail ( ) {
if ( ! detail . value ? . id ) return
detailLoading . value = true
try {
const response = ( await http . get ( ` /experiment-grades/sheets/ ${ detail . value . id } ` , {
params : {
recordPage : recordPage . value ,
recordPageSize : recordPageSize . value ,
studentKeyword : recordKeyword . value . trim ( ) || undefined ,
} ,
} ) ) . data
detail . value = response . sheet
detail . value . canEdit = response . canEdit
detail . value . canReview = response . canReview
detail . value . canPublish = response . canPublish
} catch ( error ) {
ElMessage . error ( apiErrorMessage ( error ) )
} finally {
detailLoading . value = false
}
}
function itemScore ( record : any , itemId : string ) {
return record . itemScores ? . find ( ( score : any ) =>
score . experimentGradeItemId === itemId )
}
function previewTotal ( record : any ) : number | null {
if ( record . safetyViolation || record . participationStatus === 'Absent' ) return 0
if ( ! [ 'Completed' , 'Makeup' ] . includes ( record . participationStatus ) ) return null
let total = 0
for ( const item of detail . value ? . items ? ? [ ] ) {
const score = itemScore ( record , item . id ) ? . score
if ( score == null || score === '' ) return null
total += Number ( score ) * Number ( item . weight ) / 100
}
return Math . round ( total * 10 ) / 10
}
async function saveRecords ( ) {
if ( ! detail . value ) return
saving . value = true
try {
await http . put ( ` /experiment-grades/sheets/ ${ detail . value . id } /records ` , {
records : detail . value . records . map ( ( record : any ) => ( {
id : record . id ,
participationStatus : record . participationStatus ,
safetyViolation : record . safetyViolation ,
attemptNumber : record . attemptNumber ,
submissionReference : record . submissionReference ,
submittedAt : record . submittedAt ,
isLate : record . isLate ,
teacherComment : record . teacherComment ,
itemScores : record . itemScores . map ( ( score : any ) => ( {
experimentGradeItemId : score . experimentGradeItemId ,
score : score . score ,
comment : score . comment ,
} ) ) ,
} ) ) ,
} )
ElMessage . success ( '本页实验成绩已保存' )
await loadDetail ( )
} catch ( error ) {
ElMessage . error ( apiErrorMessage ( error ) )
} finally {
saving . value = false
}
}
async function syncParticipants ( ) {
if ( ! detail . value ) return
try {
const result = ( await http . post (
` /experiment-grades/sheets/ ${ detail . value . id } /sync-participants ` ,
) ) . data
ElMessage . success (
` 名单已同步:新增 ${ result . added } 人,移除 ${ result . removed } 人,当前 ${ result . total } 人 ` ,
)
recordPage . value = 1
await loadDetail ( )
await load ( )
} catch ( error ) {
ElMessage . error ( apiErrorMessage ( error ) )
}
}
async function submitSheet ( ) {
try {
await ElMessageBox . confirm (
'提交后教师不能继续修改,学院审核退回后方可重新录入。' ,
'提交实验成绩审核' ,
{ confirmButtonText : '确认提交' , cancelButtonText : '继续检查' , type : 'warning' } ,
)
await http . post ( ` /experiment-grades/sheets/ ${ detail . value . id } /submit ` )
ElMessage . success ( '实验成绩已提交审核' )
await loadDetail ( )
await load ( )
} catch ( error : any ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( apiErrorMessage ( error ) )
}
}
async function approveSheet ( ) {
try {
await http . post ( ` /experiment-grades/sheets/ ${ detail . value . id } /approve ` )
ElMessage . success ( '实验成绩审核通过' )
await loadDetail ( )
await load ( )
} catch ( error ) {
ElMessage . error ( apiErrorMessage ( error ) )
}
}
async function returnSheet ( ) {
try {
const { value } = await ElMessageBox . prompt (
'请填写教师需要修正的具体内容。' ,
'退回实验成绩' ,
{
confirmButtonText : '确认退回' ,
cancelButtonText : '取消' ,
inputType : 'textarea' ,
inputValidator : ( value ) => value . trim ( ) ? true : '必须填写退回意见' ,
} ,
)
await http . post ( ` /experiment-grades/sheets/ ${ detail . value . id } /return ` , {
comment : value ,
} )
ElMessage . success ( '实验成绩已退回' )
await loadDetail ( )
await load ( )
} catch ( error : any ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( apiErrorMessage ( error ) )
}
}
async function publishSheet ( ) {
try {
await ElMessageBox . confirm (
'发布后学生可查看各评分项和实验总评,课程成绩也可以导入这份结果。' ,
'发布实验成绩' ,
{ confirmButtonText : '确认发布' , cancelButtonText : '暂不发布' , type : 'warning' } ,
)
await http . post ( ` /experiment-grades/sheets/ ${ detail . value . id } /publish ` )
ElMessage . success ( '实验成绩已向学生发布' )
await loadDetail ( )
await load ( )
} catch ( error : any ) {
if ( error === 'cancel' || error === 'close' ) return
ElMessage . error ( apiErrorMessage ( error ) )
}
}
function scoreTone ( result : any ) {
if ( result . totalScore == null ) return 'is-pending'
return result . isPassed ? 'is-passed' : 'is-failed'
}
onMounted ( async ( ) => {
if ( ! isStudent . value ) {
try {
terms . value = ( await http . get ( '/base-data/terms' ) ) . data
termId . value = defaultAcademicTermId ( terms . value ) ? ? ''
} catch ( error ) {
ElMessage . error ( apiErrorMessage ( error ) )
}
}
await load ( )
} )
< / script >
< template >
< main class = "experiment-grade-page" >
< section class = "page-intro grade-intro" >
< div >
< span class = "section-kicker" > LAB ASSESSMENT LEDGER < / span >
< h2 > { { isStudent ? '我的实验成绩' : '实验成绩管理' } } < / h2 >
< p v-if = "isStudent" > 实验成绩独立记录操作 、 结果 、 报告和到场情况 , 不与课程总评混在一起 。 < / p >
< p v-else > 按实验项目建立独立评分方案 , 审核发布后可选择汇总到课程成绩单 。 < / p >
< / div >
< el -button :icon = "Refresh" @click ="load" > 刷新 < / el -button >
< / section >
< section class = "assessment-flow" aria -label = " 实验成绩流程 " >
< div > < span > PROJECT < / span > < b > 实验项目 < / b > < small > 集中安排 / 自主预约 < / small > < / div >
< i > → < / i >
< div > < span > RUBRIC < / span > < b > 独立评分 < / b > < small > 操作 · 结果 · 报告 · 规范 < / small > < / div >
< i > → < / i >
< div > < span > PUBLISH < / span > < b > 审核发布 < / b > < small > 学生查看 / 课程可选导入 < / small > < / div >
< / section >
< template v-if = "isStudent" >
< section v-loading = "loading" class="student-result-grid" >
< article
v-for = "result in studentResults"
:key = "result.id"
class = "student-result-card"
:class = "scoreTone(result)"
>
< header >
< div >
< span > { { result . projectCode } } · { { modeLabels [ result . arrangementMode ] } } < / span >
< h3 > { { result . projectName } } < / h3 >
< p > { { result . courseCode } } · { { result . courseName } } · { { result . termName } } < / p >
< / div >
< div class = "score-seal" >
< strong > { { result . totalScore ? ? '—' } } < / strong >
< small > { { result . totalScore == null ? participationLabels [ result . participationStatus ] : result . isPassed ? '通过' : '未通过' } } < / small >
< / div >
< / header >
< div class = "result-facts" >
< span > 参与状态 < b > { { participationLabels [ result . participationStatus ] } } < / b > < / span >
< span > 合格线 < b > { { result . passScore } } < / b > < / span >
< span > 实验次数 < b > { { result . attemptNumber } } < / b > < / span >
< span > 提交情况 < b > { { result . isLate ? '迟交' : result . submittedAt ? '已提交' : '未登记' } } < / b > < / span >
< / div >
< div class = "result-rubric" >
< div v-for = "item in result.items" :key="item.experimentGradeItemId" >
< span > { { item . name } } · { { item . weight } } % < / span >
< b > { { item . score ? ? '—' } } < / b >
< small v-if = "item.comment" > {{ item.comment }} < / small >
< / div >
< / div >
< p v-if = "result.safetyViolation" class="safety-alert" > 已登记安全违规 , 本次实验按规则判定为 0 分 。 < / p >
< p v-if = "result.teacherComment" class="teacher-comment" > {{ result.teacherComment }} < / p >
< / article >
< el -empty v-if = "!loading && !studentResults.length" description="暂无已发布的实验成绩" / >
< / section >
< / template >
< template v-else >
< section class = "grade-toolbar" >
< 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 >
< el-select v-model = "statusFilter" clearable placeholder="全部成绩状态" @change="load" >
< el -option v-for = "(label, value) in statusLabels" :key="value" :label="label" :value="value" / >
< / el-select >
< span > 每个实验项目独立建单 ; 课程只读取已发布的汇总快照 。 < / span >
< / section >
< section v-loading = "loading" class="grade-project-list" >
< article v-for = "project in projects" :key="project.id" class="grade-project-card" >
< div class = "project-mark" >
< span > { { project . code } } < / span >
< b > { { modeLabels [ project . arrangementMode ] } } < / b >
< / div >
< div class = "project-summary" >
< h3 > { { project . name } } < / h3 >
< p > { { project . courseCode } } · { { project . courseName } } < / p >
< small > { { project . taskNumber } } · { { project . termName } } · { { project . teacherNames . join ( '、' ) || '教师待定' } } < / small >
< / div >
< template v-if = "project.sheet" >
< div class = "sheet-progress" >
< el-tag : type = "statusTypes[project.sheet.status] as any" effect = "plain" >
{ { statusLabels [ project . sheet . status ] } }
< / el-tag >
< span > < b > { { project . sheet . scoredCount } } < / b > / { { project . sheet . studentCount } } 已计分 < / span >
< span > 评分项 { { project . sheet . itemCount } } 个 · 合格线 { { project . sheet . passScore } } < / span >
< / div >
< el-button type = "primary" plain :icon = "EditPen" @click ="openDetail(project)" >
打开成绩单
< / el -button >
< / template >
< template v-else >
< div class = "sheet-empty" >
< b > 尚未建立实验成绩单 < / b >
< span > 建立时会按集中名单或有效预约生成评分对象 。 < / span >
< / div >
< el-button type = "primary" :icon = "Plus" @click ="openCreate(project)" >
建立成绩单
< / el -button >
< / template >
< / article >
< el-empty v-if = "!loading && !projects.length" description="当前筛选条件下没有可评分实验项目" / >
< / section >
< / template >
< el-dialog
v-model = "schemeDialog"
: title = "`${editingScheme ? '调整评分方案' : '建立实验成绩单'} · ${schemeProject?.name ?? ''}`"
width = "720px"
top = "5vh"
>
< div class = "scheme-form" >
< el-alert
v-if = "editingScheme"
title = "调整评分方案会清空原有分项得分,参与状态和教师评语保留。"
type = "warning"
show -icon
:closable = "false"
/ >
< div class = "scheme-basics" >
< label >
< span > 项目贡献权重 < / span >
< el-input-number v-model = "schemeForm.contributionWeight" :min="0.1" :max="100" :step="0.5" / >
< small > 多个实验项目汇总到课程时按此相对权重计算 < / small >
< / label >
< label >
< span > 实验合格线 < / span >
< el-input-number v-model = "schemeForm.passScore" :min="0" :max="100" :step="0.5" / >
< small > 仅决定该实验是否通过 , 不直接计算课程绩点 < / small >
< / label >
< / div >
< div class = "rubric-editor" >
< header >
< div > < span > RUBRIC < / span > < b > 实验评分项 < / b > < / div >
< strong : class = "{ invalid: schemeWeightTotal !== 100 }" > 合计 { { schemeWeightTotal } } % < / strong >
< / header >
< div v-for = "(item, index) in schemeForm.items" :key="index" class="rubric-row" >
< el -input v-model = "item.name" maxlength="60" placeholder="评分项名称" / >
< el-select v-model = "item.kind" >
< el -option v-for = "(label, value) in kindLabels" :key="value" :label="label" :value="value" / >
< / el-select >
< el-input-number v-model = "item.weight" :min="0" :max="100" :controls="false" / >
< span > % < / span >
< el-button text type = "danger" : disabled = "schemeForm.items.length === 1" @click ="schemeForm.items.splice(index, 1)" >
删除
< / el -button >
< / div >
< el-button text type = "primary" :icon = "Plus" @click ="addSchemeItem" > 增加评分项 < / el -button >
< / div >
< / div >
< template # footer >
< el-button @click ="schemeDialog = false" > 取消 < / el -button >
< el-button type = "primary" @click ="saveScheme" > {{ editingScheme ? ' 保存并重置分项 ' : ' 建立成绩单 ' }} < / el -button >
< / template >
< / el-dialog >
< el-drawer
v-model = "detailDrawer"
title = "实验评分工作台"
size = "94%"
class = "experiment-grade-drawer"
>
< div v-if = "detail" v-loading="detailLoading" class="grade-workbench" >
< header class = "workbench-head" >
< div >
< span > { { detail . projectCode } } · { { modeLabels [ detail . arrangementMode ] } } < / span >
< h3 > { { detail . projectName } } < / h3 >
< p > { { detail . courseCode } } · { { detail . courseName } } · { { detail . termName } } < / p >
< / div >
< div class = "workbench-status" >
< el-tag : type = "statusTypes[detail.status] as any" effect = "dark" > { { statusLabels [ detail . status ] } } < / el-tag >
< small > 项目贡献权重 { { detail . contributionWeight } } · 合格线 { { detail . passScore } } < / small >
< / div >
< / header >
< section class = "workbench-metrics" >
< div > < span > 学生数 < / span > < b > { { detail . recordTotal } } < / b > < / div >
< div > < span > 本页平均 < / span > < b > { { detailAverage } } < / b > < / div >
< div > < span > 本页通过 < / span > < b > { { detailPassed } } < / b > < / div >
< div > < span > 评分项 < / span > < b > { { detail . items . length } } < / b > < / div >
< / section >
< div class = "record-toolbar" >
< el-input
v-model = "recordKeyword"
clearable
placeholder = "搜索学号、姓名或班级"
@keyup.enter ="recordPage = 1; loadDetail()"
@clear ="recordPage = 1; loadDetail()"
/ >
< el-button @click ="recordPage = 1; loadDetail()" > 查询 < / el -button >
< el-button v-if = "detail.canEdit" :icon="Refresh" @click="syncParticipants" > 同步参与名单 < / el -button >
< el-button v-if = "detail.canEdit" :icon="Setting" @click="openSettings" > 评分方案 < / el -button >
< / div >
< el-table :data = "detail.records" row -key = " id " border class = "grade-record-table" >
< el-table-column type = "expand" width = "48" >
< template # default = "{ row }" >
< div class = "record-evidence" >
< label >
< span > 提交物或报告地址 < / span >
< el-input v-if = "detail.canEdit" v-model="row.submissionReference" maxlength="500" / >
< b v-else > {{ row.submissionReference | | ' 未登记 ' }} < / b >
< / label >
< label >
< span > 提交时间 < / span >
< el-date-picker
v-if = "detail.canEdit"
v-model = "row.submittedAt"
type = "datetime"
value -format = " YYYY -MM -DDTHH : mm : ss "
placeholder = "未登记"
/ >
< b v-else > {{ row.submittedAt ? new Date ( row.submittedAt ) .toLocaleString ( ' zh -CN ' ) : ' 未登记 ' }} < / b >
< / label >
< label >
< span > 迟交 < / span >
< el-switch v-if = "detail.canEdit" v-model="row.isLate" / >
< b v-else > {{ row.isLate ? ' 是 ' : ' 否 ' }} < / b >
< / label >
< label class = "comment-field" >
< span > 教师总评 < / span >
< el-input v-if = "detail.canEdit" v-model="row.teacherComment" type="textarea" maxlength="500" show -word -limit / >
< b v-else > {{ row.teacherComment | | ' 无 ' }} < / b >
< / label >
< label
v-for = "item in detail.items"
:key = "item.id"
class = "comment-field"
>
< span > { { item . name } } 评语 < / span >
< el-input
v-if = "detail.canEdit"
: model -value = " itemScore ( row , item.id ) ? .comment "
maxlength = "300"
@ update : model -value = " ( value : string ) = > { const target = itemScore ( row , item . id ) ; if ( target ) target . comment = value } "
/>
<b v-else>{{ itemScore(row, item.id)?.comment || '无' }}</b>
</label>
</div>
</template>
</el-table-column>
<el-table-column label=" 学生 " fixed min-width=" 180 ">
<template #default=" { row } ">
<b>{{ row.name }}</b>
<small class=" student - line ">{{ row.studentNumber }} · {{ row.className }}</small>
</template>
</el-table-column>
<el-table-column label=" 参与状态 " width=" 125 ">
<template #default=" { row } ">
<el-select v-if=" detail . canEdit " v-model=" row . participationStatus " size=" small ">
<el-option v-for=" ( label , value ) in participationLabels " :key=" value " :label=" label " :value=" value " />
</el-select>
<span v-else>{{ participationLabels[row.participationStatus] }}</span>
</template>
</el-table-column>
<el-table-column
v-for=" item in detail . items "
:key=" item . id "
:label=" ` ${ item . name } ${ item . weight } % ` "
width=" 125 "
>
<template #default=" { row } ">
<el-input-number
v-if=" detail . canEdit && [ 'Completed' , 'Makeup' ] . includes ( row . participationStatus ) "
:model-value=" itemScore ( row , item . id ) ? . score "
:min=" 0 "
:max=" 100 "
:step=" 0.5 "
:controls=" false "
size=" small "
@update:model-value=" ( value : number | undefined ) => { const target = itemScore ( row , item . id ) ; if ( target ) target . score = value } "
/>
<span v-else>{{ itemScore(row, item.id)?.score ?? '—' }}</span>
</template>
</el-table-column>
<el-table-column label=" 安全违规 " width=" 105 ">
<template #default=" { row } ">
<el-switch v-if=" detail . canEdit " v-model=" row . safetyViolation " />
<el-tag v-else-if=" row . safetyViolation " type=" danger " size=" small ">是</el-tag>
<span v-else>否</span>
</template>
</el-table-column>
<el-table-column label=" 次数 " width=" 90 ">
<template #default=" { row } ">
<el-input-number v-if=" detail . canEdit " v-model=" row . attemptNumber " :min=" 1 " :max=" 20 " :controls=" false " size=" small " />
<span v-else>{{ row.attemptNumber }}</span>
</template>
</el-table-column>
<el-table-column label=" 实验总评 " fixed=" right " width=" 105 ">
<template #default=" { row } ">
<b class=" record - total " :class=" { failed : previewTotal ( row ) != null && previewTotal ( row ) ! < detail . passScore } ">
{{ previewTotal(row) ?? '—' }}
</b>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page=" recordPage "
v-model:page-size=" recordPageSize "
:total=" detail . recordTotal "
:page-sizes=" [ 20 , 50 , 100 ] "
layout=" total , sizes , prev , pager , next "
@current-change=" loadDetail "
@size-change=" recordPage = 1 ; loadDetail ( ) "
/>
<footer class=" workbench - actions ">
<div>
<b v-if=" detail . reviewComment ">审核意见:{{ detail.reviewComment }}</b>
<span>开课学院:{{ detail.courseCollegeName }}</span>
</div>
<div>
<el-button v-if=" detail . canEdit " :icon=" EditPen " :loading=" saving " @click=" saveRecords ">保存本页</el-button>
<el-button v-if=" detail . canEdit " type=" primary " :icon=" Promotion " @click=" submitSheet ">提交审核</el-button>
<el-button v-if=" detail . canReview " type=" danger " plain @click=" returnSheet ">退回修改</el-button>
<el-button v-if=" detail . canReview " type=" success " :icon=" Check " @click=" approveSheet ">审核通过</el-button>
<el-button v-if=" detail . canPublish " type=" primary " :icon=" Promotion " @click=" publishSheet " > 发布成绩 < / el-button >
< / div >
< / footer >
< / div >
< / el-drawer >
< / main >
< / template >
< style scoped >
. experiment - grade - page { -- grade - blue : # 245 d82 ; -- grade - teal : # 168276 ; -- grade - ink : # 173041 ; display : grid ; gap : 18 px ; }
. grade - intro { align - items : flex - start ; }
. assessment - flow { display : grid ; grid - template - columns : 1 fr 48 px 1 fr 48 px 1 fr ; align - items : stretch ; border : 1 px solid # d8e4e7 ; background : # fff ; }
. assessment - flow > div { padding : 15 px 18 px ; display : grid ; gap : 4 px ; border - top : 4 px solid var ( -- grade - blue ) ; }
. assessment - flow > div : nth - of - type ( 2 ) { border - top - color : # 4 c778f ; }
. assessment - flow > div : nth - of - type ( 3 ) { border - top - color : var ( -- grade - teal ) ; }
. assessment - flow > div span { color : # 66808 c ; font : 700 10 px / 1.2 Consolas , monospace ; letter - spacing : .08 em ; }
. assessment - flow > div b { color : var ( -- grade - ink ) ; font - size : 15 px ; }
. assessment - flow > div small { color : var ( -- muted ) ; }
. assessment - flow > i { display : grid ; place - items : center ; color : # 8 aa0aa ; font - style : normal ; background : # f4f7f8 ; }
. grade - toolbar { display : flex ; flex - wrap : wrap ; align - items : center ; gap : 10 px ; }
. grade - toolbar . el - select { width : 220 px ; }
. grade - toolbar > span { margin - left : auto ; color : var ( -- muted ) ; font - size : 12 px ; }
. grade - project - list { display : grid ; gap : 12 px ; min - height : 180 px ; }
. grade - project - card { min - width : 0 ; padding : 16 px 18 px ; display : grid ; grid - template - columns : 130 px minmax ( 220 px , 1.4 fr ) minmax ( 220 px , 1 fr ) auto ; align - items : center ; gap : 18 px ; border : 1 px solid # d9e4e8 ; border - left : 5 px solid var ( -- grade - blue ) ; background : # fff ; box - shadow : 0 6 px 18 px rgb ( 30 68 86 / 5 % ) ; }
. project - mark { display : grid ; gap : 7 px ; }
. project - mark span { overflow : hidden ; color : # 607 b88 ; font : 700 10 px / 1.3 Consolas , monospace ; text - overflow : ellipsis ; }
. project - mark b { color : var ( -- grade - blue ) ; font - size : 12 px ; }
. project - summary { min - width : 0 ; }
. project - summary h3 { margin : 0 0 5 px ; color : var ( -- grade - ink ) ; font - size : 17 px ; }
. project - summary p { margin : 0 0 4 px ; color : # 365 f73 ; font - size : 13 px ; }
. project - summary small { display : block ; overflow : hidden ; color : var ( -- muted ) ; text - overflow : ellipsis ; white - space : nowrap ; }
. sheet - progress , . sheet - empty { display : grid ; gap : 5 px ; color : # 607681 ; font - size : 12 px ; }
. sheet - progress b { color : var ( -- grade - teal ) ; }
. sheet - empty b { color : # 5 d7480 ; }
. student - result - grid { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 340 px , 1 fr ) ) ; gap : 15 px ; min - height : 180 px ; }
. student - result - card { overflow : hidden ; border : 1 px solid # d8e4e6 ; border - top : 5 px solid # 80939 b ; background : # fff ; }
. student - result - card . is - passed { border - top - color : var ( -- grade - teal ) ; }
. student - result - card . is - failed { border - top - color : # b5543d ; }
. student - result - card > header { padding : 18 px ; display : flex ; justify - content : space - between ; gap : 16 px ; border - bottom : 1 px solid # e4eaed ; }
. student - result - card header span { color : # 607 d89 ; font : 700 10 px / 1.3 Consolas , monospace ; }
. student - result - card h3 { margin : 5 px 0 ; color : var ( -- grade - ink ) ; font - size : 18 px ; }
. student - result - card header p { margin : 0 ; color : var ( -- muted ) ; font - size : 12 px ; }
. score - seal { width : 72 px ; flex : 0 0 72 px ; display : grid ; place - content : center ; border - left : 1 px solid # dce6e8 ; text - align : center ; }
. score - seal strong { color : var ( -- grade - teal ) ; font : 750 28 px / 1 Consolas , monospace ; }
. is - failed . score - seal strong { color : # b5543d ; }
. score - seal small { margin - top : 5 px ; color : var ( -- muted ) ; }
. result - facts { padding : 12 px 18 px ; display : grid ; grid - template - columns : repeat ( 4 , 1 fr ) ; gap : 8 px ; background : # f5f8f8 ; }
. result - facts span { display : grid ; gap : 3 px ; color : # 758991 ; font - size : 10 px ; }
. result - facts b { color : # 365762 ; font - size : 12 px ; }
. result - rubric { padding : 14 px 18 px ; display : grid ; gap : 8 px ; }
. result - rubric > div { padding : 8 px 10 px ; display : grid ; grid - template - columns : 1 fr auto ; gap : 4 px 10 px ; border - left : 3 px solid # 9 cb6bb ; background : # f7f9f9 ; }
. result - rubric span { color : # 58727 c ; font - size : 12 px ; }
. result - rubric b { color : var ( -- grade - ink ) ; }
. result - rubric small { grid - column : 1 / - 1 ; color : var ( -- muted ) ; }
. safety - alert , . teacher - comment { margin : 0 18 px 15 px ; padding : 10 px 12 px ; font - size : 12 px ; }
. safety - alert { color : # 8 e382b ; background : # fff0ed ; }
. teacher - comment { color : # 49686 e ; background : # eef6f4 ; }
. scheme - form { display : grid ; gap : 18 px ; }
. scheme - basics { display : grid ; grid - template - columns : 1 fr 1 fr ; gap : 14 px ; }
. scheme - basics label { padding : 14 px ; display : grid ; gap : 8 px ; border : 1 px solid # dce5e8 ; background : # f8fafb ; }
. scheme - basics label > span { color : var ( -- grade - ink ) ; font - weight : 700 ; }
. scheme - basics label small { color : var ( -- muted ) ; line - height : 1.5 ; }
. rubric - editor { border : 1 px solid # dce5e8 ; }
. rubric - editor header { padding : 12 px 14 px ; display : flex ; justify - content : space - between ; border - bottom : 1 px solid # dce5e8 ; background : # f3f7f7 ; }
. rubric - editor header div { display : flex ; align - items : baseline ; gap : 10 px ; }
. rubric - editor header span { color : var ( -- grade - teal ) ; font : 700 10 px / 1 Consolas , monospace ; }
. rubric - editor header b { color : var ( -- grade - ink ) ; }
. rubric - editor header strong { color : var ( -- grade - teal ) ; }
. rubric - editor header strong . invalid { color : # b5543d ; }
. rubric - row { padding : 9 px 12 px ; display : grid ; grid - template - columns : 1.3 fr 150 px 100 px 20 px auto ; align - items : center ; gap : 8 px ; border - bottom : 1 px solid # edf1f2 ; }
. rubric - editor > . el - button { margin : 6 px 10 px ; }
: global ( . experiment - grade - drawer . el - drawer _ _body ) { padding : 0 ; }
. grade - workbench { min - height : 100 % ; display : grid ; align - content : start ; gap : 15 px ; background : # f4f7f8 ; }
. workbench - head { padding : 18 px 22 px ; display : flex ; justify - content : space - between ; gap : 20 px ; border - bottom : 1 px solid # d9e3e6 ; background : # fff ; }
. workbench - head span { color : # 5 d7c89 ; font : 700 10 px / 1.3 Consolas , monospace ; }
. workbench - head h3 { margin : 5 px 0 ; color : var ( -- grade - ink ) ; font - size : 21 px ; }
. workbench - head p { margin : 0 ; color : var ( -- muted ) ; }
. workbench - status { display : grid ; justify - items : end ; align - content : center ; gap : 7 px ; }
. workbench - status small { color : var ( -- muted ) ; }
. workbench - metrics { margin : 0 18 px ; display : grid ; grid - template - columns : repeat ( 4 , 1 fr ) ; border : 1 px solid # d9e4e6 ; background : # fff ; }
. workbench - metrics div { padding : 12 px 16 px ; display : grid ; gap : 3 px ; border - right : 1 px solid # e2e9eb ; }
. workbench - metrics div : last - child { border - right : 0 ; }
. workbench - metrics span { color : var ( -- muted ) ; font - size : 11 px ; }
. workbench - metrics b { color : var ( -- grade - ink ) ; font - size : 20 px ; }
. record - toolbar { margin : 0 18 px ; display : flex ; gap : 8 px ; }
. record - toolbar . el - input { max - width : 320 px ; }
. grade - record - table { width : calc ( 100 % - 36 px ) ; margin : 0 18 px ; }
. student - line { display : block ; margin - top : 3 px ; color : var ( -- muted ) ; }
. record - total { color : var ( -- grade - teal ) ; font : 750 16 px / 1 Consolas , monospace ; }
. record - total . failed { color : # b5543d ; }
. record - evidence { padding : 14 px 18 px ; display : grid ; grid - template - columns : repeat ( 3 , minmax ( 180 px , 1 fr ) ) ; gap : 12 px ; background : # f7faf9 ; }
. record - evidence label { display : grid ; align - content : start ; gap : 6 px ; }
. record - evidence label > span { color : # 536 f78 ; font - size : 11 px ; font - weight : 700 ; }
. record - evidence label > b { color : # 304 e58 ; font - size : 12 px ; font - weight : 500 ; }
. record - evidence . comment - field { grid - column : span 2 ; }
. grade - workbench > . el - pagination { margin : 0 18 px ; justify - content : flex - end ; }
. workbench - actions { padding : 15 px 20 px ; display : flex ; justify - content : space - between ; gap : 14 px ; border - top : 1 px solid # d9e3e6 ; background : # fff ; }
. workbench - actions > div { display : flex ; flex - wrap : wrap ; align - items : center ; gap : 8 px ; }
. workbench - actions b { color : # a24f36 ; font - size : 12 px ; }
. workbench - actions span { color : var ( -- muted ) ; font - size : 12 px ; }
@ media ( max - width : 980 px ) {
. grade - project - card { grid - template - columns : 110 px minmax ( 0 , 1 fr ) auto ; }
. sheet - progress , . sheet - empty { grid - column : 2 ; }
. grade - project - card > . el - button { grid - column : 3 ; grid - row : 1 / span 2 ; }
}
@ media ( max - width : 700 px ) {
. assessment - flow { grid - template - columns : 1 fr ; }
. assessment - flow > i { display : none ; }
. grade - toolbar { align - items : stretch ; flex - direction : column ; }
. grade - toolbar . el - select { width : 100 % ; }
. grade - toolbar > span { margin - left : 0 ; }
. grade - project - card { grid - template - columns : 1 fr ; }
. sheet - progress , . sheet - empty , . grade - project - card > . el - button { grid - column : 1 ; grid - row : auto ; width : 100 % ; }
. student - result - grid { grid - template - columns : 1 fr ; }
. student - result - card > header { align - items : stretch ; flex - direction : column ; }
. score - seal { width : auto ; padding - top : 12 px ; grid - template - columns : auto auto ; gap : 8 px ; border - top : 1 px solid # dce6e8 ; border - left : 0 ; }
. result - facts { grid - template - columns : repeat ( 2 , 1 fr ) ; }
. scheme - basics { grid - template - columns : 1 fr ; }
. rubric - row { grid - template - columns : 1 fr 120 px 80 px 20 px ; }
. rubric - row . el - button { grid - column : 1 / - 1 ; justify - self : end ; }
. workbench - head , . workbench - actions { align - items : stretch ; flex - direction : column ; }
. workbench - status { justify - items : start ; }
. workbench - metrics { grid - template - columns : repeat ( 2 , 1 fr ) ; }
. record - toolbar { flex - wrap : wrap ; }
. record - toolbar . el - input { max - width : none ; flex : 1 1 100 % ; }
. record - evidence { grid - template - columns : 1 fr ; }
. record - evidence . comment - field { grid - column : 1 ; }
}
< / style >