StudentQuestionImg.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. <template>
  2. <div
  3. class="canvas_image"
  4. v-loading="isLoading"
  5. element-loading-text="加载中..."
  6. :element-loading-spinner="loadingSvgIcon"
  7. element-loading-background="#ffffff"
  8. >
  9. <!-- PointCanvas 组件需要确保也是 Vue 3 版本 -->
  10. <PointCanvas
  11. ref="pointCanvasRef"
  12. :usedCardType="usedCardType"
  13. :drawData="currentDrawData"
  14. :paperImage="paperImage"
  15. type="question"
  16. ></PointCanvas>
  17. </div>
  18. </template>
  19. <script setup lang="ts">
  20. import { ref, watch, onMounted, nextTick } from "vue";
  21. import PointCanvas from "@/components/QuestionPoint.vue"; // 小题的画布版本
  22. import { getStudentPaperCardInfo } from "@/api/analysis";
  23. import { loadingSvgIcon } from "@/utils/common";
  24. // --- 类型定义 ---
  25. interface PagePaintingVO {
  26. x: number;
  27. y: number;
  28. w: number;
  29. h: number;
  30. page: number;
  31. [key: string]: any;
  32. }
  33. interface QuestionVO {
  34. questionName: string;
  35. fullScore: number;
  36. score: number;
  37. titleType: number; // 1: 客观题, 2: 主观题, etc.
  38. samplingPosition: string; // JSON string
  39. drawLineData?: any;
  40. model?: any;
  41. pagePaintingVOS?: PagePaintingVO[];
  42. [key: string]: any;
  43. }
  44. interface PageVO {
  45. picUrl: string;
  46. page: number;
  47. questionVOS?: QuestionVO[];
  48. [key: string]: any;
  49. }
  50. interface PaperDataResult {
  51. pageVOS: PageVO[];
  52. usedCardType: number; // 1: 系统卡, 2: 三方卡
  53. [key: string]: any;
  54. }
  55. interface Props {
  56. paperInfo?: Record<string, any>; // 试卷信息
  57. paperData?: Record<string, any>; // 试卷分析批量查看答题卡数据
  58. isBatch?: boolean; // 试卷分析批量查看答题卡
  59. }
  60. const props = withDefaults(defineProps<Props>(), {
  61. paperInfo: () => ({}),
  62. paperData: () => ({}),
  63. isBatch: false,
  64. });
  65. // --- 响应式数据 ---
  66. const pointCanvasRef = ref<InstanceType<typeof PointCanvas> | null>(null);
  67. const paperImageList = ref<PageVO[]>([]); // 学生试卷图片列表
  68. const currentIndex = ref<number>(0); // 当前学生试卷图片索引
  69. const currentPaperUrl = ref<string>(""); // 当前学生试卷图片地址
  70. const paperImage = ref<{ url: string; page: number }[]>([]); // 学生试卷图片列表(用于显示)
  71. const currentDownLoadName = ref<string>(""); // 当前学生试卷图片下载名称
  72. const currentDrawData = ref<any[]>([]); // 当前学生试卷答题标记数据
  73. const questionList = ref<QuestionVO[]>([]); // 学生试卷题目列表
  74. const usedCardType = ref<number | null>(null); // 1系统卡 2 三方卡
  75. const isLoading = ref<boolean>(false); // 是否正在加载中
  76. // --- 方法 ---
  77. // 获取图片信息 (宽高)
  78. const GetImageInfo = async (
  79. imageUrl: string,
  80. ): Promise<{ width: number; height: number }> => {
  81. try {
  82. // 注意:fetch 可能在某些环境下需要配置代理或处理 CORS
  83. const response = await fetch(imageUrl + "?x-oss-process=image/info");
  84. if (!response.ok) {
  85. throw new Error("Network response was not ok");
  86. }
  87. const data = await response.json();
  88. const imageWidth = Number(data.ImageWidth?.value || 0);
  89. const imageHeight = Number(data.ImageHeight?.value || 0);
  90. return {
  91. width: imageWidth,
  92. height: imageHeight,
  93. };
  94. } catch (error) {
  95. console.error("获取图片信息失败:", error);
  96. // 返回默认值或抛出错误,视业务需求而定
  97. return { width: 0, height: 0 };
  98. }
  99. };
  100. // 获取切块图片的地址 (OSS Crop)
  101. const GetQuestionImgUrl = (
  102. url: string,
  103. x: number,
  104. y: number,
  105. w: number,
  106. h: number,
  107. ): string => {
  108. const ossProcessParam = "x-oss-process=image";
  109. const cropParams = `/crop,x_${Math.round(x)},y_${Math.round(y)},w_${Math.round(w)},h_${Math.round(h)}`;
  110. if (url.includes(ossProcessParam)) {
  111. return url + cropParams;
  112. } else {
  113. const separator = url.includes("?") ? "&" : "?";
  114. return url + `${separator}${ossProcessParam}${cropParams}`;
  115. }
  116. };
  117. // 更新当前试卷数据的公共方法
  118. const UpdateCurrentPaperData = async () => {
  119. if (questionList.value.length === 0) return;
  120. const currentQuestionItem = questionList.value[0];
  121. console.log("打印当前的题目数据", currentQuestionItem);
  122. // 获取第一张图片的尺寸用于坐标转换
  123. // 注意:如果 paperImageList 为空,这里需要保护
  124. if (paperImageList.value.length === 0) return;
  125. const firstPageUrl = paperImageList.value[0].picUrl;
  126. let imageInfo = { width: 0, height: 0 };
  127. if (firstPageUrl) {
  128. imageInfo = await GetImageInfo(firstPageUrl);
  129. }
  130. //兼容旧的数据
  131. if (paperImageList.value[currentIndex.value].useType) {
  132. usedCardType.value = paperImageList.value[currentIndex.value].useType; //卡类型需要调整为从此页获取
  133. }
  134. // 1. 处理显示的图片 (paperImage)
  135. if (currentQuestionItem.titleType == 1) {
  136. // 如果是客观题,显示整张试卷
  137. currentPaperUrl.value = paperImageList.value[currentIndex.value].picUrl;
  138. paperImage.value = [
  139. {
  140. url: currentPaperUrl.value,
  141. page: paperImageList.value[currentIndex.value].page,
  142. },
  143. ];
  144. } else {
  145. // 否则显示对应的切块图片
  146. const list = currentQuestionItem.pagePaintingVOS || [];
  147. paperImage.value = [];
  148. for (const item of list) {
  149. const pageItem = paperImageList.value.find((p) => p.page == item.page);
  150. if (!pageItem) continue;
  151. let obj: { url: string; page: number } = {
  152. url: "",
  153. page: pageItem.page,
  154. };
  155. if (usedCardType.value == 1) {
  156. // 系统卡:计算相对坐标
  157. let templateInfo = {
  158. width: 794 - 30 * 2, // A4 减去边距
  159. height: 1123 - 25 * 2,
  160. };
  161. // 如果长大于宽 就是A3
  162. if (imageInfo.width > imageInfo.height) {
  163. templateInfo = {
  164. width: 1588 - 30 * 2, // A3 减去边距
  165. height: 1123 - 25 * 2,
  166. };
  167. }
  168. const newBlockPoint = {
  169. x: ((item.x - 30) / templateInfo.width) * imageInfo.width,
  170. y: ((item.y - 25) / templateInfo.height) * imageInfo.height,
  171. w: (item.w / templateInfo.width) * imageInfo.width,
  172. h: (item.h / templateInfo.height) * imageInfo.height,
  173. page: item.page,
  174. };
  175. obj.url = GetQuestionImgUrl(
  176. pageItem.picUrl,
  177. newBlockPoint.x,
  178. newBlockPoint.y,
  179. newBlockPoint.w,
  180. newBlockPoint.h,
  181. );
  182. } else {
  183. // 三方卡
  184. obj.url = GetQuestionImgUrl(
  185. pageItem.picUrl,
  186. item.x,
  187. item.y,
  188. item.w,
  189. item.h,
  190. );
  191. }
  192. paperImage.value.push(obj);
  193. }
  194. }
  195. // 2. 处理采分点坐标 (currentDrawData)
  196. let positionX = 0;
  197. let positionY = 0;
  198. try {
  199. const point = JSON.parse(currentQuestionItem.samplingPosition || "{}");
  200. positionX = point.x || 0;
  201. positionY = point.y || 0;
  202. } catch (e) {
  203. console.error("解析采样位置失败", e);
  204. }
  205. if (usedCardType.value == 1) {
  206. // 系统卡:需要根据模板的坐标进行转换
  207. let templateInfo = {
  208. width: 794,
  209. height: 1123,
  210. };
  211. if (imageInfo.width > imageInfo.height) {
  212. templateInfo = {
  213. width: 1588,
  214. height: 1123,
  215. };
  216. }
  217. // 计算试卷相对于模板的倍率
  218. const offsetScale = imageInfo.width / templateInfo.width;
  219. positionX = parseFloat((offsetScale * positionX).toFixed(2));
  220. positionY = parseFloat((offsetScale * positionY).toFixed(2));
  221. }
  222. const drawDataItem = {
  223. id: "",
  224. name: currentQuestionItem.questionName,
  225. fullScore: currentQuestionItem.fullScore, // 满分
  226. score: currentQuestionItem.score, // 学生得分
  227. drawLineData: currentQuestionItem.drawLineData, // 划线标记的数据
  228. x: positionX,
  229. y: positionY,
  230. titleType: currentQuestionItem.titleType, // 题目类型
  231. pagePaintingVOS: currentQuestionItem.pagePaintingVOS, // 切块图片数据
  232. model: currentQuestionItem.model, // 优秀 典型错误数据
  233. samplingPosition: currentQuestionItem.samplingPosition, // 采分点数据位置
  234. };
  235. currentDrawData.value = [drawDataItem];
  236. currentDownLoadName.value = "答题卡";
  237. };
  238. // 处理批量查看的数据
  239. const StudentPaperData = (res: PaperDataResult | any) => {
  240. paperImageList.value = res?.pageVOS || [];
  241. usedCardType.value = res?.usedCardType || 2; // 默认三方卡
  242. // 合并所有试卷图片中的题目列表
  243. let allQuestions: QuestionVO[] = [];
  244. paperImageList.value.forEach((item) => {
  245. if (item.questionVOS && item.questionVOS.length > 0) {
  246. allQuestions = allQuestions.concat(item.questionVOS);
  247. }
  248. });
  249. questionList.value = allQuestions;
  250. currentIndex.value = 0;
  251. if (questionList.value.length > 0) {
  252. UpdateCurrentPaperData();
  253. }
  254. nextTick(() => {
  255. isLoading.value = false;
  256. });
  257. };
  258. // 获取学生试卷详情信息
  259. const GetStudentPaperInfo = () => {
  260. console.log("加载学生小题试卷信息参数", props.paperInfo);
  261. if (props.paperInfo?.examPaperId && props.paperInfo?.platformNumber != null) {
  262. isLoading.value = true;
  263. getStudentPaperCardInfo(props.paperInfo)
  264. .then((res) => {
  265. console.log("打印学生试卷详情信息", res);
  266. if (res.code == 200 && res.data) {
  267. const data = res.data as PaperDataResult;
  268. paperImageList.value = data.pageVOS || [];
  269. usedCardType.value = data.usedCardType || 2;
  270. // 合并所有试卷图片中的题目列表
  271. let allQuestions: QuestionVO[] = [];
  272. paperImageList.value.forEach((item) => {
  273. if (item.questionVOS && item.questionVOS.length > 0) {
  274. allQuestions = allQuestions.concat(item.questionVOS);
  275. }
  276. });
  277. questionList.value = allQuestions;
  278. // 重置索引并更新当前试卷数据
  279. currentIndex.value = 0;
  280. if (questionList.value.length > 0) {
  281. UpdateCurrentPaperData();
  282. }
  283. nextTick(() => {
  284. isLoading.value = false;
  285. });
  286. } else {
  287. nextTick(() => {
  288. isLoading.value = false;
  289. });
  290. }
  291. })
  292. .catch((err) => {
  293. console.error(err);
  294. nextTick(() => {
  295. isLoading.value = false;
  296. });
  297. });
  298. } else {
  299. if (props.isBatch) {
  300. StudentPaperData(props.paperData as PaperDataResult);
  301. } else {
  302. currentDrawData.value = [];
  303. isLoading.value = false;
  304. }
  305. }
  306. };
  307. // --- 监听器 ---
  308. watch(
  309. () => props.paperInfo,
  310. (newVal) => {
  311. if (newVal && Object.keys(newVal).length > 0) {
  312. currentIndex.value = 0;
  313. currentDrawData.value = [];
  314. paperImage.value = [];
  315. console.log("学生试卷信息更新了", newVal);
  316. GetStudentPaperInfo();
  317. }
  318. },
  319. { deep: true },
  320. );
  321. watch(
  322. () => props.paperData,
  323. (newVal) => {
  324. if (newVal && Object.keys(newVal).length > 0) {
  325. currentIndex.value = 0;
  326. currentDrawData.value = [];
  327. paperImage.value = [];
  328. StudentPaperData(newVal as PaperDataResult);
  329. }
  330. },
  331. { deep: true },
  332. );
  333. // --- 生命周期 ---
  334. onMounted(() => {
  335. // 初始加载
  336. GetStudentPaperInfo();
  337. });
  338. </script>
  339. <style lang="scss" scoped>
  340. .canvas_image {
  341. width: 100%;
  342. height: 100%;
  343. margin: auto;
  344. // background-color: green;
  345. .main_container {
  346. width: 100%;
  347. height: 100%;
  348. position: relative;
  349. }
  350. :deep(.el-loading-spinner) {
  351. .el-loading-text {
  352. font-size: 14px;
  353. margin: 0 auto;
  354. }
  355. .circular {
  356. width: 20px;
  357. height: 20px;
  358. color: #2e64fa;
  359. }
  360. }
  361. }
  362. </style>