DetailTargetStatistics.vue 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225
  1. <template>
  2. <el-dialog
  3. class="page-dialog"
  4. :visible.sync="modalIsShow"
  5. :close-on-click-modal="false"
  6. :close-on-press-escape="false"
  7. :show-close="false"
  8. append-to-body
  9. fullscreen
  10. @opened="dialogOpend"
  11. @close="dialogClose"
  12. >
  13. <div slot="title" class="box-justify">
  14. <div>
  15. <span>{{ course.courseName }}</span>
  16. </div>
  17. <div>
  18. <el-button
  19. type="primary"
  20. :loading="downloading"
  21. icon="el-icon-download"
  22. @click="toExportNormalScore"
  23. >导出平时成绩</el-button
  24. >
  25. <el-button
  26. type="primary"
  27. :loading="downloading"
  28. icon="el-icon-download"
  29. @click="toExport"
  30. >导出报告</el-button
  31. >
  32. <el-button
  33. type="primary"
  34. :loading="downloading"
  35. icon="el-icon-document-checked"
  36. @click="toSave(false)"
  37. >保存报告</el-button
  38. >
  39. <el-button class="btn-back" @click="cancel">
  40. 返回<i class="el-icon-arrow-right"></i>
  41. </el-button>
  42. </div>
  43. </div>
  44. <div v-if="modalIsShow">
  45. <!-- base info -->
  46. <div class="page-head box-justify">
  47. <h2>
  48. <span>课程基本情况</span>
  49. </h2>
  50. <el-select
  51. v-if="dataList.length > 1"
  52. v-model="curReportId"
  53. placeholder="请选择"
  54. @change="reportChange"
  55. >
  56. <el-option
  57. v-for="(row, index) in dataList"
  58. :key="index"
  59. :label="row.teachClassName"
  60. :value="row.id"
  61. ></el-option>
  62. </el-select>
  63. </div>
  64. <div class="part-box part-box-pad">
  65. <table class="table table-tiny">
  66. <colgroup>
  67. <col width="100" />
  68. <col />
  69. <col width="100" />
  70. <col />
  71. <col width="110" />
  72. <col />
  73. <col width="100" />
  74. <col />
  75. </colgroup>
  76. <tr>
  77. <td class="td-bg">课程名称</td>
  78. <td>{{ commonInfo.courseName }}</td>
  79. <td class="td-bg">课程代码</td>
  80. <td>{{ commonInfo.courseCode }}</td>
  81. <td class="td-bg">课程英文名称</td>
  82. <td>
  83. <el-input
  84. v-model.trim="courseBasicInfo.courseEnName"
  85. placeholder="请填写"
  86. size="small"
  87. :maxlength="50"
  88. ></el-input>
  89. </td>
  90. <td class="td-bg">学分</td>
  91. <td>
  92. <el-input-number
  93. v-model="courseBasicInfo.credit"
  94. placeholder="请录入学分"
  95. size="small"
  96. :min="0"
  97. :max="999"
  98. :controls="false"
  99. style="width: 100px"
  100. ></el-input-number>
  101. </td>
  102. </tr>
  103. <tr>
  104. <td class="td-bg">课程性质</td>
  105. <td>
  106. <el-input
  107. v-model.trim="courseBasicInfo.courseType"
  108. placeholder="请填写"
  109. size="small"
  110. :maxlength="50"
  111. ></el-input>
  112. </td>
  113. <td class="td-bg">开课学院</td>
  114. <td>{{ courseBasicInfo.college }}</td>
  115. <td class="td-bg">开课专业</td>
  116. <td>{{ courseBasicInfo.profession }}</td>
  117. <td class="td-bg">学时</td>
  118. <td>
  119. <el-input-number
  120. v-model="courseBasicInfo.period"
  121. placeholder="请填写"
  122. :min="0"
  123. :max="999"
  124. :controls="false"
  125. size="small"
  126. style="width: 100px"
  127. ></el-input-number>
  128. </td>
  129. </tr>
  130. <tr>
  131. <td class="td-bg">开课班级</td>
  132. <td>
  133. <el-input
  134. v-model.trim="courseBasicInfo.teachingObject"
  135. placeholder="请填写"
  136. size="small"
  137. :maxlength="50"
  138. ></el-input>
  139. </td>
  140. <td class="td-bg">开课学期</td>
  141. <td>{{ courseBasicInfo.openTime }}</td>
  142. <td class="td-bg">学生人数</td>
  143. <td>{{ courseBasicInfo.participantCount }}</td>
  144. <td class="td-bg">期望值</td>
  145. <td>
  146. {{ courseBasicInfo.courseDegree }}
  147. </td>
  148. </tr>
  149. <tr>
  150. <td class="td-bg">任课老师</td>
  151. <td>
  152. <el-input
  153. v-model.trim="courseBasicInfo.teacher"
  154. placeholder="请填写"
  155. size="small"
  156. :maxlength="50"
  157. ></el-input>
  158. </td>
  159. <td class="td-bg">评价人</td>
  160. <td colspan="5">
  161. <el-input
  162. v-model.trim="courseBasicInfo.director"
  163. placeholder="请填写评价负责人姓名"
  164. size="small"
  165. :maxlength="50"
  166. ></el-input>
  167. </td>
  168. </tr>
  169. </table>
  170. </div>
  171. <div class="page-head">
  172. <h2>
  173. <span>课程目标考核分布</span>
  174. <el-popover
  175. popper-class="el-popper-dark"
  176. placement="right-start"
  177. width="500"
  178. trigger="hover"
  179. >
  180. <i class="el-icon-question" slot="reference"></i>
  181. <div>
  182. <p>
  183. 课程目标分布会自动按题库系统里试卷下试题标记的属性内容进行显示
  184. </p>
  185. </div>
  186. </el-popover>
  187. </h2>
  188. </div>
  189. <div class="part-box part-box-pad">
  190. <el-table :data="questionInfoTable">
  191. <el-table-column
  192. label="试题号"
  193. prop="name"
  194. width="200"
  195. align="center"
  196. fixed="left"
  197. ></el-table-column>
  198. <el-table-column
  199. label="合计"
  200. prop="totalScore"
  201. width="60"
  202. align="center"
  203. fixed="left"
  204. ></el-table-column>
  205. <el-table-column
  206. v-for="struct in paperStructs"
  207. :key="struct.mainNumber"
  208. :label="struct.mainNumber"
  209. align="center"
  210. >
  211. <el-table-column
  212. v-for="subNumber in struct.subNumbers"
  213. :key="subNumber"
  214. :label="subNumber"
  215. :prop="`${struct.mainNumber}_${subNumber}`"
  216. align="center"
  217. min-width="50"
  218. >
  219. </el-table-column>
  220. </el-table-column>
  221. </el-table>
  222. <div class="chart-box" style="height: 300px; margin-top: 20px">
  223. <v-chart
  224. v-if="questionInfoChartOption"
  225. :option="questionInfoChartOption"
  226. ></v-chart>
  227. </div>
  228. </div>
  229. <div class="page-head">
  230. <h2>
  231. <span>课程目标达成评价结果</span>
  232. <el-popover
  233. popper-class="el-popper-dark"
  234. placement="right-start"
  235. width="500"
  236. trigger="hover"
  237. >
  238. <i class="el-icon-question" slot="reference"></i>
  239. <div>
  240. <p>1.评价依据:填写通过考核什么内容实现课程目标的评价;</p>
  241. <p>2.目标分值:对应考核环节的满分;</p>
  242. <p>
  243. 3.权重:对应考核环节在对应的课程目标中的权重,目标下各项权重相加等于1;
  244. </p>
  245. <p>4.实际平均分:为参与评价的学生在该环节的平均分;</p>
  246. <p>5.整体课程目标达成评价值:为课程分目标达成评价值的最小值。</p>
  247. </div>
  248. </el-popover>
  249. </h2>
  250. </div>
  251. <div class="part-box part-box-pad">
  252. <h4 class="part-title mb-2">课程考核成绩评价结果</h4>
  253. <table class="table">
  254. <tr class="td-bg">
  255. <th>课程目标</th>
  256. <th>评价依据</th>
  257. <th>评价环节</th>
  258. <th>权重</th>
  259. <th>目标分值</th>
  260. <th>实际平均分</th>
  261. <th>目标达成评价值</th>
  262. </tr>
  263. <template v-for="item in courseTargetList">
  264. <tr
  265. v-for="(evaluation, eindex) in item.evaluationList"
  266. :key="`${item.targetId}-${eindex}`"
  267. >
  268. <!-- 课程目标 -->
  269. <td
  270. v-if="!eindex"
  271. :rowspan="item.evaluationList.length"
  272. style="width: 240px"
  273. >
  274. {{ item.targetName }}
  275. </td>
  276. <!-- 评价依据 -->
  277. <td v-if="!eindex" :rowspan="item.evaluationList.length">
  278. <p
  279. v-for="edesc in item.graduationRequirementPoint.split(',')"
  280. :key="edesc"
  281. >
  282. {{ edesc }}
  283. </p>
  284. </td>
  285. <!-- 评价环节 -->
  286. <td style="width: 140px">{{ evaluation.evaluation }}</td>
  287. <!-- 权重 -->
  288. <td style="width: 80px">
  289. {{ evaluation.targetWeight }}
  290. </td>
  291. <!-- 目标分值 -->
  292. <td style="width: 100px">{{ evaluation.targetScore }}</td>
  293. <!-- 实际平均分 -->
  294. <td style="width: 100px">{{ evaluation.targetAvgScore }}</td>
  295. <!-- 目标达成评价值 -->
  296. <td
  297. v-if="!eindex"
  298. :rowspan="item.evaluationList.length"
  299. style="width: 140px"
  300. >
  301. {{ item.evaluationValue }}
  302. </td>
  303. </tr>
  304. </template>
  305. <tr>
  306. <td colspan="6">课程总目标</td>
  307. <td>{{ courseTargetValue }}</td>
  308. </tr>
  309. </table>
  310. <div class="chart-box" style="height: 400px; margin-top: 20px">
  311. <v-chart
  312. v-if="courseTargetListChartOption"
  313. :option="courseTargetListChartOption"
  314. ></v-chart>
  315. </div>
  316. <h4 class="part-title mb-2">课程考核成绩评价明细结果</h4>
  317. <el-table
  318. :data="studentScoreTable"
  319. :span-method="studentScoreSpanMethod"
  320. >
  321. <el-table-column
  322. label="姓名"
  323. prop="name"
  324. min-width="120"
  325. align="center"
  326. fixed="left"
  327. ></el-table-column>
  328. <el-table-column
  329. label="学号"
  330. prop="studentCode"
  331. width="160"
  332. align="center"
  333. fixed="left"
  334. ></el-table-column>
  335. <el-table-column
  336. v-for="(target, tindex) in courseTargets"
  337. :key="tindex"
  338. :label="target.targetName"
  339. align="center"
  340. >
  341. <el-table-column
  342. v-if="target.finalDimensions.length"
  343. :label="`期末成绩(${target.finalWeight}%)`"
  344. :prop="`${target.targetId}-final`"
  345. width="140"
  346. align="center"
  347. >
  348. </el-table-column>
  349. <el-table-column
  350. v-for="work in target.usualWorks"
  351. :key="`${target.targetId}${work.evaluation}`"
  352. :label="`${work.evaluation}(${work.targetWeight}%)`"
  353. :prop="`${target.targetId}-usual-${work.evaluation}`"
  354. min-width="100"
  355. align="center"
  356. >
  357. </el-table-column>
  358. </el-table-column>
  359. <el-table-column
  360. v-if="comprehensiveScoreShow"
  361. label="综合成绩"
  362. prop="score"
  363. align="center"
  364. width="100"
  365. ></el-table-column>
  366. <el-table-column
  367. label="达成度"
  368. prop="targetRate"
  369. align="center"
  370. width="100"
  371. ></el-table-column>
  372. </el-table>
  373. </div>
  374. <div class="page-head">
  375. <h2>
  376. <span> 课程目标达成度评价结果分析及改进措施 </span>
  377. </h2>
  378. </div>
  379. <div class="part-box part-box-pad">
  380. <div
  381. v-for="item in courseSuggest"
  382. :key="item.targetId"
  383. class="target-suggest mb-4"
  384. >
  385. <h4 class="part-title">{{ item.targetName }}</h4>
  386. <div
  387. v-if="item.chartOption"
  388. class="chart-box"
  389. style="height: 400px; margin: 10px 0"
  390. >
  391. <v-chart :option="item.chartOption"></v-chart>
  392. </div>
  393. <el-form label-position="top">
  394. <el-form-item label="达成情况">
  395. <el-input
  396. v-model="item.finishPoints"
  397. type="textarea"
  398. :autosize="{ minRows: 2, maxRows: 5 }"
  399. resize="none"
  400. placeholder="请输入"
  401. clearable
  402. maxlength="999"
  403. show-word-limit
  404. ></el-input>
  405. </el-form-item>
  406. <el-form-item label="课程支撑毕业要求达成情况评价">
  407. <el-input
  408. v-model="item.requirementPoints"
  409. type="textarea"
  410. :autosize="{ minRows: 2, maxRows: 5 }"
  411. resize="none"
  412. placeholder="请输入"
  413. clearable
  414. maxlength="999"
  415. show-word-limit
  416. ></el-input>
  417. </el-form-item>
  418. <el-form-item label="课程持续改进">
  419. <el-input
  420. v-model="item.courseSuggest"
  421. type="textarea"
  422. :autosize="{ minRows: 2, maxRows: 5 }"
  423. resize="none"
  424. placeholder="请输入"
  425. clearable
  426. maxlength="999"
  427. show-word-limit
  428. ></el-input>
  429. </el-form-item>
  430. </el-form>
  431. </div>
  432. </div>
  433. </div>
  434. <div slot="footer"></div>
  435. </el-dialog>
  436. </template>
  437. <script>
  438. // import { reportData } from "./data";
  439. import {
  440. targetStatisticsDetail,
  441. targetStatisticsReport,
  442. targetStatisticsNormalScoreExport,
  443. targetStatisticsSave,
  444. targetStatisticsChangeCheck,
  445. } from "../../api";
  446. import { downloadByApi } from "@/plugins/download";
  447. import { calcSum, deepCopy, toPrecision } from "@/plugins/utils";
  448. import timeMixin from "@/mixins/timeMixin";
  449. import { omit } from "lodash";
  450. export default {
  451. name: "detail-target-statistics",
  452. mixins: [timeMixin],
  453. props: {
  454. course: {
  455. type: Object,
  456. default() {
  457. return {};
  458. },
  459. },
  460. },
  461. filters: {
  462. percentFilter(val) {
  463. const num = val || 0;
  464. const perc = num % 10 ? 2 : 1;
  465. return ((num || 0) / 100).toFixed(perc);
  466. },
  467. },
  468. data() {
  469. return {
  470. modalIsShow: false,
  471. expectancyList: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
  472. modalForm: {},
  473. dataList: [],
  474. curReportId: "",
  475. commonInfo: {},
  476. courseBasicInfo: {},
  477. paperStructs: [],
  478. questionInfoTable: [],
  479. courseTargetList: [],
  480. courseTargetValue: 0.67,
  481. questionInfoChartOption: null,
  482. courseTargetListChartOption: null,
  483. courseTargets: [],
  484. targetColumnCounts: [],
  485. studentScoreTable: [],
  486. courseSuggest: [],
  487. downloading: false,
  488. // 是否显示综合成绩
  489. comprehensiveScoreShow: true,
  490. };
  491. },
  492. beforeDestroy() {
  493. this.clearSetTs();
  494. },
  495. methods: {
  496. cancel() {
  497. this.modalIsShow = false;
  498. },
  499. open() {
  500. this.modalIsShow = true;
  501. },
  502. dialogClose() {
  503. this.clearSetTs();
  504. },
  505. dialogOpend() {
  506. this.initData();
  507. this.openAutoSave();
  508. },
  509. openAutoSave() {
  510. if (!this.modalIsShow) return;
  511. this.clearSetTs();
  512. this.addSetTime(async () => {
  513. await this.toSave(true);
  514. this.openAutoSave();
  515. }, 1 * 60 * 1000);
  516. },
  517. resetData() {
  518. this.commonInfo = {};
  519. this.courseBasicInfo = {};
  520. this.paperStructs = [];
  521. this.questionInfoTable = [];
  522. this.courseTargetList = [];
  523. this.courseTargetValue = 0.67;
  524. this.questionInfoChartOption = null;
  525. this.courseTargetListChartOption = null;
  526. this.courseTargets = [];
  527. this.targetColumnCounts = [];
  528. this.studentScoreTable = [];
  529. this.courseSuggest = [];
  530. this.downloading = false;
  531. },
  532. buildData(data) {
  533. this.commonInfo = data.commonInfo;
  534. this.courseBasicInfo = data.courseBasicInfo;
  535. this.comprehensiveScoreShow = data.comprehensiveScoreShow;
  536. const courseSuggest = data.courseSuggest
  537. ? JSON.parse(data.courseSuggest)
  538. : [];
  539. const targetSuggestsMap = {};
  540. courseSuggest.forEach((item) => {
  541. targetSuggestsMap[item.targetId] = {
  542. finishPoints: item.finishPoints,
  543. requirementPoints: item.requirementPoints,
  544. courseSuggest: item.courseSuggest,
  545. };
  546. });
  547. this.targetSuggestsMap = targetSuggestsMap;
  548. const courseTargetScatterMap = data.courseTargetScatterMap || {};
  549. this.courseSuggest = data.courseEvaluationResultInfo.targetList.map(
  550. (target) => {
  551. const suggest = targetSuggestsMap[target.targetId] || {};
  552. const finishPoints = suggest.finishPoints || "";
  553. const requirementPoints = suggest.requirementPoints || "";
  554. const courseSuggest = suggest.courseSuggest || "";
  555. const chartData = courseTargetScatterMap[target.targetId] || [];
  556. return {
  557. targetId: target.targetId,
  558. targetName: target.targetName,
  559. finishPoints,
  560. requirementPoints,
  561. courseSuggest,
  562. chartOption: this.getTargetChartOption(
  563. chartData,
  564. target.targetName
  565. ),
  566. };
  567. }
  568. );
  569. this.courseTargetValue =
  570. data.courseEvaluationResultInfo.targetEvaluationSumValue;
  571. this.courseTargetList = data.courseEvaluationResultInfo.targetList.map(
  572. (target) => {
  573. target.normalTargetWeight = calcSum(
  574. target.evaluationList.slice(0, -1).map((elem) => elem.targetWeight)
  575. );
  576. return target;
  577. }
  578. );
  579. const {
  580. courseEvaluationSpreadInfo: { questionInfo, scoreList },
  581. } = data;
  582. this.parsePaperStructs(questionInfo);
  583. this.parseQuestionInfoTable(questionInfo);
  584. this.questionInfoChartOption = this.getQuestionInfoChartOption(scoreList);
  585. this.courseTargetListChartOption = this.getCourseTargetListChartOption();
  586. let examStudentList =
  587. data.courseEvaluationResultDetailInfo.examStudentList;
  588. examStudentList.pop();
  589. examStudentList.splice(examStudentList.length - 2, 1);
  590. this.parseCourseTargets(examStudentList);
  591. this.parseStudentScoreTable(examStudentList);
  592. },
  593. async initData() {
  594. await this.checkChange();
  595. const data = await targetStatisticsDetail({
  596. cultureProgramId: this.course.cultureProgramId,
  597. courseId: this.course.courseId,
  598. examId: this.course.examId,
  599. paperNumber: this.course.paperNumber,
  600. });
  601. this.dataList = data || [];
  602. if (!data.length) return;
  603. let index = 0;
  604. if (this.curReportId) {
  605. index = this.dataList.findIndex((item) => item.id === this.curReportId);
  606. index = index === -1 ? 0 : index;
  607. }
  608. this.curReportId = this.dataList[index].id;
  609. this.buildData(deepCopy(this.dataList[index]));
  610. },
  611. async reportChange() {
  612. this.resetData();
  613. const rowData = this.dataList.find(
  614. (item) => item.id === this.curReportId
  615. );
  616. if (!rowData) return;
  617. this.buildData(deepCopy(rowData));
  618. },
  619. async checkChange() {
  620. const res = await targetStatisticsChangeCheck({
  621. cultureProgramId: this.course.cultureProgramId,
  622. courseId: this.course.courseId,
  623. examId: this.course.examId,
  624. paperNumber: this.course.paperNumber,
  625. report: true,
  626. });
  627. if (res.courseTargetChange) {
  628. this.$notify.warning("课程目标与已保存不一致,请重新设置权重!");
  629. }
  630. if (res.evaluationChange) {
  631. this.$notify.warning(
  632. "评价方式与已保存不一致,请重新设置权重及导入新的平时成绩!"
  633. );
  634. }
  635. if (res.targetScoreChange) {
  636. this.$notify.warning(res.targetScoreChangeStr);
  637. }
  638. },
  639. parsePaperStructs(questionInfo) {
  640. if (!questionInfo || !questionInfo.length) return;
  641. const structMap = {};
  642. questionInfo.forEach((item) => {
  643. if (!structMap[item.mainNumber]) {
  644. structMap[item.mainNumber] = [];
  645. }
  646. structMap[item.mainNumber].push(item.subNumber + "");
  647. });
  648. this.paperStructs = Object.keys(structMap).map((mainNumber) => {
  649. return {
  650. mainNumber,
  651. subNumbers: structMap[mainNumber],
  652. };
  653. });
  654. },
  655. parseQuestionInfoTable(questionInfo) {
  656. const targetMap = {};
  657. const tData = {
  658. name: "目标分值",
  659. totalScore: calcSum(questionInfo.map((item) => item.score || 0)),
  660. };
  661. questionInfo.forEach((question) => {
  662. const qno = `${question.mainNumber}_${question.subNumber}`;
  663. tData[qno] = question.score;
  664. question.targetList.forEach((target) => {
  665. if (!targetMap[target.targetId]) {
  666. targetMap[target.targetId] = {
  667. name: target.targetName,
  668. totalScore: 0,
  669. };
  670. }
  671. if (target.dimensionList.length) {
  672. targetMap[target.targetId][qno] = question.score;
  673. targetMap[target.targetId].totalScore += question.score;
  674. } else {
  675. targetMap[target.targetId][qno] = "";
  676. }
  677. });
  678. });
  679. this.questionInfoTable = [tData, ...Object.values(targetMap)];
  680. },
  681. getQuestionInfoChartOption(scoreList) {
  682. const scoreRange = scoreList.scoreRange.map((item) => {
  683. return {
  684. name: `${item.minScore}~${item.maxScore}`,
  685. value: item.rate,
  686. };
  687. });
  688. scoreRange.unshift({
  689. name: "不及格",
  690. value: scoreList.failRate,
  691. });
  692. let options = {
  693. color: ["#556dff", "#3A5AE5"],
  694. title: {
  695. text: "成绩分布图",
  696. left: "center",
  697. },
  698. grid: {
  699. left: 40,
  700. top: 60,
  701. right: 80,
  702. bottom: 40,
  703. containLabel: true,
  704. },
  705. xAxis: {
  706. type: "category",
  707. name: "成绩(分)",
  708. nameTextStyle: {
  709. color: "#363D59",
  710. },
  711. data: scoreRange.map((item) => item.name),
  712. axisLabel: {
  713. color: "#6F7482",
  714. interval: 0,
  715. fontSize: 12,
  716. margin: 12,
  717. },
  718. axisLine: {
  719. show: true,
  720. lineStyle: {
  721. color: "#EFF0F5",
  722. },
  723. },
  724. splitLine: {
  725. show: false,
  726. },
  727. axisTick: {
  728. show: false,
  729. },
  730. axisPointer: {
  731. type: "shadow",
  732. },
  733. },
  734. yAxis: {
  735. type: "value",
  736. name: "百分比(%)",
  737. nameTextStyle: {
  738. color: "#363D59",
  739. },
  740. axisLabel: {
  741. color: "#6F7482",
  742. },
  743. axisLine: {
  744. lineStyle: {
  745. color: "#EFF0F5",
  746. },
  747. },
  748. splitLine: {
  749. lineStyle: {
  750. color: "#EFF0F5",
  751. },
  752. },
  753. },
  754. series: [
  755. {
  756. name: "占比",
  757. type: "bar",
  758. barWidth: 60,
  759. data: scoreRange.map((item) => item.value),
  760. label: {
  761. show: true,
  762. formatter: "{c}%",
  763. position: "top",
  764. },
  765. },
  766. ],
  767. };
  768. return options;
  769. },
  770. getCourseTargetListChartOption() {
  771. let options = {
  772. color: ["#556dff", "#f59a23"],
  773. title: {
  774. text: "课程目标达成评价值",
  775. left: "center",
  776. },
  777. grid: {
  778. left: 40,
  779. top: 60,
  780. right: 80,
  781. bottom: 30,
  782. containLabel: true,
  783. },
  784. legend: {
  785. top: 0,
  786. data: ["达成值"],
  787. itemWidth: 12,
  788. itemHeight: 4,
  789. itemGap: 22,
  790. right: 40,
  791. },
  792. xAxis: {
  793. type: "category",
  794. name: "课程目标",
  795. nameTextStyle: {
  796. color: "#363D59",
  797. },
  798. data: this.courseTargetList.map((item) => item.targetName),
  799. axisLabel: {
  800. color: "#6F7482",
  801. interval: 0,
  802. fontSize: 12,
  803. margin: 12,
  804. },
  805. axisLine: {
  806. show: true,
  807. lineStyle: {
  808. color: "#EFF0F5",
  809. },
  810. },
  811. splitLine: {
  812. show: false,
  813. },
  814. axisTick: {
  815. show: false,
  816. },
  817. axisPointer: {
  818. type: "shadow",
  819. },
  820. },
  821. yAxis: {
  822. type: "value",
  823. name: "期望值",
  824. min: 0,
  825. max: 1,
  826. interval: 0.1,
  827. nameTextStyle: {
  828. color: "#363D59",
  829. },
  830. axisLabel: {
  831. color: "#6F7482",
  832. },
  833. axisLine: {
  834. lineStyle: {
  835. color: "#EFF0F5",
  836. },
  837. },
  838. splitLine: {
  839. lineStyle: {
  840. color: "#EFF0F5",
  841. },
  842. },
  843. },
  844. series: [
  845. {
  846. name: "达成值",
  847. type: "bar",
  848. barWidth: 60,
  849. data: this.courseTargetList.map((item) => item.evaluationValue),
  850. label: {
  851. show: true,
  852. position: "top",
  853. },
  854. markLine: {
  855. data: [
  856. {
  857. name: "期望值",
  858. yAxis: this.courseBasicInfo.courseDegree,
  859. symbol: "none",
  860. symbolSize: 0,
  861. lineStyle: {
  862. color: "#f59a23",
  863. width: 2,
  864. type: "solid",
  865. },
  866. },
  867. ],
  868. },
  869. },
  870. ],
  871. };
  872. return options;
  873. },
  874. getTargetChartOption(data, targetName) {
  875. if (!data.length) return null;
  876. const dataList = data.map((item, index) => {
  877. const val = Object.entries(item)[0];
  878. return {
  879. index: index + 1,
  880. name: val[0],
  881. value: val[1],
  882. };
  883. });
  884. let options = {
  885. color: ["#556dff", "#f59a23"],
  886. title: {
  887. text: `${targetName}个体达成情况`,
  888. left: "center",
  889. },
  890. grid: {
  891. left: 40,
  892. top: 60,
  893. right: 80,
  894. bottom: 30,
  895. containLabel: true,
  896. },
  897. legend: {
  898. top: 0,
  899. data: ["达成值"],
  900. itemWidth: 12,
  901. itemHeight: 4,
  902. itemGap: 22,
  903. right: 40,
  904. },
  905. xAxis: {
  906. type: "category",
  907. name: "学生代号",
  908. nameTextStyle: {
  909. color: "#363D59",
  910. },
  911. data: dataList.map((item) => item.index),
  912. axisLabel: {
  913. color: "#6F7482",
  914. fontSize: 12,
  915. },
  916. axisLine: {
  917. show: true,
  918. lineStyle: {
  919. color: "#EFF0F5",
  920. },
  921. },
  922. splitLine: {
  923. show: false,
  924. },
  925. axisTick: {
  926. show: false,
  927. },
  928. axisPointer: {
  929. type: "shadow",
  930. },
  931. },
  932. yAxis: {
  933. type: "value",
  934. name: "期望值",
  935. min: 0,
  936. max: 1,
  937. interval: 0.1,
  938. nameTextStyle: {
  939. color: "#363D59",
  940. },
  941. axisLabel: {
  942. color: "#6F7482",
  943. },
  944. axisLine: {
  945. lineStyle: {
  946. color: "#EFF0F5",
  947. },
  948. },
  949. splitLine: {
  950. lineStyle: {
  951. color: "#EFF0F5",
  952. },
  953. },
  954. },
  955. series: [
  956. {
  957. name: "达成值",
  958. type: "scatter",
  959. data: dataList.map((item) => item.value),
  960. markLine: {
  961. data: [
  962. {
  963. name: "期望值",
  964. yAxis: this.courseBasicInfo.courseDegree,
  965. symbol: "none",
  966. symbolSize: 0,
  967. lineStyle: {
  968. color: "#f59a23",
  969. width: 2,
  970. type: "solid",
  971. },
  972. },
  973. ],
  974. },
  975. },
  976. ],
  977. };
  978. return options;
  979. },
  980. parseCourseTargets(examStudentList) {
  981. if (!examStudentList || !examStudentList.length) return;
  982. const targetList = examStudentList[0].targetList;
  983. const targetVals = {};
  984. this.courseTargetList.forEach((target) => {
  985. targetVals[target.targetId] = target.evaluationValue;
  986. });
  987. let tColumnCounts = [];
  988. this.courseTargets = targetList.map((target) => {
  989. const ntarget = {
  990. targetId: target.targetId,
  991. targetName: target.targetName,
  992. finalWeight: target.finalScore?.targetWeight,
  993. usualWeight: 0,
  994. evaluationValue: targetVals[target.targetId],
  995. };
  996. ntarget.usualWeight = calcSum(
  997. (target.usualScore?.scoreList || []).map((item) => item.targetWeight)
  998. );
  999. ntarget.finalDimensions = target.finalScore ? ["期末成绩"] : [];
  1000. ntarget.usualWorks = (target.usualScore?.scoreList || []).map(
  1001. (item) => {
  1002. return {
  1003. evaluation: item.evaluation,
  1004. targetWeight: item.targetWeight,
  1005. };
  1006. }
  1007. );
  1008. tColumnCounts.push(
  1009. ntarget.finalDimensions.length + ntarget.usualWorks.length
  1010. );
  1011. return ntarget;
  1012. });
  1013. // 只展示有期末成绩或平时成绩的目标
  1014. this.courseTargets = this.courseTargets.filter(
  1015. (item) => item.finalDimensions.length || item.usualWorks.length
  1016. );
  1017. let tCount = 0;
  1018. this.targetColumnCounts = tColumnCounts.map((item) => {
  1019. tCount += item;
  1020. return tCount;
  1021. });
  1022. },
  1023. getTargetFirseKey(target) {
  1024. if (target.finalDimensions.length) {
  1025. return `${target.targetId}-final`;
  1026. } else {
  1027. return `${target.targetId}-usual-${target.usualWorks[0].evaluation}`;
  1028. }
  1029. },
  1030. parseStudentScoreTable(examStudentList) {
  1031. if (!this.courseTargets.length) return;
  1032. const lastIndex = examStudentList.length - 1;
  1033. const studentScoreTable = examStudentList.map((student, sindex) => {
  1034. const nitem = {
  1035. name: student.name,
  1036. studentCode: student.studentCode,
  1037. score: student.score,
  1038. targetRate: toPrecision(student.score / 100, 2),
  1039. };
  1040. const finalScoreKey =
  1041. lastIndex === sindex ? "matrixAvgScore" : "targetScoreSum";
  1042. const workScoreKey = lastIndex === sindex ? "matrixAvgScore" : "score";
  1043. student.targetList.forEach((target, index) => {
  1044. nitem[`${target.targetId}-final`] =
  1045. target.finalScore && target.finalScore[finalScoreKey];
  1046. (target.usualScore?.scoreList || []).forEach((work) => {
  1047. nitem[`${target.targetId}-usual-${work.evaluation}`] =
  1048. work[workScoreKey];
  1049. });
  1050. });
  1051. return nitem;
  1052. });
  1053. const targetData = {
  1054. name: "课程目标达成度",
  1055. score: "",
  1056. };
  1057. this.courseTargets.forEach((target) => {
  1058. targetData[this.getTargetFirseKey(target)] = target.evaluationValue;
  1059. });
  1060. studentScoreTable.push(targetData);
  1061. const fTarget = this.courseTargets[0];
  1062. const fKey = this.getTargetFirseKey(fTarget);
  1063. studentScoreTable.push({
  1064. name: "课程达成度",
  1065. [fKey]: this.courseTargetValue,
  1066. });
  1067. this.studentScoreTable = studentScoreTable;
  1068. },
  1069. studentScoreSpanMethod({ row, column, rowIndex, columnIndex }) {
  1070. const lineCount = this.studentScoreTable.length - 1;
  1071. const maxTargetColumnCount = this.targetColumnCounts.slice(-1)[0];
  1072. if (rowIndex === lineCount) {
  1073. if (columnIndex === 0) {
  1074. return [1, 2];
  1075. } else if (columnIndex === 2) {
  1076. return [1, maxTargetColumnCount + 2];
  1077. } else {
  1078. return [0, 0];
  1079. }
  1080. }
  1081. if (rowIndex === lineCount - 1) {
  1082. const colsMap = {};
  1083. const targetColumns = [0, ...this.targetColumnCounts.slice(0, -1)];
  1084. targetColumns.map((item, index) => {
  1085. colsMap[item] = this.targetColumnCounts[index] - item;
  1086. });
  1087. if (columnIndex === 0) {
  1088. return [1, 2];
  1089. } else {
  1090. if (targetColumns.includes(columnIndex - 2)) {
  1091. return [1, colsMap[columnIndex - 2]];
  1092. } else if (columnIndex < maxTargetColumnCount + 2) {
  1093. return [0, 0];
  1094. } else {
  1095. return [1, 1];
  1096. }
  1097. }
  1098. }
  1099. if (rowIndex === lineCount - 2) {
  1100. if (columnIndex === 0) {
  1101. return [1, 2];
  1102. } else if (columnIndex === 1) {
  1103. return [0, 0];
  1104. } else {
  1105. return [1, 1];
  1106. }
  1107. }
  1108. },
  1109. async toSave(autoSave = false) {
  1110. if (this.downloading) return;
  1111. this.downloading = true;
  1112. const courseSuggest = this.courseSuggest.map((item) =>
  1113. omit(item, ["chartOption"])
  1114. );
  1115. const res = await targetStatisticsSave({
  1116. id: this.curReportId,
  1117. cultureProgramId: this.course.cultureProgramId,
  1118. courseId: this.course.courseId,
  1119. examId: this.course.examId,
  1120. ...this.courseBasicInfo,
  1121. courseSuggest: JSON.stringify(courseSuggest),
  1122. }).catch(() => {});
  1123. this.downloading = false;
  1124. if (!res) return;
  1125. this.$message.success("保存成功!");
  1126. if (autoSave) {
  1127. this.openAutoSave();
  1128. } else {
  1129. this.initData();
  1130. }
  1131. },
  1132. async toExport() {
  1133. if (this.downloading) return;
  1134. this.downloading = true;
  1135. const res = await downloadByApi(() => {
  1136. const datas = {
  1137. id: this.curReportId,
  1138. cultureProgramId: this.course.cultureProgramId,
  1139. courseId: this.course.courseId,
  1140. };
  1141. return targetStatisticsReport(datas);
  1142. }).catch((e) => {
  1143. this.$message.error(e || "下载失败,请重新尝试!");
  1144. });
  1145. this.downloading = false;
  1146. if (!res) return;
  1147. this.$message.success("下载成功!");
  1148. },
  1149. async toExportNormalScore() {
  1150. if (this.downloading) return;
  1151. this.downloading = true;
  1152. const res = await downloadByApi(() => {
  1153. const datas = {
  1154. id: this.curReportId,
  1155. cultureProgramId: this.course.cultureProgramId,
  1156. courseId: this.course.courseId,
  1157. };
  1158. return targetStatisticsNormalScoreExport(datas);
  1159. }).catch((e) => {
  1160. this.$message.error(e || "下载失败,请重新尝试!");
  1161. });
  1162. this.downloading = false;
  1163. if (!res) return;
  1164. this.$message.success("下载成功!");
  1165. },
  1166. },
  1167. };
  1168. </script>
  1169. <style scoped>
  1170. .td-bg {
  1171. color: #8b8fa1;
  1172. font-weight: 500;
  1173. }
  1174. </style>