optionDetail.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. <template>
  2. <!-- 成绩查询 - 成绩单 -->
  3. <ReportModule
  4. ref="reportModuleRef"
  5. :showTitle="false"
  6. :showDescribe="false"
  7. :showPrintBtn="false"
  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. >
  17. <template #title_left>
  18. <el-input
  19. v-model="state.keyWord"
  20. clearable
  21. style="width: 200px"
  22. placeholder="请输入学号或姓名"
  23. class="input_with"
  24. @input="HandleSearch"
  25. >
  26. <template #append>
  27. <el-button :icon="Search" @click="HandleSearch" />
  28. </template>
  29. </el-input>
  30. <span class="count_item"
  31. >应考:{{ state.tableCount.examStudentCount }}人</span
  32. >
  33. <span class="count_item">实考:{{ state.tableCount.normalCount }}人</span>
  34. <span class="count_item orange"
  35. >缺考:{{ state.tableCount.missExamCount }}人</span
  36. >
  37. </template>
  38. <template #module_table_chart>
  39. <el-table
  40. :data="state.tableData"
  41. border
  42. stripe
  43. ref="tableRef"
  44. :height="state.tableHeight"
  45. v-loading="state.tableLoading"
  46. :element-loading-text="state.loadingText"
  47. :element-loading-spinner="loadingSvgIcon"
  48. element-loading-background="#ffffff"
  49. >
  50. <el-table-column
  51. type="index"
  52. :index="GetIndexNumber"
  53. align="center"
  54. width="70"
  55. label="序号"
  56. fixed="left"
  57. ></el-table-column>
  58. <el-table-column
  59. v-for="item in state.staticHeaderData"
  60. :key="item.prop"
  61. :prop="item.prop"
  62. :label="item.label"
  63. min-width="80"
  64. fixed="left"
  65. >
  66. <template #default="scope">
  67. <span
  68. v-if="
  69. scope.row.score != '缺考' &&
  70. scope.row.score != '违纪' &&
  71. item.prop === 'studentUserName'
  72. "
  73. @click="OpenStudentPaper(scope.row)"
  74. style="cursor: pointer; color: #2e64fa"
  75. >
  76. {{ scope.row[item.prop] }}
  77. </span>
  78. <template v-else>
  79. {{ scope.row?.[item.prop] || "-" }}
  80. </template>
  81. </template>
  82. </el-table-column>
  83. <el-table-column
  84. v-for="(item, index) in state.answerDataTitle"
  85. :key="item.prop"
  86. :prop="item.prop"
  87. :label="item.label"
  88. min-width="100"
  89. >
  90. <template #default="scope">{{
  91. scope.row?.dynamicsData?.answerData?.[index] || "-"
  92. }}</template>
  93. </el-table-column>
  94. </el-table>
  95. </template>
  96. </ReportModule>
  97. <StudentPaper
  98. :modelValue="state.showStudentPaperDialog"
  99. :paperInfo="state.paperInfo"
  100. :pageTitle="state.paperTitle"
  101. @updateModelValue="UpdateModelValue"
  102. ></StudentPaper>
  103. </template>
  104. <script lang="ts" setup>
  105. import ReportModule from "@/components/ReportModule.vue";
  106. import StudentPaper from "@/components/StudentPaper.vue"; //学生答题卡组件
  107. import {
  108. studentTranscriptTitle,
  109. queryJointStudentStatistics,
  110. studentTranscript,
  111. studentTranscriptExcel,
  112. } from "@/api/analysis";
  113. import { useAnalysisStore } from "@/store/analysis";
  114. import { Search } from "@element-plus/icons-vue";
  115. import { downloadExcel } from "@/utils/exportExcel";
  116. import { loadingSvgIcon } from "@/utils/common";
  117. import {
  118. onMounted,
  119. onUnmounted,
  120. reactive,
  121. ref,
  122. computed,
  123. nextTick,
  124. watch,
  125. } from "vue";
  126. import { throttle } from "lodash";
  127. interface TableColumn {
  128. prop: string;
  129. label: string;
  130. display?: boolean;
  131. [key: string]: any;
  132. }
  133. interface TableCount {
  134. examStudentCount: number | string;
  135. normalCount: number | string;
  136. missExamCount: number | string;
  137. [key: string]: any; // 防止后端返回额外字段导致类型校验报错
  138. }
  139. interface PageInfo {
  140. pageSize: number;
  141. pageNum: number;
  142. total: number;
  143. }
  144. interface PaperInfo {
  145. examPaperId: string | number;
  146. platformNumber: string;
  147. questionId: string;
  148. }
  149. interface State {
  150. keyWord: string;
  151. tableHeight: Number;
  152. tableData: any[];
  153. staticHeaderData: TableColumn[];
  154. answerDataTitle: TableColumn[];
  155. tableCount: TableCount;
  156. pageInfo: PageInfo;
  157. tableLoading: boolean; // 修复:使用小写 boolean
  158. loadingText: string; // 修复:使用小写 string
  159. paperInfo: PaperInfo;
  160. paperTitle: string;
  161. showStudentPaperDialog: boolean;
  162. }
  163. const analysisStore = useAnalysisStore();
  164. const getExamName = computed(() => {
  165. return analysisStore.analysisExamInfo.examName || "";
  166. });
  167. const tableRef = ref<any>(null);
  168. const reportModuleRef = ref<any>(null);
  169. const state = reactive<State>({
  170. keyWord: "",
  171. tableHeight: 1000, //表格高度
  172. tableData: [],
  173. staticHeaderData: [],
  174. answerDataTitle: [],
  175. tableCount: {
  176. examStudentCount: 0,
  177. normalCount: 0,
  178. missExamCount: 0,
  179. },
  180. pageInfo: {
  181. pageSize: 50,
  182. pageNum: 1,
  183. total: 0,
  184. },
  185. tableLoading: true,
  186. loadingText: "加载中,请稍后...",
  187. paperInfo: {
  188. examPaperId: "", //考试科目id
  189. platformNumber: "", //学籍号平台号
  190. questionId: "", //题目id
  191. }, //学生试卷信息
  192. paperTitle: "", //学生试卷标题
  193. showStudentPaperDialog: false, //是否显示学生答题卡弹窗
  194. });
  195. /** 获取表头 */
  196. const GetStudentTranscriptTitle = async () => {
  197. if (!analysisStore.filterObject) return;
  198. try {
  199. const res = await studentTranscriptTitle({
  200. ...analysisStore.filterObject,
  201. topicControl: 0,
  202. });
  203. if (res.code === 200) {
  204. const staticHeaderData = res.data?.title?.staticHeaderData || [];
  205. state.staticHeaderData = staticHeaderData.filter(
  206. (item: TableColumn) => item.display, // 优化:使用已定义的接口类型
  207. );
  208. state.answerDataTitle =
  209. res.data?.title?.dynamicsHeaderData?.answerDataTitle || [];
  210. }
  211. } catch (err) {
  212. console.error("获取表头失败:", err);
  213. }
  214. };
  215. /** 获取顶部统计 */
  216. const GetTableCount = async () => {
  217. if (!analysisStore.filterObject) return;
  218. try {
  219. const res = await queryJointStudentStatistics({
  220. ...analysisStore.filterObject,
  221. topicControl: 0,
  222. queryStr: state.keyWord,
  223. });
  224. if (res.code === 200) {
  225. state.tableCount = res.data || {};
  226. }
  227. } catch (err) {
  228. console.error("获取统计数据失败:", err);
  229. }
  230. };
  231. /** 获取表格数据 */
  232. const GetTableData = async (isInit: boolean = false) => {
  233. if (!analysisStore.filterObject) return;
  234. try {
  235. state.tableLoading = true;
  236. const res = await studentTranscript({
  237. ...analysisStore.filterObject,
  238. topicControl: 0,
  239. queryStr: state.keyWord,
  240. pageParam: {
  241. pageNum: state.pageInfo.pageNum,
  242. pageSize: state.pageInfo.pageSize,
  243. },
  244. });
  245. if (res.code === 200) {
  246. state.tableData = res.data?.listData || [];
  247. state.pageInfo.total = Number(res.data?.total || 0);
  248. }
  249. } catch (err) {
  250. console.error("获取表格数据失败:", err);
  251. } finally {
  252. state.tableLoading = false;
  253. // 数据加载完成后重置滚动
  254. nextTick(() => {
  255. TableScrollTop();
  256. if (isInit) {
  257. SetTableHeight();
  258. }
  259. });
  260. }
  261. };
  262. //获取序号
  263. const GetIndexNumber = (index: number) => {
  264. let indexCount =
  265. (state.pageInfo.pageNum - 1) * state.pageInfo.pageSize + index + 1;
  266. return indexCount;
  267. };
  268. // 当前页码事件
  269. const ChangeCurrentPage = (val: number) => {
  270. state.pageInfo.pageNum = val;
  271. GetTableData();
  272. };
  273. // 每页显示个数事件
  274. const ChangePageSize = (val: number) => {
  275. state.pageInfo.pageSize = val;
  276. state.pageInfo.pageNum = 1;
  277. GetTableData();
  278. };
  279. // 导出Excel
  280. const ExportExcel = () => {
  281. // 1. 设置加载状态
  282. reportModuleRef.value?.SetExportLoading?.(true);
  283. // 2. 参数
  284. const params = {
  285. ...analysisStore.filterObject,
  286. topicControl: 0,
  287. topicControlName: "选项明细",
  288. queryStr: state.keyWord,
  289. pageParam: {
  290. pageNum: state.pageInfo.pageNum,
  291. pageSize: state.pageInfo.pageSize,
  292. },
  293. staticHeaderData: state.staticHeaderData,
  294. dynamicsHeaderDataMap: {
  295. answerDataTitle: state.answerDataTitle,
  296. },
  297. };
  298. // 3. 调用通用下载方法,并在完成后重置加载状态
  299. downloadExcel(studentTranscriptExcel, params).finally(() => {
  300. reportModuleRef.value?.SetExportLoading?.(false);
  301. });
  302. };
  303. //打开答题卡弹窗
  304. const OpenStudentPaper = (row: any) => {
  305. // 修复:移除多余的 prop 参数,添加 row 类型
  306. state.paperInfo = {
  307. examPaperId: analysisStore.filterObject?.subjectId || "", // 修复:增加可选链和默认值
  308. platformNumber: row.studentRegistrationCode, //学籍号平台号
  309. questionId: "", //题目id
  310. };
  311. state.paperTitle = `${getExamName.value}-${analysisStore.filterObject?.subjectName || ""}-${row.className}-${row.studentUserName}`; //学生姓名
  312. state.showStudentPaperDialog = true;
  313. };
  314. //更新弹窗状态
  315. const UpdateModelValue = (val: boolean) => {
  316. state.showStudentPaperDialog = val;
  317. };
  318. // 搜索事件
  319. const HandleSearch = () => {
  320. state.pageInfo.pageNum = 1;
  321. GetTableData();
  322. };
  323. // 表格内容滚动置顶
  324. const TableScrollTop = () => {
  325. const el = tableRef.value?.$el;
  326. if (!el) return;
  327. // 在下一帧执行,确保 DOM 已更新
  328. requestAnimationFrame(() => {
  329. // 尝试多种选择器,兼容不同版本
  330. const scrollWrap =
  331. el.querySelector(".el-scrollbar__wrap") ||
  332. el.querySelector(".el-table__body-wrapper") ||
  333. el.querySelector(".el-table__fixed-body-wrapper");
  334. if (scrollWrap) {
  335. scrollWrap.scrollTop = 0;
  336. scrollWrap.scrollLeft = 0;
  337. } else {
  338. // 若仍未找到,延迟再试一次(表格可能尚未完全渲染)
  339. setTimeout(() => {
  340. const retry =
  341. el.querySelector(".el-scrollbar__wrap") ||
  342. el.querySelector(".el-table__body-wrapper");
  343. if (retry) {
  344. retry.scrollTop = 0;
  345. retry.scrollLeft = 0;
  346. }
  347. }, 200);
  348. }
  349. });
  350. };
  351. // 设置表格高度
  352. const SetTableHeight = () => {
  353. const container = document.getElementsByClassName("report_content")?.[0];
  354. if (container) {
  355. state.tableHeight = container.clientHeight - 152;
  356. }
  357. };
  358. // 窗口大小改变事件
  359. const HandleResize = throttle(() => {
  360. SetTableHeight();
  361. }, 200);
  362. // 初始化
  363. const PageInit = () => {
  364. state.pageInfo.pageNum = 1;
  365. GetStudentTranscriptTitle();
  366. GetTableCount();
  367. GetTableData(true);
  368. };
  369. // 监听筛选条件
  370. watch(
  371. () => analysisStore.filterObject,
  372. () => {
  373. // 修复:移除不必要的 async
  374. PageInit();
  375. },
  376. { deep: true },
  377. );
  378. onMounted(() => {
  379. window.addEventListener("resize", HandleResize);
  380. PageInit();
  381. });
  382. onUnmounted(() => {
  383. // 移除监听 防止内存泄漏
  384. window.removeEventListener("resize", HandleResize);
  385. });
  386. </script>
  387. <style lang="scss" scoped>
  388. .input_with {
  389. margin-right: 10px;
  390. }
  391. .count_item {
  392. font-size: 16px;
  393. font-weight: 400;
  394. color: #333;
  395. line-height: 24px;
  396. margin-left: 10px;
  397. &.orange {
  398. color: #fb9f34;
  399. }
  400. }
  401. .checkbox_group {
  402. :deep(.el-checkbox) {
  403. margin-right: 10px;
  404. &:first-child {
  405. margin-right: 20px;
  406. }
  407. }
  408. }
  409. </style>