errorAnalysis.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. <template>
  2. <!-- 成绩查询 成绩单 -->
  3. <ReportModule
  4. ref="reportModuleRef"
  5. :titleList="['错题分析表']"
  6. :showDescribe="false"
  7. tableOrChart="table"
  8. printText="打印错题名单"
  9. :showTablePage="false"
  10. @ExportExcel="ExportExcel"
  11. @PrintPdf="PrintPdf"
  12. >
  13. <template #module_table_chart>
  14. <el-table
  15. :data="state.tableData"
  16. border
  17. stripe
  18. row-key="questionId"
  19. max-height="700"
  20. :span-method="SpanMethod"
  21. :row-class-name="ErrorTableRowClassName"
  22. :cell-class-name="ErrorTableCellClassName"
  23. v-loading="state.tableLoading"
  24. :element-loading-text="state.loadingText"
  25. :element-loading-spinner="loadingSvgIcon"
  26. element-loading-background="#ffffff"
  27. >
  28. <el-table-column
  29. prop="questionName"
  30. label="小题名称"
  31. width="80"
  32. align="center"
  33. show-overflow-tooltip
  34. />
  35. <el-table-column
  36. prop="questionType"
  37. label="题型"
  38. width="80"
  39. align="center"
  40. show-overflow-tooltip
  41. />
  42. <el-table-column
  43. prop="questionScore"
  44. label="满分"
  45. width="70"
  46. align="center"
  47. show-overflow-tooltip
  48. />
  49. <el-table-column
  50. prop="difficulty"
  51. label="难度"
  52. width="70"
  53. align="center"
  54. show-overflow-tooltip
  55. />
  56. <el-table-column
  57. prop="discrimination"
  58. label="区分度"
  59. width="80"
  60. align="center"
  61. show-overflow-tooltip
  62. />
  63. <el-table-column
  64. prop="averageScore"
  65. label="平均分"
  66. width="70"
  67. align="center"
  68. show-overflow-tooltip
  69. />
  70. <el-table-column
  71. prop="scoreRate"
  72. label="得分率"
  73. width="80"
  74. align="center"
  75. show-overflow-tooltip
  76. />
  77. <el-table-column
  78. prop="studentCount"
  79. label="答题人数"
  80. width="80"
  81. align="center"
  82. show-overflow-tooltip
  83. />
  84. <el-table-column
  85. prop="errorCount"
  86. label="错题人数"
  87. width="80"
  88. align="center"
  89. show-overflow-tooltip
  90. />
  91. <el-table-column
  92. prop="answerValue"
  93. label="正确答案"
  94. width="80"
  95. align="center"
  96. show-overflow-tooltip
  97. >
  98. <template #default="scope">
  99. {{ scope.row.answerValue ?? "-" }}
  100. </template>
  101. </el-table-column>
  102. <el-table-column prop="errorDetail" label="错题详情" align="center">
  103. <el-table-column
  104. :align="key == 'registrationCodeList' ? 'left' : 'center'"
  105. v-for="(key, index) in [
  106. 'name',
  107. 'rate',
  108. 'studentNum',
  109. 'registrationCodeList',
  110. ]"
  111. :key="key"
  112. label=""
  113. :prop="key"
  114. :min-width="[60, 70, 60, 384][index]"
  115. :show-overflow-tooltip="
  116. key == 'registrationCodeList' ? false : true
  117. "
  118. >
  119. <template #default="{ row: org }">
  120. <span v-if="key == 'registrationCodeList'">{{ org[key] }}</span>
  121. <span v-else-if="key == 'rate'">{{
  122. org[key] == "-" ? "-" : `${org[key]}%`
  123. }}</span>
  124. <span v-else>{{ org[key] }}</span>
  125. </template>
  126. </el-table-column>
  127. </el-table-column>
  128. </el-table>
  129. </template>
  130. </ReportModule>
  131. <!-- <ErrorsPdf
  132. ref="errPdfReport"
  133. :tableData="state.tableData"
  134. :className="analysisStore.filterObject.classGroupName"
  135. :subjectName="analysisStore.filterObject.subjectName || ''"
  136. :examName="getExamName"
  137. @PdfLoadEnd="PdfLoadEnd"
  138. /> -->
  139. <ErrorsTablePdf
  140. ref="errPdfReport"
  141. :tableData="state.pdfTableData"
  142. :className="analysisStore.filterObject.classGroupName"
  143. :subjectName="analysisStore.filterObject.subjectName || ''"
  144. :examName="getExamName"
  145. @PdfLoadEnd="PdfLoadEnd"
  146. />
  147. <div class="page_dialog padding_0">
  148. <el-dialog title="选择班级" center v-model="showClassPop" width="480px">
  149. <div class="dialog_center padding_20 page_search">
  150. <div class="select_check_content score_class_popup">
  151. <div class="item_option">
  152. <el-checkbox
  153. :indeterminate="isIndeterminate"
  154. v-model="checkAll"
  155. @change="HandleCheckAllChange"
  156. >全部</el-checkbox
  157. >
  158. </div>
  159. <el-checkbox-group
  160. v-model="selectClassList"
  161. @change="HandleCheckedClassChange"
  162. >
  163. <el-checkbox
  164. :value="item.classIdCode"
  165. v-for="item in classList"
  166. :title="item.className"
  167. >{{ TruncateMiddleText(item.className, 5, 3, 1) }}</el-checkbox
  168. >
  169. </el-checkbox-group>
  170. </div>
  171. </div>
  172. <div slot="footer" class="el-dialog__footer dialog-footer">
  173. <el-button size="small" @click="showClassPop = false">取 消</el-button>
  174. <el-button
  175. size="small"
  176. type="primary"
  177. @click="ConfirmEnter()"
  178. :loading="buttonLoading"
  179. >确 定</el-button
  180. >
  181. </div>
  182. </el-dialog>
  183. </div>
  184. </template>
  185. <script lang="ts" setup>
  186. import ReportModule from "@/components/ReportModule.vue";
  187. import { useAnalysisStore } from "@/store/analysis";
  188. // import ErrorsPdf from "@/components/ErrorsPdf.vue";
  189. import ErrorsTablePdf from "@/components/ErrorsTablePdf.vue";
  190. import { onMounted, reactive, computed, ref, watch, nextTick } from "vue";
  191. import { errorQuestionAnalysis, exportErrorQuestion } from "@/api/analysis";
  192. import { loadingSvgIcon, truncateMiddleText } from "@/utils/common";
  193. import {
  194. downloadExcel,
  195. GetExcelFileName,
  196. errorQuestionDataStaticHeaderData,
  197. errorQuestionDataChildHeaderData,
  198. } from "@/utils/exportExcel";
  199. import { ElMessage } from "element-plus";
  200. // 定义表格行数据类型
  201. interface TableDataItem {
  202. questionId: string | number;
  203. questionName?: string;
  204. questionType?: string;
  205. questionScore?: number | string;
  206. difficulty?: number | string;
  207. discrimination?: number | string;
  208. averageScore?: number | string;
  209. scoreRate?: number | string;
  210. studentCount?: number;
  211. errorCount?: number;
  212. answerValue?: string;
  213. answerList?: any[];
  214. questionStatsList?: any[];
  215. rowspan?: number;
  216. colspan?: number;
  217. rowKey?: number;
  218. rowspanIndex?: number;
  219. [key: string]: any;
  220. }
  221. const state = reactive<{
  222. allTableData: any[];
  223. tableData: TableDataItem[];
  224. pdfTableData: TableDataItem[];
  225. tableLoading: boolean;
  226. loadingText: string;
  227. }>({
  228. allTableData: [],
  229. tableData: [],
  230. pdfTableData: [],
  231. tableLoading: true,
  232. loadingText: "加载中,请稍后...",
  233. });
  234. const analysisStore = useAnalysisStore();
  235. const reportModuleRef = ref<any>(null);
  236. const errPdfReport = ref<any>(null);
  237. const getExamName = computed(() => {
  238. return analysisStore.analysisExamInfo.examName || "";
  239. });
  240. const buttonLoading = ref<boolean>(false); //按钮loading
  241. const showClassPop = ref<boolean>(false); //选择班级弹框
  242. const selectClassList = ref<string[]>([]); //选中的班级
  243. const isIndeterminate = ref<boolean>(false); //是否班级全选半选
  244. const checkAll = ref<boolean>(false); //班级全选
  245. const classList = computed(() => {
  246. //班级列表
  247. const classNameList =
  248. analysisStore.filtersData.find((item: any) => item.type === "className")
  249. ?.list || [];
  250. return classNameList.filter((item: any) => item.classLevel == 2);
  251. });
  252. //班级全选
  253. const HandleCheckAllChange = () => {
  254. if (checkAll.value) {
  255. selectClassList.value = classList.value.map((item: any) => item.classIdCode);
  256. } else {
  257. selectClassList.value = [];
  258. }
  259. };
  260. //班级选择改变事件
  261. const HandleCheckedClassChange = () => {
  262. if (selectClassList.value.length > 0) {
  263. checkAll.value = true;
  264. if (selectClassList.value.length == classList.value.length) {
  265. isIndeterminate.value = false;
  266. } else {
  267. isIndeterminate.value = true;
  268. }
  269. } else {
  270. checkAll.value = false;
  271. isIndeterminate.value = false;
  272. }
  273. };
  274. const TruncateMiddleText = (
  275. text: string,
  276. num: number,
  277. startStr: number,
  278. endStr: number,
  279. ) => {
  280. return truncateMiddleText(text, num, startStr, endStr);
  281. };
  282. // 错题分析
  283. const GetErrorQuestionAnalysis = async () => {
  284. state.tableLoading = true;
  285. const res: any = await errorQuestionAnalysis({
  286. ...(analysisStore.filterObject as any),
  287. });
  288. if (res.code === 200) {
  289. const allTableData = res.data || [];
  290. state.allTableData = allTableData;
  291. state.tableData = [];
  292. state.pdfTableData = [];
  293. const tableData = allTableData.filter((item: any) => {
  294. if (analysisStore.filterObject.classLevel == 0) {
  295. return item.classIdCode.indexOf("school") > -1;
  296. } else {
  297. return item.className == analysisStore.filterObject.classGroupName;
  298. }
  299. });
  300. if (tableData?.length) {
  301. tableData[0].questionStatsList.forEach((item: any, key: number) => {
  302. const answerListLen = item?.answerList?.length || 0;
  303. if (answerListLen == 0) {
  304. item.answerList = [
  305. {
  306. name: "-",
  307. rate: "-",
  308. registrationCodeList: "-",
  309. score: "-",
  310. studentNum: "-",
  311. },
  312. ];
  313. }
  314. const rowspan = item.answerList.length;
  315. item.answerList.forEach((answer: any, index: number) => {
  316. if (index == 0) {
  317. state.tableData.push({
  318. ...item,
  319. scoreRate: `${item.scoreRate}%`,
  320. ...answer,
  321. rowspan: rowspan, // 合并行
  322. colspan: 1,
  323. rowKey: index, // 行索引
  324. rowspanIndex: key, // 合并行之后的索引
  325. });
  326. state.pdfTableData.push({
  327. ...item,
  328. scoreRate: `${item.scoreRate}%`,
  329. ...answer,
  330. rowspan: rowspan, // 合并行
  331. colspan: 1,
  332. rowKey: index, // 行索引
  333. rowspanIndex: key, // 合并行之后的索引
  334. });
  335. } else {
  336. state.tableData.push({
  337. ...item,
  338. questionId: `${item.questionId}_${index}`,
  339. scoreRate: `${item.scoreRate}%`,
  340. ...answer,
  341. rowKey: index, // 行索引
  342. rowspanIndex: key, // 合并行之后的索引
  343. });
  344. state.pdfTableData.push({
  345. ...item,
  346. questionId: `${item.questionId}_${index}`,
  347. scoreRate: `${item.scoreRate}%`,
  348. ...answer,
  349. rowKey: index, // 行索引
  350. rowspanIndex: key, // 合并行之后的索引
  351. });
  352. }
  353. });
  354. });
  355. }
  356. } else {
  357. state.allTableData = [];
  358. state.tableData = [];
  359. state.pdfTableData = [];
  360. }
  361. state.tableLoading = false;
  362. };
  363. const GetErrorQuestionAnalysisPdf = (className: string) => {
  364. state.pdfTableData = [];
  365. const tableData = state.allTableData.filter((item: any) => {
  366. return className.indexOf(item.className) != -1;
  367. });
  368. if (tableData?.length) {
  369. tableData[0].questionStatsList.forEach((item: any, key: number) => {
  370. const answerListLen = item?.answerList?.length || 0;
  371. if (answerListLen == 0) {
  372. item.answerList = [
  373. {
  374. name: "-",
  375. rate: "-",
  376. registrationCodeList: "-",
  377. score: "-",
  378. studentNum: "-",
  379. },
  380. ];
  381. }
  382. const rowspan = item.answerList.length;
  383. item.answerList.forEach((answer: any, index: number) => {
  384. if (index == 0) {
  385. state.pdfTableData.push({
  386. ...item,
  387. scoreRate: `${item.scoreRate}%`,
  388. ...answer,
  389. rowspan: rowspan, // 合并行
  390. colspan: 1,
  391. rowKey: index, // 行索引
  392. rowspanIndex: key, // 合并行之后的索引
  393. });
  394. } else {
  395. state.pdfTableData.push({
  396. ...item,
  397. questionId: `${item.questionId}_${index}`,
  398. scoreRate: `${item.scoreRate}%`,
  399. ...answer,
  400. rowKey: index, // 行索引
  401. rowspanIndex: key, // 合并行之后的索引
  402. });
  403. }
  404. });
  405. });
  406. }
  407. };
  408. // 错题分析 合并单元格
  409. const SpanMethod = ({ row, column, rowIndex, columnIndex }: any) => {
  410. const props = [
  411. "errorDetail",
  412. "name",
  413. "rate",
  414. "studentNum",
  415. "registrationCodeList",
  416. ];
  417. if (props.indexOf(column.property) == -1) {
  418. if (row.rowspan) {
  419. return {
  420. rowspan: row.rowspan,
  421. colspan: row.colspan,
  422. };
  423. } else {
  424. return {
  425. rowspan: 0,
  426. colspan: 0,
  427. };
  428. }
  429. }
  430. };
  431. // 定义行样式
  432. const ErrorTableRowClassName = ({ row, rowIndex }: any) => {
  433. if (row.rowspanIndex % 2 === 0) {
  434. return "row_color_FFFFFF";
  435. } else {
  436. return "row_color_FAFAFA";
  437. }
  438. };
  439. // 设置单元格样式
  440. const ErrorTableCellClassName = ({
  441. row,
  442. column,
  443. rowIndex,
  444. columnIndex,
  445. }: any) => {
  446. if (column.property == "registrationCodeList") {
  447. return "white_space_normal";
  448. } else {
  449. return "";
  450. }
  451. };
  452. // 导出Excel
  453. const ExportExcel = () => {
  454. // 1. 设置加载状态
  455. reportModuleRef.value?.SetExportLoading?.(true);
  456. // 2. 参数
  457. const examName = getExamName.value;
  458. let params: any = {
  459. fileName: `${GetExcelFileName(examName, analysisStore.filterObject as any, "错题分析表")}`,
  460. examName: examName, // 考试名称
  461. sheetName: "错题分析表", // sheet页名称
  462. };
  463. const staticHeader: any[] = errorQuestionDataStaticHeaderData();
  464. const childHeader = ["name", "rate", "studentNum", "registrationCodeList"];
  465. const childHeaderData: any[] = errorQuestionDataChildHeaderData(childHeader);
  466. const staticHeaderData = [...staticHeader, ...childHeaderData];
  467. params.staticHeaderData = staticHeaderData;
  468. params.childHeaderData = [];
  469. params.dynamicsHeaderData = [];
  470. let dataList: any[] = [];
  471. state.pdfTableData.forEach((item) => {
  472. const rowData: any[] = [];
  473. staticHeaderData.forEach((header: any) => {
  474. if (header.prop == "rate") {
  475. const rate = `${item[header.prop]}${item[header.prop] == "-" ? "" : "%"}`;
  476. rowData.push(rate);
  477. } else {
  478. rowData.push(item?.[header.prop] ?? "-");
  479. }
  480. });
  481. dataList.push(rowData);
  482. });
  483. params.dataList = dataList;
  484. params.mergeColumn = staticHeader.length - 1; // 合并的列
  485. // 3. 调用通用下载方法,并在完成后重置加载状态
  486. downloadExcel(exportErrorQuestion, params).finally(() => {
  487. reportModuleRef.value?.SetExportLoading?.(false);
  488. });
  489. };
  490. // 导出PDF
  491. const PrintPdf = () => {
  492. // if (analysisStore?.filterObject?.classLevel === 0) {
  493. // //年级
  494. // selectClassList.value = [];
  495. // showClassPop.value = true; //显示弹框
  496. // } else {
  497. reportModuleRef.value?.SetPrintLoading?.(true);
  498. errPdfReport.value?.DownloadPdf?.();
  499. // }
  500. };
  501. // 导出PDF 加载完成
  502. const PdfLoadEnd = () => {
  503. reportModuleRef.value?.SetPrintLoading?.(false);
  504. };
  505. //确定选择班级
  506. const ConfirmEnter = async () => {
  507. if (selectClassList.value.length == 0) {
  508. ElMessage.warning("请至少选择一个班级!");
  509. return;
  510. }
  511. buttonLoading.value = true;
  512. reportModuleRef.value?.SetPrintLoading?.(true);
  513. const classInfo = classList.value.filter(
  514. (item) => selectClassList.value.indexOf(item.classIdCode) != -1
  515. ).sort((a, b) => a.classCode - b.classCode);
  516. const className = classInfo.map((item) => item.className);
  517. GetErrorQuestionAnalysisPdf(className);
  518. console.log(classInfo,className,state.pdfTableData,1212122);
  519. // 关键:等子组件按新数据开始分页后再调导出。
  520. // DownloadPdf 内部会 await 分页校正循环(最多 12 轮)收敛后才截图,
  521. // 否则会截到分页重建中的死节点(scrollWidth=0 → 空图 → jsPDF 报 UNKNOWN)
  522. await nextTick();
  523. // setTimeout(() => {
  524. showClassPop.value = false; //先关闭弹框,不等PDF生成完成
  525. // }, 500);
  526. errPdfReport.value
  527. ?.DownloadPdf?.() //加载并导出PDF
  528. .then(() => {
  529. // ElMessage.success("成绩单已在新页面打开,请去下载或打印");
  530. })
  531. .catch((err) => {
  532. console.error("导出成绩单PDF失败:", err);
  533. // ElMessage.error("导出PDF失败,请重试");
  534. })
  535. .finally(() => {
  536. buttonLoading.value = false;
  537. reportModuleRef.value?.SetPrintLoading?.(false);
  538. });
  539. };
  540. // 初始化
  541. const pageInit = () => {
  542. GetErrorQuestionAnalysis();
  543. };
  544. // 监听筛选条件
  545. watch(
  546. () => analysisStore.filterObject,
  547. async () => {
  548. pageInit();
  549. },
  550. { deep: true },
  551. );
  552. onMounted(() => {
  553. pageInit();
  554. });
  555. </script>
  556. <style lang="scss" scoped>
  557. :deep(.el-table) {
  558. .el-table__header-wrapper {
  559. .el-table__header {
  560. .is-group {
  561. tr {
  562. &:nth-child(2) {
  563. display: none;
  564. }
  565. }
  566. }
  567. }
  568. }
  569. .el-table__cell {
  570. &.white_space_normal {
  571. .cell {
  572. white-space: normal;
  573. padding: 10px 0 10px 10px !important;
  574. }
  575. }
  576. }
  577. &.el-table--striped {
  578. .el-table__body {
  579. tr {
  580. &.el-table__row--striped {
  581. &.row_color_FFFFFF {
  582. td.el-table__cell {
  583. background: #ffffff;
  584. }
  585. }
  586. }
  587. }
  588. }
  589. }
  590. &.el-table--striped
  591. .el-table__body
  592. tr.el-table__row.row_color_FAFAFA
  593. td.el-table__cell {
  594. background: #fafafa;
  595. }
  596. &.el-table--striped
  597. .el-table__body
  598. tr.el-table__row--striped.row_color_FAFAFA
  599. td.el-table__cell {
  600. background: #fafafa;
  601. }
  602. &.el-table--enable-row-hover .el-table__body tr.row_color_FFFFFF > td {
  603. background-color: transparent !important;
  604. }
  605. }
  606. </style>