StudentPaper.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <template>
  2. <el-dialog
  3. title="预览答题卡"
  4. append-to-body
  5. v-model="showDialog"
  6. class="page_full_dialog"
  7. fullscreen
  8. >
  9. <div class="page_header" ref="pageHeaderRef">
  10. <div class="back_button" @click="GoBack">
  11. <i class="iconfont icon_return"></i>返回
  12. </div>
  13. <div class="header_title">
  14. {{ pageTitle }}<span class="header_title_tip">(右键点击图片另存为可保存图片到本地)</span>
  15. </div>
  16. </div>
  17. <div class="dialog_paper">
  18. <div class="paper_content">
  19. <div class="canvas_button">
  20. <div :class="currentIndex === 0 ? 'disable_button_item' : 'button_item'" @click="LastPaper">
  21. <el-icon><ArrowLeft /></el-icon>
  22. </div>
  23. </div>
  24. <div
  25. class="canvas_image"
  26. v-loading="isLoading"
  27. element-loading-text="加载中……"
  28. element-loading-spinner="el-icon-loading"
  29. element-loading-background="#ffffff"
  30. >
  31. <PaperImage
  32. :paperImgUrl="currentPaperUrl"
  33. :currentPage="currentPage"
  34. :usedCardType="usedCardType"
  35. :drawData="currentDrawData"
  36. :downLoadName="currentDownLoadName"
  37. />
  38. </div>
  39. <div class="canvas_button">
  40. <div :class="currentIndex === paperImageList.length - 1 ? 'disable_button_item' : 'button_item'" @click="NextPaper">
  41. <el-icon><ArrowRight /></el-icon>
  42. </div>
  43. </div>
  44. </div>
  45. <div class="paper_question" ref="paperQuestionRef">
  46. <div class="area_table">
  47. <el-table :data="questionList" border ref="questionTableRef" :key="pageKey" :max-height="questionTableHeight">
  48. <el-table-column label="小题名称" prop="questionName" align="center">
  49. <template #default="scope">
  50. {{ scope.row.questionName }}
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="满分/答案" align="center">
  54. <template #default="scope">
  55. {{ scope.row.fullScore }}
  56. <span v-if="scope.row.questionAnswer">/ {{ scope.row.questionAnswer }}</span>
  57. </template>
  58. </el-table-column>
  59. <el-table-column label="得分/答案" align="center">
  60. <template #default="scope">
  61. <div :class="scope.row.fullScore == scope.row.score ? '' : 'question_score'">
  62. {{ scope.row.score }}
  63. <span v-if="scope.row.answer">/ {{ scope.row.answer }}</span>
  64. </div>
  65. </template>
  66. </el-table-column>
  67. </el-table>
  68. </div>
  69. </div>
  70. </div>
  71. </el-dialog>
  72. </template>
  73. <script lang="ts" setup>
  74. import { ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
  75. import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
  76. import PaperImage from "@/components/PaperImage.vue"; // 学生试卷组件
  77. import { getStudentPaperCardInfo } from "@/api/analysis";
  78. // ================= 类型定义 =================
  79. interface PaperInfo {
  80. examPaperId: string;
  81. platformNumber: string | number | null;
  82. [key: string]: any;
  83. }
  84. interface QuestionItem {
  85. questionName: string;
  86. fullScore: number | string;
  87. score: number | string;
  88. questionAnswer?: string;
  89. answer?: string;
  90. samplingPosition?: string;
  91. pagePaintingVOS?: any[];
  92. questionId?: string | number;
  93. [key: string]: any;
  94. }
  95. interface CrossDrawItem {
  96. page: number | string;
  97. item: QuestionItem;
  98. }
  99. // ================= Props & Emits =================
  100. const props = withDefaults(
  101. defineProps<{
  102. paperInfo?: PaperInfo;
  103. pageTitle?: string;
  104. modelValue?: boolean; // 替代原有的 value
  105. examLevel?: number | string;
  106. }>(),
  107. {
  108. paperInfo: () => ({}) as PaperInfo,
  109. pageTitle: "",
  110. modelValue: false,
  111. examLevel: 2,
  112. }
  113. );
  114. const emit = defineEmits<{
  115. (e: "updateModelValue", value: boolean): void;
  116. }>();
  117. const showDialog = ref(false);
  118. // ================= 响应式数据 (替代 data) =================
  119. const paperImageList = ref<any[]>([]); // 学生试卷图片列表
  120. const currentIndex = ref(0); // 当前学生试卷图片索引
  121. const currentPage = ref(1); // 当前学生试卷页码 默认第一页
  122. const currentPaperUrl = ref(""); // 当前学生试卷图片地址
  123. const currentDownLoadName = ref(""); // 当前学生试卷图片下载名称
  124. const currentDrawData = ref<any[]>([]); // 当前学生试卷答题标记数据
  125. const questionList = ref<QuestionItem[]>([]); // 学生试卷题目列表
  126. const questionTableHeight = ref<number | string>(0); // 学生试卷题目列表高度
  127. const usedCardType = ref<number | null>(null); // 1系统卡 2 三方卡
  128. const isLoading = ref(false); // 是否正在加载中
  129. const studentName = ref(""); // 学生姓名
  130. const crossDrawData = ref<CrossDrawItem[]>([]); // 跨页的批注数据
  131. const pageKey = ref(1);
  132. // DOM Refs
  133. const pageHeaderRef = ref<HTMLElement | null>(null);
  134. const paperQuestionRef = ref<HTMLElement | null>(null);
  135. const questionTableRef = ref();
  136. // ================= 监听器 =================
  137. watch(
  138. () => props.modelValue,
  139. (newVal) => {
  140. if (newVal) {
  141. showDialog.value = true;
  142. currentIndex.value = 0;
  143. pageKey.value += 1;
  144. GetStudentPaperInfo();
  145. }
  146. }
  147. );
  148. // ================= 方法 (替代 methods) =================
  149. // 禁用右键菜单的方法
  150. const DisableRightClick = (event: MouseEvent) => {
  151. event.preventDefault();
  152. return false;
  153. };
  154. // 返回
  155. const GoBack = () => {
  156. currentPaperUrl.value = "";
  157. currentDrawData.value = [];
  158. isLoading.value = true;
  159. showDialog.value = false;
  160. emit("updateModelValue", false);
  161. };
  162. // 上一个试卷
  163. const LastPaper = () => {
  164. if (currentIndex.value > 0) {
  165. currentIndex.value--;
  166. UpdateCurrentPaperData();
  167. }
  168. };
  169. // 下一个试卷
  170. const NextPaper = () => {
  171. if (currentIndex.value < paperImageList.value.length - 1) {
  172. currentIndex.value++;
  173. UpdateCurrentPaperData();
  174. }
  175. };
  176. // 获取学生试卷详情信息
  177. const GetStudentPaperInfo = () => {
  178. currentPaperUrl.value = "";
  179. if (props.paperInfo?.examPaperId && props.paperInfo?.platformNumber != null) {
  180. isLoading.value = true;
  181. getStudentPaperCardInfo(props.paperInfo).then((res: any) => {
  182. if (res.code == 200 && res.data) {
  183. paperImageList.value = res.data.pageVOS || [];
  184. usedCardType.value = res.data.usedCardType;
  185. // 重置索引并更新当前试卷数据
  186. currentIndex.value = 0;
  187. UpdateCurrentPaperData();
  188. // 合并所有试卷图片中的题目列表
  189. let allQuestions: QuestionItem[] = [];
  190. // 先添加总分数据
  191. const totalScore: QuestionItem = {
  192. questionName: "总分",
  193. fullScore: res.data.fullScore || 150,
  194. score: res.data.totalScore,
  195. questionAnswer: "",
  196. answer: "",
  197. samplingPosition: '{"x":195,"y":147,"page":1}',
  198. };
  199. if (paperImageList.value.length > 0 && paperImageList.value[0].questionVOS) {
  200. paperImageList.value[0].questionVOS.unshift(totalScore);
  201. }
  202. if (props.examLevel == 1) {
  203. // 联考
  204. allQuestions = res?.data?.studentAnswerBOS || [];
  205. } else {
  206. // 校考
  207. paperImageList.value.forEach((item) => {
  208. if (item.questionVOS && item.questionVOS.length > 0) {
  209. allQuestions = allQuestions.concat(item.questionVOS);
  210. }
  211. });
  212. }
  213. questionList.value = allQuestions;
  214. CalculateTableHeight(); // 计算表格高度
  215. nextTick(() => {
  216. setTimeout(() => {
  217. isLoading.value = false;
  218. }, 500);
  219. });
  220. } else {
  221. currentPaperUrl.value = "";
  222. currentDrawData.value = [];
  223. paperImageList.value = [];
  224. currentIndex.value = 0;
  225. questionList.value = [];
  226. questionTableHeight.value = "";
  227. nextTick(() => {
  228. isLoading.value = false;
  229. });
  230. }
  231. });
  232. }
  233. };
  234. // 计算表格高度(更精确的方式)
  235. const CalculateTableHeight = () => {
  236. nextTick(() => {
  237. if (paperQuestionRef.value && pageHeaderRef.value) {
  238. const availableHeight = window.innerHeight - 65 - 80;
  239. questionTableHeight.value = availableHeight;
  240. } else {
  241. const availableHeight = window.innerHeight - 65 - 40 - 40;
  242. questionTableHeight.value = availableHeight;
  243. }
  244. });
  245. };
  246. // 是否重复
  247. const IsRepeat = (obj: CrossDrawItem): boolean => {
  248. return crossDrawData.value.some(
  249. (item) => item.page == obj.page && item.item.questionId == obj.item.questionId
  250. );
  251. };
  252. // 更新当前试卷数据的公共方法
  253. const UpdateCurrentPaperData = () => {
  254. if (paperImageList.value.length > 0 && currentIndex.value < paperImageList.value.length) {
  255. const currentItem = paperImageList.value[currentIndex.value];
  256. currentPaperUrl.value = currentItem.picUrl;
  257. // 兼容旧的数据
  258. if (currentItem.useType) {
  259. usedCardType.value = currentItem.useType;
  260. }
  261. let drawData = currentItem.questionVOS || [];
  262. currentPage.value = currentItem.page; // 打印当前的页码
  263. if (drawData.length > 0) {
  264. drawData.forEach((item: QuestionItem) => {
  265. // 如果有批阅块 且大于1个批阅块 代表跨页的
  266. if (item.pagePaintingVOS && item.pagePaintingVOS.length > 1) {
  267. item.pagePaintingVOS.forEach((block) => {
  268. if (block.page != currentPage.value) {
  269. const obj: CrossDrawItem = {
  270. page: block.page,
  271. item: item,
  272. };
  273. // 插入之前 根据page和题目id判断是否重复
  274. if (!IsRepeat(obj)) {
  275. crossDrawData.value.push(obj);
  276. }
  277. }
  278. });
  279. }
  280. });
  281. }
  282. currentDrawData.value = currentItem.questionVOS || [];
  283. currentDownLoadName.value = `${props.pageTitle}答题卡第${currentIndex.value + 1}页`; // 重置下载名称
  284. } else {
  285. // 处理边界情况
  286. currentPaperUrl.value = "";
  287. currentDrawData.value = [];
  288. paperImageList.value = [];
  289. currentIndex.value = 0;
  290. currentDownLoadName.value = "";
  291. }
  292. };
  293. // ================= 生命周期 =================
  294. onMounted(() => {
  295. // 禁用鼠标右键
  296. document.addEventListener("contextmenu", DisableRightClick);
  297. });
  298. onBeforeUnmount(() => {
  299. document.removeEventListener("contextmenu", DisableRightClick);
  300. });
  301. </script>
  302. <style lang="scss" scoped>
  303. .dialog_paper {
  304. width: 100%;
  305. height: calc(100vh - 65px);
  306. background: #f0f4fb;
  307. overflow: hidden;
  308. display: flex;
  309. justify-content: space-between;
  310. padding: 20px;
  311. box-sizing: border-box;
  312. .paper_content {
  313. width: calc(100% - 340px);
  314. height: 100%;
  315. display: flex;
  316. justify-content: flex-start;
  317. .canvas_button {
  318. width: 48px;
  319. height: 100%;
  320. display: flex;
  321. justify-content: center;
  322. align-items: center;
  323. .button_item {
  324. width: 48px;
  325. height: 48px;
  326. border-radius: 50%;
  327. background: rgba(0, 0, 0, 0.1);
  328. color: #999999;
  329. font-size: 24px;
  330. line-height: 48px;
  331. text-align: center;
  332. cursor: pointer;
  333. display: flex;
  334. justify-content: center;
  335. align-items: center;
  336. }
  337. .disable_button_item {
  338. width: 48px;
  339. height: 48px;
  340. border-radius: 50%;
  341. background: rgba(0, 0, 0, 0.1);
  342. color: #c0c4cc;
  343. font-size: 24px;
  344. line-height: 48px;
  345. text-align: center;
  346. display: flex;
  347. justify-content: center;
  348. align-items: center;
  349. }
  350. }
  351. .canvas_image {
  352. width: calc(100% - 88px - 40px);
  353. height: 100%;
  354. margin: auto;
  355. }
  356. }
  357. .paper_question {
  358. width: 320px;
  359. height: 100%;
  360. background: #ffffff;
  361. border-radius: 10px;
  362. border: 1px solid #ebeef5;
  363. padding: 20px;
  364. box-sizing: border-box;
  365. .question_score {
  366. color: #f56c6c;
  367. }
  368. }
  369. }
  370. </style>