score.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. <template>
  2. <!-- 成绩查询 - 成绩单 -->
  3. <ReportModule
  4. ref="reportModuleRef"
  5. :showTitle="false"
  6. :showDescribe="false"
  7. :showPrintBtn="true"
  8. tableOrChart="table"
  9. :currentPage="state.pageInfo.pageNum"
  10. :pageSize="state.pageInfo.pageSize"
  11. :pageSizes="[50, 100]"
  12. :total="state.pageInfo.total"
  13. @ChangePageSize="ChangePageSize"
  14. @ChangeCurrentPage="ChangeCurrentPage"
  15. @ExportExcel="ExportExcel"
  16. @PrintPdf="PrintPdf"
  17. >
  18. <template #title_left>
  19. <el-input
  20. v-model="state.keyWord"
  21. clearable
  22. style="width: 200px"
  23. placeholder="请输入学号或姓名"
  24. class="input_with"
  25. @input="HandleSearch"
  26. >
  27. <template #append>
  28. <el-button :icon="Search" @click="HandleSearch" />
  29. </template>
  30. </el-input>
  31. <span class="count_item"
  32. >应考:{{ state.tableCount.examStudentCount }}人</span
  33. >
  34. <span class="count_item">实考:{{ state.tableCount.normalCount }}人</span>
  35. <span class="count_item orange"
  36. >缺考:{{ state.tableCount.missExamCount }}人</span
  37. >
  38. </template>
  39. <template #title_right>
  40. <el-checkbox-group v-model="state.checkList" class="checkbox_group">
  41. <el-checkbox
  42. v-if="state.groupDataTitleData.length"
  43. label="显示分组"
  44. value="group"
  45. />
  46. <el-checkbox
  47. v-if="state.scoreDataTitleData.length"
  48. label="显示小题"
  49. value="question"
  50. />
  51. </el-checkbox-group>
  52. </template>
  53. <template #module_table_chart>
  54. <el-table
  55. :data="state.tableData"
  56. border
  57. stripe
  58. ref="tableRef"
  59. :height="state.tableHeight"
  60. v-loading="state.tableLoading"
  61. :element-loading-text="state.loadingText"
  62. :element-loading-spinner="loadingSvgIcon"
  63. element-loading-background="#ffffff"
  64. >
  65. <el-table-column
  66. type="index"
  67. :index="GetIndexNumber"
  68. align="center"
  69. width="70"
  70. label="序号"
  71. :fixed="state.checkList.length ? 'left' : null"
  72. ></el-table-column>
  73. <el-table-column
  74. v-for="item in state.staticHeaderData"
  75. :key="item.prop"
  76. :prop="item.prop"
  77. :label="item.label"
  78. min-width="80"
  79. :fixed="state.checkList.length ? 'left' : null"
  80. >
  81. <template #default="scope">
  82. <span
  83. v-if="
  84. scope.row.score != '缺考' &&
  85. scope.row.score != '违纪' &&
  86. item.prop === 'studentUserName'
  87. "
  88. @click="OpenStudentPaper(scope.row)"
  89. style="cursor: pointer; color: #2e64fa"
  90. >
  91. {{ scope.row[item.prop] }}
  92. </span>
  93. <template v-else>
  94. {{ scope.row?.[item.prop] || "-" }}
  95. </template>
  96. </template>
  97. </el-table-column>
  98. <template v-if="state.checkList.includes('group')">
  99. <el-table-column
  100. v-for="(item, index) in state.groupDataTitleData"
  101. :key="item.prop"
  102. :prop="item.prop"
  103. :label="item.label"
  104. min-width="100"
  105. >
  106. <template #header>
  107. <template v-if="item.label.length > 6">
  108. <el-tooltip effect="dark" :content="item.label" placement="top">
  109. <span>{{ item.label }}</span>
  110. </el-tooltip>
  111. </template>
  112. <template v-else>
  113. {{ item.label }}
  114. </template>
  115. </template>
  116. <template #default="scope">{{
  117. scope.row?.dynamicsData?.groupData?.[index] || "-"
  118. }}</template>
  119. </el-table-column>
  120. </template>
  121. <template v-if="state.checkList.includes('question')">
  122. <el-table-column
  123. v-for="(item, index) in state.scoreDataTitleData"
  124. :key="item.prop"
  125. :prop="item.prop"
  126. :label="item.label"
  127. min-width="100"
  128. >
  129. <template #default="scope">{{
  130. scope.row?.dynamicsData?.scoreData?.[index] || "-"
  131. }}</template>
  132. </el-table-column>
  133. </template>
  134. </el-table>
  135. </template>
  136. </ReportModule>
  137. <StudentPaper
  138. :modelValue="state.showStudentPaperDialog"
  139. :paperInfo="state.paperInfo"
  140. :pageTitle="state.paperTitle"
  141. @updateModelValue="UpdateModelValue"
  142. ></StudentPaper>
  143. <ScorePdf ref="scorePdfRef"></ScorePdf>
  144. <div class="page_dialog padding_0">
  145. <el-dialog
  146. title="选择班级"
  147. center
  148. v-model="showClassPop"
  149. width="480px"
  150. >
  151. <div class="dialog_center padding_20 page_search">
  152. <div class="select_check_content score_class_popup">
  153. <el-checkbox
  154. :indeterminate="isIndeterminate"
  155. v-model="checkAll"
  156. @change="HandleCheckAllChange"
  157. >全部</el-checkbox
  158. >
  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 StudentPaper from "@/components/StudentPaper.vue"; //学生答题卡组件
  188. import ScorePdf from "@/components/ScorePdf.vue";
  189. import {
  190. studentTranscriptTitle,
  191. queryJointStudentStatistics,
  192. studentTranscript,
  193. studentTranscriptExcel,
  194. } from "@/api/analysis";
  195. import { useAnalysisStore } from "@/store/analysis";
  196. import { downloadExcel } from "@/utils/exportExcel";
  197. import { Search } from "@element-plus/icons-vue";
  198. import { loadingSvgIcon, truncateMiddleText } from "@/utils/common";
  199. import { ElMessage } from "element-plus";
  200. import {
  201. nextTick,
  202. ref,
  203. onMounted,
  204. onUnmounted,
  205. reactive,
  206. computed,
  207. watch,
  208. } from "vue";
  209. import { throttle } from "lodash";
  210. interface TableColumn {
  211. prop: string;
  212. label: string;
  213. display?: boolean;
  214. [key: string]: any;
  215. }
  216. interface TableCount {
  217. examStudentCount: number | string;
  218. normalCount: number | string;
  219. missExamCount: number | string;
  220. }
  221. interface PageInfo {
  222. pageSize: number;
  223. pageNum: number;
  224. total: number;
  225. }
  226. interface State {
  227. keyWord: string;
  228. checkList: string[];
  229. tableHeight: Number;
  230. tableData: any[];
  231. staticHeaderData: TableColumn[];
  232. groupDataTitleData: TableColumn[];
  233. scoreDataTitleData: TableColumn[];
  234. tableCount: TableCount;
  235. pageInfo: PageInfo;
  236. tableLoading: Boolean;
  237. loadingText: String;
  238. paperInfo: {
  239. examPaperId: string; //考试科目id
  240. platformNumber: string; //学籍号平台号
  241. questionId: string; //题目id
  242. };
  243. paperTitle: String; //学生试卷标题
  244. showStudentPaperDialog: Boolean; //是否显示学生答题卡弹窗
  245. }
  246. const analysisStore = useAnalysisStore();
  247. const getExamName = computed(() => {
  248. return analysisStore.analysisExamInfo.examName || "";
  249. });
  250. const state = reactive<State>({
  251. keyWord: "",
  252. checkList: [],
  253. tableHeight: 1000, //表格高度
  254. tableData: [],
  255. staticHeaderData: [],
  256. groupDataTitleData: [],
  257. scoreDataTitleData: [],
  258. tableCount: {
  259. examStudentCount: 0,
  260. normalCount: 0,
  261. missExamCount: 0,
  262. },
  263. pageInfo: {
  264. pageSize: 50,
  265. pageNum: 1,
  266. total: 0,
  267. },
  268. tableLoading: true,
  269. loadingText: "加载中,请稍后...",
  270. paperInfo: {
  271. examPaperId: "", //考试科目id
  272. platformNumber: "", //学籍号平台号
  273. questionId: "", //题目id
  274. }, //学生试卷信息
  275. paperTitle: "", //学生试卷标题
  276. showStudentPaperDialog: false, //是否显示学生答题卡弹窗
  277. });
  278. const tableRef = ref<any>(null);
  279. const reportModuleRef = ref<any>(null);
  280. const scorePdfRef = ref<any>(null);//Pdf组件
  281. const showClassPop = ref<boolean>(false); //选择班级弹框
  282. const isIndeterminate = ref<boolean>(false); //是否班级全选半选
  283. const checkAll = ref<boolean>(false); //班级全选
  284. const selectClassList = ref<string[]>([]); //选中的班级
  285. const buttonLoading = ref<boolean>(false); //按钮loading
  286. const classList = computed(() => {//班级列表
  287. const classNameList = analysisStore.filtersData.find((item: any) => item.type === "className")?.list || [];
  288. return classNameList.filter((item: any) => item.classLevel == 2)
  289. });
  290. /** 获取表头 */
  291. const GetStudentTranscriptTitle = async () => {
  292. if (!analysisStore.filterObject) return;
  293. try {
  294. const res = await studentTranscriptTitle({
  295. ...analysisStore.filterObject,
  296. topicControl: 0,
  297. });
  298. if (res.code === 200) {
  299. const staticHeaderData = res.data?.title?.staticHeaderData || [];
  300. state.staticHeaderData = staticHeaderData.filter(
  301. (item: { display: any }) => item.display,
  302. );
  303. state.groupDataTitleData =
  304. res.data?.title?.dynamicsHeaderData?.groupDataTitle || [];
  305. state.scoreDataTitleData =
  306. res.data?.title?.dynamicsHeaderData?.scoreDataTitle || [];
  307. let checkList = [];
  308. if (state.groupDataTitleData.length) {
  309. checkList.push("group");
  310. }
  311. if (state.scoreDataTitleData.length) {
  312. checkList.push("question");
  313. }
  314. state.checkList = checkList.length ? [checkList[0]] : [];
  315. }
  316. } catch (err) {
  317. console.error("获取表头失败:", err);
  318. }
  319. };
  320. /** 获取顶部统计 */
  321. const GetTableCount = async () => {
  322. if (!analysisStore.filterObject) return;
  323. try {
  324. const res = await queryJointStudentStatistics({
  325. ...analysisStore.filterObject,
  326. topicControl: 0,
  327. queryStr: state.keyWord,
  328. });
  329. if (res.code === 200) {
  330. state.tableCount = res.data || {};
  331. }
  332. } catch (err) {
  333. console.error("获取统计数据失败:", err);
  334. }
  335. };
  336. /** 获取表格数据 */
  337. const GetTableData = async (isInit: boolean = false) => {
  338. if (!analysisStore.filterObject) return;
  339. try {
  340. state.tableLoading = true;
  341. const res = await studentTranscript({
  342. ...analysisStore.filterObject,
  343. topicControl: 0,
  344. queryStr: state.keyWord,
  345. pageParam: {
  346. pageNum: state.pageInfo.pageNum,
  347. pageSize: state.pageInfo.pageSize,
  348. },
  349. });
  350. if (res.code === 200) {
  351. state.tableData = res.data?.listData || [];
  352. state.pageInfo.total = Number(res.data?.total || 0);
  353. }
  354. } catch (err) {
  355. console.error("获取表格数据失败:", err);
  356. } finally {
  357. state.tableLoading = false;
  358. // 数据加载完成后重置滚动
  359. nextTick(() => {
  360. TableScrollTop();
  361. if (isInit) {
  362. SetTableHeight();
  363. }
  364. });
  365. }
  366. };
  367. //获取序号
  368. const GetIndexNumber = (index: number) => {
  369. let indexCount =
  370. (state.pageInfo.pageNum - 1) * state.pageInfo.pageSize + index + 1;
  371. return indexCount;
  372. };
  373. // 当前页码事件
  374. const ChangeCurrentPage = (val: number) => {
  375. state.pageInfo.pageNum = val;
  376. GetTableData();
  377. };
  378. // 每页显示个数事件
  379. const ChangePageSize = (val: number) => {
  380. state.pageInfo.pageSize = val;
  381. state.pageInfo.pageNum = 1;
  382. GetTableData();
  383. };
  384. // 导出Excel
  385. const ExportExcel = () => {
  386. // 1. 设置加载状态
  387. reportModuleRef.value?.SetExportLoading?.(true);
  388. // 2. 参数
  389. const params = {
  390. ...analysisStore.filterObject,
  391. topicControl: 0,
  392. topicControlName: "成绩单",
  393. queryStr: state.keyWord,
  394. pageParam: {
  395. pageNum: state.pageInfo.pageNum,
  396. pageSize: state.pageInfo.pageSize,
  397. },
  398. staticHeaderData: state.staticHeaderData,
  399. dynamicsHeaderDataMap: {},
  400. };
  401. if (state.checkList.includes("group")) {
  402. params.dynamicsHeaderDataMap.groupDataTitle = state.groupDataTitleData;
  403. }
  404. if (state.checkList.includes("question")) {
  405. params.dynamicsHeaderDataMap.scoreDataTitle = state.scoreDataTitleData;
  406. }
  407. // 3. 调用通用下载方法,并在完成后重置加载状态
  408. downloadExcel(studentTranscriptExcel, params).finally(() => {
  409. reportModuleRef.value?.SetExportLoading?.(false);
  410. });
  411. };
  412. // 打印PDF
  413. const PrintPdf = () => {
  414. selectClassList.value = [];//清空选择的数据
  415. isIndeterminate.value = false;//默认不半选
  416. buttonLoading.value = false;
  417. if(analysisStore?.filterObject?.classLevel===0){//年级
  418. selectClassList.value = classList.value.map((item: any) => item.classIdCode);
  419. checkAll.value = true;//默认全选
  420. showClassPop.value = true;//显示弹框
  421. }else{
  422. checkAll.value = false;//默认不全选
  423. showClassPop.value = false;//不显示弹框
  424. reportModuleRef.value?.SetPrintLoading?.(true);
  425. const classIdCode = analysisStore?.filterObject?.classIdCode;
  426. selectClassList.value = classIdCode? [classIdCode] : [];
  427. scorePdfRef.value?.LoadAndExportPdf(state.keyWord, selectClassList.value)
  428. .finally(() => {
  429. reportModuleRef.value?.SetPrintLoading?.(false);
  430. });//加载并导出PDF
  431. }
  432. };
  433. //班级全选
  434. const HandleCheckAllChange = () => {
  435. if (checkAll.value) {
  436. selectClassList.value = classList.value.map((item: any) => item.classIdCode);
  437. } else {
  438. selectClassList.value = [];
  439. }
  440. };
  441. //班级选择改变事件
  442. const HandleCheckedClassChange = () => {
  443. if (selectClassList.value.length > 0) {
  444. checkAll.value = true;
  445. if (selectClassList.value.length == classList.value.length) {
  446. isIndeterminate.value = false;
  447. } else {
  448. isIndeterminate.value = true;
  449. }
  450. } else {
  451. checkAll.value = false;
  452. isIndeterminate.value = false;
  453. }
  454. };
  455. const TruncateMiddleText = (
  456. text: string,
  457. num: number,
  458. startStr: number,
  459. endStr: number,
  460. ) => {
  461. return truncateMiddleText(text, num, startStr, endStr);
  462. };
  463. //确定选择班级
  464. const ConfirmEnter = () => {
  465. if (selectClassList.value.length == 0) {
  466. ElMessage.warning("请至少选择一个班级!");
  467. return;
  468. }
  469. buttonLoading.value = true;
  470. setTimeout(() => {
  471. showClassPop.value = false;//先关闭弹框,不等PDF生成完成
  472. }, 500);
  473. scorePdfRef.value?.LoadAndExportPdf(state.keyWord, selectClassList.value)//加载并导出PDF
  474. .then(() => {
  475. ElMessage.success("成绩单已在新页面打开,请去下载或打印");
  476. })
  477. .catch((err) => {
  478. console.error("导出成绩单PDF失败:", err);
  479. ElMessage.error("导出PDF失败,请重试");
  480. })
  481. .finally(() => {
  482. buttonLoading.value = false;
  483. });
  484. };
  485. //打开答题卡弹窗
  486. const OpenStudentPaper = (row, prop) => {
  487. state.paperInfo = {
  488. examPaperId: analysisStore.filterObject.subjectId, //考试科目id
  489. platformNumber: row.studentRegistrationCode, //学籍号平台号
  490. questionId: "", //题目id
  491. };
  492. state.paperTitle = `${getExamName.value}-${analysisStore.filterObject.subjectName}-${row.className}-${row.studentUserName}`; //学生姓名
  493. state.showStudentPaperDialog = true;
  494. };
  495. //更新弹窗状态
  496. const UpdateModelValue = (val: boolean) => {
  497. state.showStudentPaperDialog = val;
  498. };
  499. // 搜索事件
  500. const HandleSearch = () => {
  501. state.pageInfo.pageNum = 1;
  502. GetTableData();
  503. };
  504. // 表格内容滚动置顶
  505. const TableScrollTop = () => {
  506. const el = tableRef.value?.$el;
  507. if (!el) return;
  508. // 在下一帧执行,确保 DOM 已更新
  509. requestAnimationFrame(() => {
  510. // 尝试多种选择器,兼容不同版本
  511. const scrollWrap =
  512. el.querySelector(".el-scrollbar__wrap") ||
  513. el.querySelector(".el-table__body-wrapper") ||
  514. el.querySelector(".el-table__fixed-body-wrapper");
  515. if (scrollWrap) {
  516. scrollWrap.scrollTop = 0;
  517. scrollWrap.scrollLeft = 0;
  518. } else {
  519. // 若仍未找到,延迟再试一次(表格可能尚未完全渲染)
  520. setTimeout(() => {
  521. const retry =
  522. el.querySelector(".el-scrollbar__wrap") ||
  523. el.querySelector(".el-table__body-wrapper");
  524. if (retry) {
  525. retry.scrollTop = 0;
  526. retry.scrollLeft = 0;
  527. }
  528. }, 200);
  529. }
  530. });
  531. };
  532. // 设置表格高度
  533. const SetTableHeight = () => {
  534. const container = document.getElementsByClassName("report_content")?.[0];
  535. if (container) {
  536. state.tableHeight = container.clientHeight - 152;
  537. }
  538. };
  539. // 窗口大小改变事件
  540. const HandleResize = throttle(() => {
  541. SetTableHeight();
  542. }, 200);
  543. // 初始化
  544. const PageInit = () => {
  545. state.pageInfo.pageNum = 1;
  546. state.checkList = [];
  547. GetStudentTranscriptTitle();
  548. GetTableCount();
  549. GetTableData(true);
  550. };
  551. // 监听筛选条件
  552. watch(
  553. () => analysisStore.filterObject,
  554. async () => {
  555. PageInit();
  556. },
  557. { deep: true },
  558. );
  559. onMounted(() => {
  560. window.addEventListener("resize", HandleResize);
  561. PageInit();
  562. });
  563. onUnmounted(() => {
  564. // 移除监听 防止内存泄漏
  565. window.removeEventListener("resize", HandleResize);
  566. });
  567. </script>
  568. <style lang="scss" scoped>
  569. .input_with {
  570. margin-right: 10px;
  571. }
  572. .count_item {
  573. font-size: 16px;
  574. font-weight: 400;
  575. color: #333;
  576. line-height: 24px;
  577. margin-left: 10px;
  578. &.orange {
  579. color: #fb9f34;
  580. }
  581. }
  582. .checkbox_group {
  583. :deep(.el-checkbox) {
  584. margin-right: 10px;
  585. &:first-child {
  586. margin-right: 20px;
  587. }
  588. }
  589. }
  590. </style>