You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

726 lines
24 KiB

<template>
<div>
<el-card shadow="hover" class="m-b-20 head-card">
<div class="flex-between m-b-20">
<el-page-header @back="back" :content="goodsName"></el-page-header>
</div>
</el-card>
<div v-loading="loading">
<el-card shadow="hover" class="m-b-20">
<el-tabs v-model="classId" @tab-click="classChange">
<el-tab-pane v-for="(item, i) in classList" :key="i" :name="item.id" :label="item.className"></el-tab-pane>
</el-tabs>
<el-card shadow="hover" class="m-b-20">
<el-tabs v-model="curTab" @tab-click="tabChange">
<el-tab-pane v-for="(item) in tabs" :label="item.name" :name="item.id" :key="item.id"></el-tab-pane>
</el-tabs>
<div class="stat">
<div class="nums">
<div class="item">
<p class="name">实验总人数</p>
<p class="val">{{ peopleNum }}</p>
</div>
<div class="item item2">
<p class="name">实验平均分</p>
<p class="val">{{ avgScore }}</p>
</div>
<div class="item item3">
<p class="name">实验最高分</p>
<p class="val">{{ maxScore || 0 }}</p>
</div>
<div class="item item4">
<p class="name">实验最低分</p>
<p class="val">{{ minScore || 0 }}</p>
</div>
</div>
<div class="chart" id="chart"></div>
</div>
</el-card>
<el-card shadow="hover" class="m-b-20">
<h6 style="font-size: 16px">错误率分析</h6>
<div class="wrong">
<div class="line">
<div class="jud-name">
错误率最高:
<div class="jud-html" v-html="max.projectName"></div>
</div>
<span>参加考试{{ permissions ? (max.numberOfParticipants || 0) : (max.quantityAfterWeightRemoval || 0)
}}人&emsp;|&emsp;{{ curTab == 1 ? `共${max.itemErrorCount || 0}人做错,` : '' }}错误率{{ max.errorRate || 0
}}%</span>
</div>
<div class="line">
<div class="jud-name">
错误率最低:
<div class="jud-html" v-html="min.projectName"></div>
</div>
<span>参加考试{{ permissions ? (min.numberOfParticipants || 0) : (min.quantityAfterWeightRemoval || 0)
}}人&emsp;|&emsp;{{ curTab == 1 ? `共${min.itemErrorCount || 0}人做错,` : '' }}错误率{{ min.errorRate || 0
}}%</span>
</div>
</div>
<div class="chart" id="chart1"></div>
</el-card>
<el-card shadow="hover">
<div class="flex-between m-b-20">
<div>
<el-input placeholder="请输入姓名/学号" prefix-icon="el-icon-search" v-model="keyword" clearable></el-input>
</div>
<div>
<el-button type="primary" @click="exportData">导出成绩列表</el-button>
<!-- <el-button type="primary"
@click="exportReport">导出成绩详情</el-button> -->
</div>
</div>
<el-table :data="listData" class="table" ref="table" :key="curTab" header-align="center"
@selection-change="handleSelectionChange" row-key="reportId">
<el-table-column type="selection" width="55" align="center" :reserve-selection="true"></el-table-column>
<el-table-column type="index" width="60" label="序号" align="center">
<template slot-scope="scope">
{{ scope.$index + (page - 1) * pageSize + 1 }}
</template>
</el-table-column>
<el-table-column prop="userName" label="姓名" align="center"></el-table-column>
<el-table-column prop="workNumber" label="学号" align="center"></el-table-column>
<template v-if="curTab == 0">
<el-table-column label="练习项目数" align="center">
<template slot-scope="scope">
{{ scope.row.totalNumberOfPractices }}
</template>
</el-table-column>
<el-table-column prop="numberOfExercises" label="练习次数" width="90" align="center"></el-table-column>
<el-table-column label="累计练习时长" align="center">
<template slot-scope="scope">
{{ scope.row.cumulativePracticeTime }}min
</template>
</el-table-column>
</template>
<template v-else>
<el-table-column prop="totalNumberOfParticipants" label="参加考核次数" align="center">
</el-table-column>
<el-table-column prop="averageTimeSpent" label="平均用时" align="center">
<template slot-scope="scope">
{{ scope.row.averageTimeSpent }}min
</template>
</el-table-column>
</template>
<el-table-column prop="avgScore" label="平均分" align="center">
<template slot-scope="scope">
{{ curTab == 0 ? scope.row.avgScore : scope.row.averageScore }}
</template>
</el-table-column>
<el-table-column prop="maxScore" label="最高分" align="center"></el-table-column>
<el-table-column prop="minScore" label="最低分" align="center"></el-table-column>
<el-table-column label="操作" align="center" width="140">
<template slot-scope="scope">
<el-button type="text" @click="show(scope.row)">查看成绩详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination background layout="total, prev, pager, next" :total="total"
@current-change="handleCurrentChange" :current-page="page">
</el-pagination>
</div>
</el-card>
</el-card>
<el-dialog title="成绩详情" :visible.sync="detailVisible" width="900px" :key="curTab" :close-on-click-modal="false">
<div class="m-b-10 text-right">
<el-button type="primary" @click="exportDetail">导出</el-button>
</div>
<el-table :data="details" :key="curTab" header-align="center" row-key="id">
<el-table-column type="index" width="60" label="序号" align="center">
<template slot-scope="scope">
{{ scope.$index + (pageDetail - 1) * pageSizeDetail + 1 }}
</template>
</el-table-column>
<el-table-column prop="userName" label="姓名" width="100" align="center">
</el-table-column>
<el-table-column prop="workNumber" label="学号" width="100" align="center"></el-table-column>
<template v-if="curTab == 0">
<el-table-column prop="projectName" label="项目名称" min-width="200" align="center"></el-table-column>
<el-table-column prop="averageDuration" label="平均练习时长" width="110" align="center">
<template slot-scope="scope">
{{ scope.row.averageDuration }}min
</template>
</el-table-column>
<el-table-column prop="averageScore" label="平均分" min-width="100" align="center"></el-table-column>
</template>
<template v-else>
<el-table-column prop="experimentalName" label="考核名称" min-width="200" align="center"></el-table-column>
<el-table-column prop="averageDuration" label="用时" width="100" align="center">
<template slot-scope="scope">
{{ scope.row.timeSum }}min
</template>
</el-table-column>
<el-table-column prop="score" label="分数" min-width="100" align="center"></el-table-column>
</template>
<el-table-column label="成绩报告" align="center" width="90">
<template slot-scope="scope">
<el-button type="text" @click="toReport(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination background layout="total, prev, pager, next" :total="totalDetail" :page-size="pageSizeDetail"
@current-change="handleCurrentDetailChange" :current-page="pageDetail">
</el-pagination>
</div>
<span slot="footer" class="dialog-footer">
<el-button size="small" type="primary" @click="detailVisible = false">确定</el-button>
</span>
</el-dialog>
</div>
</div>
</template>
<script>
import Setting from "@/setting";
import util from "@/libs/util";
import echarts from "echarts";
import axios from 'axios';
export default {
data () {
return {
tabs: [
{
id: '0',
name: '练习成绩'
},
{
id: '1',
name: '考核成绩'
}
],
curTab: this.$route.query.permissions || '0',
permissions: +this.$route.query.permissions,
goodsName: this.$route.query.curriculumName,
accountId: this.$route.query.accountId,
id: +this.$route.query.id,
cid: +this.$route.query.cid,
classId: this.$route.query.classId || '',
classList: [],
keyword: "",
searchTimer: null,
listDataAll: [],
listData: [],
multipleSelection: [],
page: +this.$route.query.page || 1,
pageSize: 10,
total: 0,
peopleNum: 0, // 总人数
avgScore: 0, // 平均分
maxScore: 0,
minScore: 0,
errorAnalysis: [],
max: {},
min: {},
token: util.session.get(Setting.tokenKey),
detailVisible: false,
details: [],
multipleSelectionActivation: [],
pageDetail: 1,
pageSizeDetail: 5,
totalDetail: 0,
curRow: {},
stageNumber: {},
loading: false,
};
},
watch: {
keyword: function (val) {
clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(() => {
this.initData()
}, 500)
}
},
mounted () {
this.classId = this.$route.query.classId || ''
this.getClass()
},
methods: {
// 成绩
async getData () {
this.loading = true
const per = +this.curTab
const res = await this.$post(this.api.productReadScore, {
pageNum: this.page,
pageSize: this.pageSize,
keyWord: this.keyword,
mallId: this.id,
cid: this.cid,
permissions: per,
classId: this.classId
})
this.listData = per ? res.listOfAssessmentResults.records : res.userScoreList.records
this.total = per ? res.listOfAssessmentResults.total : res.userScoreList.total
const stat = res.experimentalStatistics
this.avgScore = stat.averageScore
this.peopleNum = stat.experimentalPopulation
this.maxScore = stat.maxScore
this.minScore = stat.minScore
this.stageNumber = res.stageNumber
const err = (per ? res.testErrorRateUnderProduct : res.projectErrorRateAnalysisUnderProduct) || []
err.forEach(e => {
e.errorRate = (+e.errorRate).toFixed(2)
})
this.errorAnalysis = err || []
this.max = err.length ? err[0] : {}
this.min = err.length ? err[err.length - 1] : {}
this.errorChart()
const { row } = this.$store.state.achievement
row && this.$nextTick(() => {
window.scrollTo(0, document.documentElement.scrollHeight)
this.show(row)
this.$store.commit('achievement/setRow', null)
})
this.getChart()
},
initData () {
this.page = 1
this.getData()
},
// 获取班级下拉框数据
getClass () {
this.$post(this.api.allClassesInOurSchool).then(({ data }) => {
data.forEach(e => {
e.id = e.id + ''
})
if (data.length && !this.classId) this.classId = data[0].id
this.classList = data
this.initData()
}).catch(res => { })
},
// 班级切换回调
classChange () {
this.$router.push({
path: 'course',
query: {
...this.$route.query,
classId: this.classId
}
})
this.initData()
},
// 练习考核切换
tabChange () {
this.permissions = +this.curTab
this.$router.push({
path: 'course',
query: {
...this.$route.query,
permissions: this.curTab
}
})
this.initData()
},
// 导出(有勾选:就导勾选中的;没有勾选:就导全部)
async exportData () {
let list = this.multipleSelection
const practice = this.curTab == 0 // 练习
// 没勾选,则查询所有成绩
if (!list.length) {
const res = await this.$post(this.api.productReadScore, {
pageNum: 1,
pageSize: 10000,
mallId: this.id,
cid: this.cid,
permissions: +this.curTab,
classId: this.classId
})
list = practice ? res.userScoreList.records : res.listOfAssessmentResults.records
}
const curClass = this.classList.find(e => e.id == this.classId)
list.forEach(e => {
e.className = curClass ? curClass.className : ''
e.goodsName = this.goodsName
e.cid = this.cid
})
axios.post(this.api[practice ? 'exportProductPracticeResults' : 'exportProductAssessResults'], list, {
headers: {
token: this.token
},
responseType: 'blob'
}).then((res) => {
util.downloadFileDirect(`${practice ? '练习' : '考核'}成绩列表.xlsx`, new Blob([res.data]))
}).catch(res => { })
},
// 导出实验报告
exportReport () {
// 没选择数据,则导出全部
const list = this.multipleSelection.length ? this.multipleSelection : this.listDataAll
list.forEach(async e => {
if (e.reportId) {
try {
const { report, userScores } = await this.$get(`${this.api.reportDetail}?reportId=${e.reportId}`)
userScores.map((e, i) => {
if (e.answer && typeof e.answer === 'string') e.answer = e.answer.replace(/<[^>]+>/g, '').replace(/(&nbsp;|&amp;|%s)/g, '').replace(/>/g, '&gt;').replace(/</g, '&lt;')
})
for (const i in report) {
if (report[i] && typeof report[i] === 'string') report[i] = report[i].replace(/<[^>]+>/g, '')
}
report.purpose = report.purpose.replace(/<[^>]+>/g, '')
const res = await this.$post(this.api[userScores.find(e => e.lcRuleRecords) ? 'exportBankExperimentReport' : 'exportLabReport'], {
...report,
experimentalData: userScores
})
util.downloadFileDirect(`${e.userName}的实验报告.docx`, new Blob([res]))
} catch (e) { }
}
})
},
handleDelete (row) { // 删除
this.$confirm("确定要删除吗?", "提示", {
type: "warning"
}).then(() => {
this.$post(this.api.deleteExperimentalReport, [row.reportId]).then(res => {
util.successMsg("删除成功");
this.getData();
}).catch(res => {
});
}).catch(() => {
});
},
delAllData () { // 批量删除
if (this.multipleSelection.length) {
this.$confirm("该项目下的所有成绩报告将会删除,是否继续?", "提示", {
type: "warning"
}).then(() => {
let ids = this.multipleSelection.map(item => {
return item.reportId;
});
this.$post(this.api.deleteExperimentalReport, ids).then(res => {
this.multipleSelection = [];
this.$refs.table.clearSelection();
util.successMsg("删除成功");
this.getData();
}).catch(res => {
});
}).catch(() => {
});
} else {
util.errorMsg("请先选择数据 !");
}
},
handleSelectionChange (val) { // 多选
this.multipleSelection = val;
},
handleCurrentChange (val) { // 切换分页
this.$router.push({
path: 'course',
query: {
...this.$route.query,
page: this.val
}
})
this.page = val
this.getData();
},
// 打开成绩详情
async show (row) {
this.curRow = row
this.detailVisible = true
this.initDetailData()
},
// 查询成绩详情
async getDetail () {
const { data } = await this.$post(this.api.productReadGradeDetails, {
pageNum: this.pageDetail,
pageSize: this.pageSizeDetail,
mallId: this.id,
permissions: +this.curTab,
studentAccountId: this.curRow.accountId
})
this.details = data.records
this.totalDetail = data.total
},
initDetailData () {
this.pageDetail = 1
this.getDetail()
},
// 详情分页
handleCurrentDetailChange (val) {
this.pageDetail = val
this.getDetail()
},
// 导出
async exportDetail () {
const practice = this.curTab == 0 // 练习
if (this.details.length) {
const res = await axios.post(this.api[practice ? 'exportDetailsOfStudentPracticeScores' : 'exportDetailsOfStudentAssessmentResults'], {
pageNum: 1,
pageSize: 1000,
mallId: this.id,
permissions: +this.curTab,
studentAccountId: this.curRow.accountId,
goodsName: this.curriculumgoodsNameName
}, {
headers: {
token: this.token
},
responseType: 'blob'
})
util.downloadFileDirect(`${this.curRow.userName}_${practice ? '练习' : '考核'}成绩详情.xlsx`, new Blob([res.data]))
}
},
// 查看成绩报告
toReport (row) {
this.$store.commit('achievement/setRow', this.curRow)
// 考核跳实验报告,练习跳项目维度的成绩详情
this.$router.push(this.curTab == 1 ? `show?reportId=${row.reportId}` : `project?id=${row.projectId || ''}&paperId=${row.paperId || ''}&projectName=${row.projectName}&classId=${this.curRow.classId || ''}&workNumber=${row.workNumber || row.userName}&mallId=${this.id}&fromCourse=1`)
},
getChart () { // 初始化折线图
const data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
for (const i in this.stageNumber) {
data[+(i.replace('num', '')) - 1] = this.stageNumber[i]
}
let myChart = echarts.init(document.getElementById("chart"));
myChart.setOption({
title: { text: "实验分数分布图" },
tooltip: {},
xAxis: {
name: this.curTab == 1 ? "分数" : "平均分",
type: "category",
boundaryGap: false,
interval: 10,
data: ["0-10", "10-20", "20-30", "30-40", "40-50", "50-60", "60-70", "70-80", "80-90", "90-100"]
},
yAxis: {
name: "人数",
type: "value",
minInterval: 5,
},
series: [{
data,
type: "line",
areaStyle: {},
label: {
show: true,
position: 'top'
},
color: ["#8191fd"]
}]
})
this.loading = false
},
// 错误率统计图
errorChart () {
const data = this.errorAnalysis
const maxFontLength = data.length > 13 ? 6 : 10
const xData = []
const yData = []
data.forEach(e => {
const el = document.createElement('div')
el.innerHTML = e.projectName
xData.push(el.innerText)
yData.push(e.errorRate)
})
const option = {
tooltip: {
trigger: 'axis',
},
grid: {
left: '5%',
right: '5%',
top: '10%',
bottom: '30%'
},
dataZoom: [//滑动条
{
// xAxisIndex: 0,//这里是从X轴的0刻度开始
show: true,
type: 'slider',
start: 0,
end: 150,
xAxisIndex: [0],
bottom: -10,
},
// {
// // xAxisIndex: 0,//这里是从X轴的0刻度开始
// type: 'inside',
// realtime: true,
// start: 0,
// end: 150,
// }
],
xAxis: [{
type: 'category',
axisLine: {
lineStyle: {
color: '#57617B'
}
},
interval: 5,
splitNumber: 5,
axisLabel: {
// interval: '',
textStyle: {
color: '#333',
},
formatter: function (value, index) {
value = value.substring(0, maxFontLength) + (value.length > maxFontLength ? '...' : '')
// if (index % 2 != 0) {
// return '\n\n' + value;
// } else {
// return value;
// }
return value
},
rotate: data.length > 16 ? 45 : 0
},
data: xData
}],
yAxis: [
{
type: 'value',
name: '错误率',
nameGap: 10,
axisLine: {
lineStyle: {
color: '#333'
}
},
axisLabel: {
margin: 10,
textStyle: {
fontSize: 12,
color: '#333'
},
formatter: '{value}%'
},
}
],
series: [{
name: '错误率',
type: 'bar',
barWidth: 25,
axisLabel: {
margin: 10,
textStyle: {
fontSize: 12,
color: '#333'
},
formatter: '{value}%'
},
itemStyle: {
normal: {
barBorderRadius: [10, 10, 0, 0],
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [{
offset: 0,
color: "#009AFD"
}, {
offset: 0.8,
color: "#33DAFF"
}], false),
shadowColor: 'rgba(0, 0, 0, 0.1)',
}
},
data: yData
}
]
}
echarts.init(document.querySelector(`#chart1`)).setOption(option)
},
back () {
this.$router.push(this.$store.state.achievement.referrer)
},
}
};
</script>
<style lang="scss" scoped>
/deep/ .head-card {
.el-card__body {
padding-bottom: 0px;
.el-tabs__header {
margin-bottom: 1px;
.el-tabs__nav-wrap::after {
display: none;
}
.el-tabs__item {
font-size: 18px;
}
}
}
}
.chart {
height: 300px;
}
.stat {
display: flex;
.nums {
display: flex;
align-items: center;
flex-wrap: wrap;
width: 640px;
margin-right: 20px;
.item {
width: 300px;
padding: 30px 30px;
margin: 0 10px;
box-sizing: border-box;
border-radius: 8px;
background: url('../../../assets/img/total.png') 0 0/100% 100% no-repeat;
p {
font-size: 18px;
color: #ffffff;
}
.val {
margin-top: 10px;
color: #ffffff;
font-size: 36px;
}
}
.item:nth-child(2) {
background-image: url('../../../assets/img/avg.png');
}
.item:nth-child(3) {
background-image: url('../../../assets/img/ach1.png');
}
.item:nth-child(4) {
background-image: url('../../../assets/img/ach2.png');
}
}
.chart {
width: calc(100% - 660px);
}
}
.wrong {
.line {
display: flex;
width: 920px;
margin: 0 auto 10px;
.jud-name {
display: inline-flex;
width: 500px;
margin-right: 100px;
}
.jud-html {
max-width: 410px;
}
}
}
</style>