MarkBody.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. <template>
  2. <div class="mark-body" @scroll="viewScroll">
  3. <div ref="dragContainer" class="mark-body-container">
  4. <div v-if="!store.currentTask" class="mark-body-none">
  5. <div>
  6. <img src="@/assets/image-none-task.png" />
  7. <p>
  8. {{ store.message }}
  9. </p>
  10. </div>
  11. </div>
  12. <div v-else-if="store.isScanImage" :style="{ width: answerPaperScale }">
  13. <div
  14. v-for="(item, index) in sliceImagesWithTrackList"
  15. :key="index"
  16. class="single-image-container"
  17. >
  18. <img :src="item.url" draggable="false" />
  19. <MarkDrawTrack
  20. :trackList="item.trackList"
  21. :specialTagList="item.tagList"
  22. :sliceImageHeight="item.originalImageHeight"
  23. :sliceImageWidth="item.originalImageWidth"
  24. :dx="0"
  25. :dy="0"
  26. />
  27. <!-- 客观题答案标记 -->
  28. <template v-if="item.answerTags">
  29. <div
  30. v-for="(tag, tindex) in item.answerTags"
  31. :key="`tag-${tindex}`"
  32. :style="tag.style"
  33. >
  34. {{ tag.answer }}
  35. </div>
  36. </template>
  37. <!-- 试题评分明细 -->
  38. <template v-if="item.markDetail">
  39. <div
  40. v-for="(minfo, mindex) in item.markDetail"
  41. :key="`mark-${mindex}`"
  42. :style="minfo.style"
  43. class="mark-info"
  44. >
  45. <div v-if="minfo.isFillQuestion">
  46. <div
  47. v-for="user in minfo.users"
  48. :key="user.userId"
  49. :style="{ color: user.color }"
  50. >
  51. <p>{{ user.prename }}:{{ user.userName }},评分:</p>
  52. <p>
  53. {{
  54. user.scores
  55. .map((s) => `${s.subNumber}:${s.score}分`)
  56. .join(",")
  57. }}
  58. </p>
  59. </div>
  60. </div>
  61. <div v-else>
  62. <p
  63. v-for="user in minfo.users"
  64. :key="user.userId"
  65. :style="{ color: user.color }"
  66. >
  67. {{ user.prename }}:{{ user.userName }},评分:{{
  68. user.score
  69. }}
  70. </p>
  71. </div>
  72. <h3>得分:{{ minfo.score }},满分:{{ minfo.maxScore }}</h3>
  73. </div>
  74. </template>
  75. <!-- 总分 -->
  76. <div class="mark-total">
  77. 总分:{{ totalScore }},主观题得分:{{
  78. subjectiveScore
  79. }},客观题得分:{{ objectiveScore }}
  80. </div>
  81. <hr class="image-seperator" />
  82. </div>
  83. </div>
  84. <div v-else>未知数据</div>
  85. </div>
  86. </div>
  87. </template>
  88. <script setup lang="ts">
  89. import { reactive, watch } from "vue";
  90. import { store } from "@/store/store";
  91. import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
  92. import type {
  93. SpecialTag,
  94. Track,
  95. ColorMap,
  96. PaperRecogData,
  97. Question,
  98. } from "@/types";
  99. import { useTimers } from "@/setups/useTimers";
  100. import { loadImage, addHeaderTrackColorAttr, calcSum } from "@/utils/utils";
  101. import { dragImage } from "@/features/mark/use/draggable";
  102. import { maxNum } from "@/utils/utils";
  103. interface SliceImage {
  104. url: string;
  105. trackList: Array<Track>;
  106. tagList: Array<SpecialTag>;
  107. originalImageWidth: number;
  108. originalImageHeight: number;
  109. width: string; // 图片在整个图片列表里面的宽度比例
  110. answerTags?: AnswerTagItem[];
  111. markDetail?: MarkDetailItem[];
  112. }
  113. const { origImageUrls = "sliceUrls" } = defineProps<{
  114. origImageUrls?: "sheetUrls" | "sliceUrls";
  115. }>();
  116. const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
  117. const { dragContainer } = dragImage();
  118. const viewScroll = () => {
  119. if (
  120. dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
  121. dragContainer.value.scrollHeight
  122. ) {
  123. emit("getScrollStatus");
  124. }
  125. };
  126. const { addTimeout } = useTimers();
  127. const totalScore = $computed(() => {
  128. return store.currentTask?.markerScore || 0;
  129. });
  130. const objectiveScore = $computed(() => {
  131. return store.currentTask?.objectiveScore || 0;
  132. });
  133. const subjectiveScore = $computed(() => {
  134. return totalScore - objectiveScore;
  135. });
  136. let sliceImagesWithTrackList: SliceImage[] = reactive([]);
  137. let maxImageWidth = 0;
  138. function addTrackColorAttr(tList: Track[]): Track[] {
  139. let markerIds: (number | undefined)[] = tList
  140. .map((v) => v.userId)
  141. .filter((x) => !!x);
  142. markerIds = Array.from(new Set(markerIds));
  143. // markerIds.sort();
  144. let colorMap: ColorMap = {};
  145. for (let i = 0; i < markerIds.length; i++) {
  146. const mId: any = markerIds[i];
  147. if (i == 0) {
  148. colorMap[mId + ""] = "red";
  149. } else if (i == 1) {
  150. colorMap[mId + ""] = "blue";
  151. } else if (i > 1) {
  152. colorMap[mId + ""] = "gray";
  153. }
  154. }
  155. if (Object.keys(colorMap).length > 1) {
  156. emit("getIsMultComments", true);
  157. }
  158. tList = tList.map((item: Track) => {
  159. item.color = colorMap[item.userId + ""] || "red";
  160. item.isByMultMark = markerIds.length > 1;
  161. return item;
  162. });
  163. return tList;
  164. }
  165. function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
  166. let markerIds: (number | undefined)[] = tList
  167. .map((v) => v.userId)
  168. .filter((x) => !!x);
  169. markerIds = Array.from(new Set(markerIds));
  170. // markerIds.sort();
  171. let colorMap: ColorMap = {};
  172. for (let i = 0; i < markerIds.length; i++) {
  173. const mId: any = markerIds[i];
  174. if (i == 0) {
  175. colorMap[mId + ""] = "red";
  176. } else if (i == 1) {
  177. colorMap[mId + ""] = "blue";
  178. } else if (i > 1) {
  179. colorMap[mId + ""] = "gray";
  180. }
  181. }
  182. tList = tList.map((item: SpecialTag) => {
  183. item.color = colorMap[item.userId + ""] || "red";
  184. item.isByMultMark = markerIds.length > 1;
  185. return item;
  186. });
  187. return tList;
  188. }
  189. async function processImage() {
  190. if (!store.currentTask) return;
  191. const images = [];
  192. const urls = store.currentTask[origImageUrls] || [];
  193. for (const url of urls) {
  194. const image = await loadImage(url);
  195. images.push(image);
  196. }
  197. maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
  198. const trackLists = (store.currentTask.questionList || [])
  199. // .map((q) => q.trackList)
  200. .map((q) => {
  201. let tList = q.trackList;
  202. return q.headerTrack?.length
  203. ? addHeaderTrackColorAttr(q.headerTrack)
  204. : addTrackColorAttr(tList);
  205. })
  206. .flat();
  207. store.setting.doubleTrack = trackLists.some((item) => item.isByMultMark);
  208. // 解析各试题答题区域以及评分
  209. const markDetailList = parseMarkDetailList();
  210. for (const url of urls) {
  211. const indexInSliceUrls = urls.indexOf(url) + 1;
  212. const image = images[indexInSliceUrls - 1];
  213. const thisImageTrackList = trackLists.filter(
  214. (t) => t.offsetIndex === indexInSliceUrls
  215. );
  216. const thisImageTagList = store.currentTask.headerTagList?.length
  217. ? addHeaderTrackColorAttr(
  218. (store.currentTask.headerTagList || []).filter(
  219. (t) => t.offsetIndex === indexInSliceUrls
  220. )
  221. )
  222. : addTagColorAttr(
  223. (store.currentTask.specialTagList || []).filter(
  224. (t) => t.offsetIndex === indexInSliceUrls
  225. )
  226. );
  227. const answerTags = paserRecogData(image, indexInSliceUrls - 1);
  228. sliceImagesWithTrackList.push({
  229. url,
  230. trackList: thisImageTrackList,
  231. tagList: thisImageTagList,
  232. originalImageWidth: image.naturalWidth,
  233. originalImageHeight: image.naturalHeight,
  234. width: (image.naturalWidth / maxImageWidth) * 100 + "%",
  235. answerTags,
  236. markDetail: markDetailList[indexInSliceUrls - 1],
  237. });
  238. }
  239. }
  240. // 解析客观题答案展示位置
  241. interface AnswerTagItem {
  242. mainNumber: number;
  243. subNumber: string;
  244. answer: string;
  245. style: Record<string, string>;
  246. }
  247. function paserRecogData(imgDom: HTMLImageElement, imageIndex): AnswerTagItem[] {
  248. if (
  249. !store.currentTask.recogDatas?.length ||
  250. !store.currentTask.recogDatas[imageIndex]
  251. )
  252. return [];
  253. const answerMap = store.currentTask.answerMap || {};
  254. const { naturalWidth, naturalHeight } = imgDom;
  255. const recogData: PaperRecogData = JSON.parse(
  256. window.atob(store.currentTask.recogDatas[imageIndex])
  257. );
  258. const answerTags: AnswerTagItem[] = [];
  259. // const optionsBlocks = [];
  260. recogData.question.forEach((question) => {
  261. question.fill_result.forEach((result) => {
  262. const tagSize = result.fill_size[1];
  263. const fillPositions = result.fill_position.map((pos) => {
  264. return pos.split(",").map((n) => n * 1);
  265. });
  266. const offsetLt = result.fill_size.map((item) => item * 0.4);
  267. const tagLeft =
  268. maxNum(fillPositions.map((pos) => pos[0])) +
  269. result.fill_size[0] -
  270. offsetLt[0];
  271. const tagTop = fillPositions[0][1] - offsetLt[1];
  272. const { answer, isRight } =
  273. answerMap[`${result.main_number}_${result.sub_number}`] || {};
  274. answerTags.push({
  275. mainNumber: result.main_number,
  276. subNumber: result.sub_number,
  277. answer,
  278. style: {
  279. height: ((100 * tagSize) / naturalHeight).toFixed(4) + "%",
  280. fontSize: ((100 * 20) / tagSize).toFixed(4) + "%",
  281. left: ((100 * tagLeft) / naturalWidth).toFixed(4) + "%",
  282. top: ((100 * tagTop) / naturalHeight).toFixed(4) + "%",
  283. position: "absolute",
  284. color: isRight ? "#05b575" : "#f53f3f",
  285. fontWeight: 600,
  286. lineHeight: 1,
  287. zIndex: 9,
  288. },
  289. });
  290. // 测试:选项框
  291. // fillPositions.forEach((fp, index) => {
  292. // optionsBlocks.push({
  293. // mainNumber: result.main_number,
  294. // subNumber: result.sub_number,
  295. // filled: !!result.fill_option[index],
  296. // style: {
  297. // width:
  298. // ((100 * result.fill_size[0]) / naturalWidth).toFixed(4) + "%",
  299. // height:
  300. // ((100 * result.fill_size[1]) / naturalHeight).toFixed(4) + "%",
  301. // left:
  302. // ((100 * (fp[0] - offsetLt[0])) / naturalWidth).toFixed(4) + "%",
  303. // top:
  304. // ((100 * (fp[1] - offsetLt[1])) / naturalHeight).toFixed(4) + "%",
  305. // position: "absolute",
  306. // border: "1px solid #f53f3f",
  307. // background: result.fill_option[index]
  308. // ? "rgba(245, 63, 63, 0.5)"
  309. // : "transparent",
  310. // zIndex: 9,
  311. // },
  312. // });
  313. // });
  314. });
  315. });
  316. return answerTags;
  317. }
  318. interface QuestionItem {
  319. mainNumber: number;
  320. subNumber: number | string;
  321. }
  322. interface QuestionArea {
  323. i: number;
  324. x: number;
  325. y: number;
  326. w: number;
  327. h: number;
  328. qStruct: string;
  329. }
  330. function parseQuestionAreas(questions: QuestionItem[]) {
  331. if (!questions.length || !store.currentTask.cardData?.length) return [];
  332. let pictureConfigs: QuestionArea[] = [];
  333. const structs = questions.map(
  334. (item) => `${item.mainNumber}_${item.subNumber}`
  335. );
  336. store.currentTask.cardData.forEach((page, pindex) => {
  337. page.exchange.answer_area.forEach((area) => {
  338. const [x, y, w, h] = area.area;
  339. const qStruct = `${area.main_number}_${area.sub_number}`;
  340. const pConfig: QuestionArea = {
  341. i: pindex + 1,
  342. x,
  343. y,
  344. w,
  345. h,
  346. qStruct,
  347. };
  348. if (typeof area.sub_number === "number") {
  349. if (!structs.includes(qStruct)) return;
  350. pictureConfigs.push(pConfig);
  351. return;
  352. }
  353. // 复合区域处理,比如填空题,多个小题合并为一个区域
  354. if (typeof area.sub_number === "string") {
  355. const areaStructs = area.sub_number
  356. .split(",")
  357. .map((subNumber) => `${area.main_number}_${subNumber}`);
  358. if (
  359. structs.some((struct) => areaStructs.includes(struct)) &&
  360. !pictureConfigs.find((item) => item.qStruct === qStruct)
  361. ) {
  362. pictureConfigs.push(pConfig);
  363. }
  364. }
  365. });
  366. });
  367. // console.log(pictureConfigs);
  368. // 合并相邻区域
  369. pictureConfigs.sort((a, b) => {
  370. return a.i - b.i || a.x - b.x || a.y - b.y;
  371. });
  372. let combinePictureConfigList: QuestionArea[] = [];
  373. let prevConfig = null;
  374. pictureConfigs.forEach((item, index) => {
  375. if (!index) {
  376. prevConfig = { ...item };
  377. combinePictureConfigList.push(prevConfig);
  378. return;
  379. }
  380. const elasticRate = 0.01;
  381. if (
  382. prevConfig.i === item.i &&
  383. prevConfig.y + prevConfig.h + elasticRate >= item.y &&
  384. prevConfig.w === item.w &&
  385. prevConfig.x === item.x
  386. ) {
  387. prevConfig.h = item.y + item.h - prevConfig.y;
  388. } else {
  389. prevConfig = { ...item };
  390. combinePictureConfigList.push(prevConfig);
  391. }
  392. });
  393. // console.log(combinePictureConfigList);
  394. return combinePictureConfigList;
  395. }
  396. // 获取属于填空题的试题号
  397. function getFillLines() {
  398. if (!store.currentTask.cardData?.length) return {};
  399. const questions: Record<number, string[]> = {};
  400. store.currentTask.cardData.forEach((page) => {
  401. page.columns.forEach((column) => {
  402. column.elements.forEach((element) => {
  403. if (element.type !== "FILL_LINE") return;
  404. if (!questions[element.topicNo]) questions[element.topicNo] = [];
  405. for (let i = 0; i < element.questionsCount; i++) {
  406. questions[element.topicNo].push(
  407. `${element.topicNo}_${element.startNumber + i}`
  408. );
  409. }
  410. });
  411. });
  412. });
  413. return questions;
  414. }
  415. // 解析各试题答题区域以及评分
  416. interface MarkDetailUserItem {
  417. userId: string;
  418. userName: string;
  419. prename: string;
  420. color: string;
  421. scores: Array<{ subNumber: string; score: number }>;
  422. score: number;
  423. }
  424. type UserMapType = Record<string, MarkDetailUserItem>;
  425. interface MarkDetailItem {
  426. mainNumber: number;
  427. subNumber: string;
  428. isFillQuestion: boolean;
  429. score: number;
  430. maxScore: number;
  431. users: MarkDetailUserItem[];
  432. area: QuestionArea;
  433. style: Record<string, string>;
  434. }
  435. function parseMarkDetailList(): Array<MarkDetailItem[]> {
  436. const dataList: Array<MarkDetailItem[]> = [];
  437. const questions = store.currentTask.questionList || [];
  438. const fillQues = getFillLines();
  439. let fillQuestions = [] as Question[];
  440. let otherQuestions = questions;
  441. if (Object.keys(fillQues).length) {
  442. const fillQNos = Object.values(fillQues).flat();
  443. fillQuestions = questions.filter((q) =>
  444. fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
  445. );
  446. otherQuestions = questions.filter(
  447. (q) => !fillQNos.includes(`${q.mainNumber}_${q.subNumber}`)
  448. );
  449. }
  450. // 填空题:合并所有小题为一个区域
  451. Object.values(fillQues).forEach((qnos) => {
  452. const groupQuestions = fillQuestions.filter((q) =>
  453. qnos.includes(`${q.mainNumber}_${q.subNumber}`)
  454. );
  455. const areas = parseQuestionAreas(groupQuestions);
  456. if (!areas.length) return;
  457. const area = { ...areas[0] };
  458. const imgIndex = area.i - 1;
  459. if (!dataList[imgIndex]) {
  460. dataList[imgIndex] = [];
  461. }
  462. const userMap: UserMapType = {};
  463. // 大题分成两个部分给两个人评 与 大题被两人同时评 是不一样的
  464. const isDoubleMark = !groupQuestions.some((question) => {
  465. const userIds = question.trackList.map((track) => track.userId);
  466. const uids = new Set(userIds);
  467. return uids.size === 1;
  468. });
  469. groupQuestions.forEach((question) => {
  470. question.trackList.forEach((track) => {
  471. if (!userMap[track.userId]) {
  472. userMap[track.userId] = {
  473. userId: track.userId,
  474. userName: track.userName,
  475. color: track.color || "red",
  476. prename: "",
  477. scores: [],
  478. score: 0,
  479. };
  480. }
  481. const existUserScore = userMap[track.userId].scores.find(
  482. (s) => s.subNumber === track.subNumber
  483. );
  484. if (existUserScore) {
  485. existUserScore.score += track.score;
  486. } else {
  487. userMap[track.userId].scores.push({
  488. score: track.score,
  489. subNumber: track.subNumber,
  490. });
  491. }
  492. });
  493. });
  494. const users = Object.values(userMap).map((user, index) => {
  495. const zhs = ["一", "二", "三"];
  496. const prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
  497. return {
  498. ...user,
  499. prename,
  500. score: calcSum(user.scores.map((s) => s.score)),
  501. };
  502. });
  503. const score = calcSum(groupQuestions.map((item) => item.score || 0));
  504. const maxScore = calcSum(groupQuestions.map((item) => item.maxScore));
  505. dataList[imgIndex].push({
  506. mainNumber: groupQuestions[0].mainNumber,
  507. subNumber: "",
  508. isFillQuestion: true,
  509. score,
  510. maxScore,
  511. users,
  512. area,
  513. style: {
  514. position: "absolute",
  515. left: (100 * area.x).toFixed(4) + "%",
  516. top: (100 * area.y).toFixed(4) + "%",
  517. width: (100 * area.w).toFixed(4) + "%",
  518. fontSize: "14px",
  519. lineHeight: 1,
  520. zIndex: 9,
  521. },
  522. });
  523. });
  524. // 其他试题
  525. otherQuestions.forEach((question) => {
  526. const areas = parseQuestionAreas([question]);
  527. const area = { ...areas[0] };
  528. const imgIndex = area.i - 1;
  529. if (!dataList[imgIndex]) {
  530. dataList[imgIndex] = [];
  531. }
  532. const userMap: UserMapType = {};
  533. const isArbitration = Boolean(question.headerTrack?.length);
  534. const tList = isArbitration ? question.headerTrack : question.trackList;
  535. tList.forEach((track) => {
  536. if (!userMap[track.userId]) {
  537. userMap[track.userId] = {
  538. userId: track.userId,
  539. userName: track.userName,
  540. color: track.color || "red",
  541. prename: "",
  542. scores: [],
  543. score: 0,
  544. };
  545. }
  546. userMap[track.userId].scores.push({
  547. score: track.score,
  548. subNumber: track.subNumber,
  549. });
  550. });
  551. const isDoubleMark = Object.keys(userMap).length > 1;
  552. const users = Object.values(userMap).map((user, index) => {
  553. const zhs = ["一", "二", "三"];
  554. let prename = "";
  555. if (isArbitration) {
  556. prename = "仲裁";
  557. } else {
  558. prename = isDoubleMark ? `${zhs[index] || ""}评` : "评卷员";
  559. }
  560. return {
  561. ...user,
  562. prename,
  563. score: calcSum(user.scores.map((s) => s.score)),
  564. };
  565. });
  566. dataList[imgIndex].push({
  567. mainNumber: question.mainNumber,
  568. subNumber: question.subNumber,
  569. isFillQuestion: false,
  570. score: question.score,
  571. maxScore: question.maxScore,
  572. users,
  573. area,
  574. style: {
  575. position: "absolute",
  576. left: (100 * area.x).toFixed(4) + "%",
  577. top: (100 * area.y).toFixed(4) + "%",
  578. width: (100 * area.w).toFixed(4) + "%",
  579. fontSize: "14px",
  580. lineHeight: 1,
  581. zIndex: 9,
  582. },
  583. });
  584. });
  585. return dataList;
  586. }
  587. // should not render twice at the same time
  588. let renderLock = false;
  589. const renderPaperAndMark = async () => {
  590. if (renderLock) {
  591. console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
  592. await new Promise((res) => setTimeout(res, 1000));
  593. await renderPaperAndMark();
  594. return;
  595. }
  596. renderLock = true;
  597. sliceImagesWithTrackList.splice(0);
  598. if (!store.currentTask) {
  599. renderLock = false;
  600. return;
  601. }
  602. try {
  603. store.globalMask = true;
  604. await processImage();
  605. } catch (error) {
  606. sliceImagesWithTrackList.splice(0);
  607. console.log("render error ", error);
  608. // 图片加载出错,自动加载下一个任务
  609. emit("error");
  610. } finally {
  611. await new Promise((res) => setTimeout(res, 500));
  612. store.globalMask = false;
  613. renderLock = false;
  614. }
  615. };
  616. watch(() => store.currentTask, renderPaperAndMark);
  617. watch(
  618. (): (number | undefined)[] => [
  619. store.minimapScrollToX,
  620. store.minimapScrollToY,
  621. ],
  622. () => {
  623. const container = document.querySelector<HTMLDivElement>(
  624. ".mark-body-container"
  625. );
  626. addTimeout(() => {
  627. if (
  628. container &&
  629. typeof store.minimapScrollToX === "number" &&
  630. typeof store.minimapScrollToY === "number"
  631. ) {
  632. const { scrollWidth, scrollHeight } = container;
  633. container.scrollTo({
  634. top: scrollHeight * store.minimapScrollToY,
  635. left: scrollWidth * store.minimapScrollToX,
  636. behavior: "smooth",
  637. });
  638. }
  639. }, 10);
  640. }
  641. );
  642. const answerPaperScale = $computed(() => {
  643. // 放大、缩小不影响页面之前的滚动条定位
  644. let percentWidth = 0;
  645. let percentTop = 0;
  646. const container = document.querySelector(".mark-body-container");
  647. if (container) {
  648. const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
  649. percentWidth = scrollLeft / scrollWidth;
  650. percentTop = scrollTop / scrollHeight;
  651. }
  652. addTimeout(() => {
  653. if (container) {
  654. const { scrollWidth, scrollHeight } = container;
  655. container.scrollTo({
  656. left: scrollWidth * percentWidth,
  657. top: scrollHeight * percentTop,
  658. });
  659. }
  660. }, 10);
  661. const scale = store.setting.uiSetting["answer.paper.scale"];
  662. return scale * 100 + "%";
  663. });
  664. </script>
  665. <style scoped>
  666. .mark-body-container {
  667. overflow: auto;
  668. background-color: var(--app-container-bg-color);
  669. background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
  670. linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
  671. linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
  672. linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
  673. background-size: 20px 20px;
  674. background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
  675. transform: inherit;
  676. cursor: grab;
  677. user-select: none;
  678. }
  679. .mark-body-container img {
  680. width: 100%;
  681. }
  682. .single-image-container {
  683. position: relative;
  684. }
  685. .image-seperator {
  686. border: 2px solid rgba(120, 120, 120, 0.1);
  687. }
  688. .mark-info {
  689. display: flex;
  690. justify-content: space-between;
  691. }
  692. .mark-info h3 {
  693. font-size: 20px;
  694. font-weight: bold;
  695. line-height: 1;
  696. color: #f53f3f;
  697. }
  698. .mark-info p {
  699. margin: 0;
  700. line-height: 20px;
  701. font-weight: bold;
  702. }
  703. .mark-total {
  704. font-size: 20px;
  705. font-weight: bold;
  706. position: absolute;
  707. top: 1%;
  708. left: 15%;
  709. z-index: 9;
  710. color: #f53f3f;
  711. }
  712. </style>