CommonMarkBody.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  1. <template>
  2. <div class="mark-body">
  3. <div v-if="markStatus" class="mark-body-status">
  4. {{ markStatus }}
  5. </div>
  6. <div ref="dragContainer" class="mark-body-container">
  7. <div v-if="!store.currentTask" class="mark-body-none">
  8. <div>
  9. <img src="@/assets/image-none-task.png" />
  10. <p>
  11. {{ store.message }}
  12. </p>
  13. </div>
  14. </div>
  15. <div
  16. v-else-if="store.isScanImage"
  17. :style="{ width: answerPaperScale }"
  18. :class="[`rotate-board-${rotateBoard}`]"
  19. >
  20. <template
  21. v-for="(item, index) in sliceImagesWithTrackList"
  22. :key="index"
  23. >
  24. <div class="single-image-container">
  25. <img
  26. :src="item.url"
  27. draggable="false"
  28. @click="(event) => innerMakeTrack(event, item)"
  29. @contextmenu="showBigImage"
  30. />
  31. <MarkDrawTrack
  32. :trackList="item.trackList"
  33. :specialTagList="item.tagList"
  34. :sliceImageWidth="item.sliceImageWidth"
  35. :sliceImageHeight="item.sliceImageHeight"
  36. :dx="item.dx"
  37. :dy="item.dy"
  38. @deleteSpecialtag="(tag) => deleteSpecialtag(item, tag)"
  39. />
  40. </div>
  41. <hr class="image-seperator" />
  42. </template>
  43. </div>
  44. <div v-else-if="store.isMultiMedia">
  45. <MultiMediaMarkBody />
  46. </div>
  47. <div v-else>未知数据</div>
  48. </div>
  49. </div>
  50. </template>
  51. <script setup lang="ts">
  52. import { onMounted, onUnmounted, reactive, watch, watchEffect } from "vue";
  53. import { store } from "@/store/store";
  54. import MarkDrawTrack from "./MarkDrawTrack.vue";
  55. import type { SliceImage } from "@/types";
  56. import { useTimers } from "@/setups/useTimers";
  57. import {
  58. getDataUrlForSliceConfig,
  59. getDataUrlForSplitConfig,
  60. loadImage,
  61. } from "@/utils/utils";
  62. import { dragImage } from "./use/draggable";
  63. import MultiMediaMarkBody from "./MultiMediaMarkBody.vue";
  64. import "viewerjs/dist/viewer.css";
  65. import Viewer from "viewerjs";
  66. import { message } from "ant-design-vue";
  67. import EventBus from "@/plugins/eventBus";
  68. type MakeTrack = (
  69. event: MouseEvent,
  70. item: SliceImage,
  71. maxSliceWidth: number,
  72. theFinalHeight: number
  73. ) => void | (() => void);
  74. const {
  75. hasMarkResultToRender = false,
  76. makeTrack = () => console.debug("非评卷界面makeTrack没有意义"),
  77. } = defineProps<{
  78. hasMarkResultToRender?: boolean;
  79. makeTrack?: MakeTrack;
  80. }>();
  81. const emit = defineEmits(["error"]);
  82. //#region : 图片拖动。在轨迹模式下,仅当没有选择分数时可用。
  83. const { dragContainer } = dragImage();
  84. //#endregion : 图片拖动
  85. const { addTimeout } = useTimers();
  86. //#region : 缩略图定位
  87. watch(
  88. () => [store.minimapScrollToX, store.minimapScrollToY],
  89. () => {
  90. const container = document.querySelector<HTMLDivElement>(
  91. ".mark-body-container"
  92. );
  93. addTimeout(() => {
  94. if (
  95. container &&
  96. typeof store.minimapScrollToX === "number" &&
  97. typeof store.minimapScrollToY === "number"
  98. ) {
  99. const { scrollWidth, scrollHeight } = container;
  100. container.scrollTo({
  101. top: scrollHeight * store.minimapScrollToY,
  102. left: scrollWidth * store.minimapScrollToX,
  103. behavior: "smooth",
  104. });
  105. }
  106. }, 10);
  107. }
  108. );
  109. //#endregion : 缩略图定位
  110. //#region : 快捷键定位
  111. const scrollContainerByKey = (e: KeyboardEvent) => {
  112. const container = document.querySelector<HTMLDivElement>(
  113. ".mark-body-container"
  114. );
  115. if (!container) {
  116. return;
  117. }
  118. if (e.key === "w") {
  119. container.scrollBy({ top: -100, behavior: "smooth" });
  120. } else if (e.key === "s") {
  121. container.scrollBy({ top: 100, behavior: "smooth" });
  122. } else if (e.key === "a") {
  123. container.scrollBy({ left: -100, behavior: "smooth" });
  124. } else if (e.key === "d") {
  125. container.scrollBy({ left: 100, behavior: "smooth" });
  126. }
  127. };
  128. onMounted(() => {
  129. document.addEventListener("keypress", scrollContainerByKey);
  130. });
  131. onUnmounted(() => {
  132. document.removeEventListener("keypress", scrollContainerByKey);
  133. });
  134. //#endregion : 快捷键定位
  135. //#region : 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
  136. let rotateBoard = $ref(0);
  137. let sliceImagesWithTrackList: SliceImage[] = reactive([]);
  138. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  139. let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
  140. watch(
  141. () => sliceImagesWithTrackList,
  142. () => {
  143. EventBus.emit("draw-change", sliceImagesWithTrackList);
  144. },
  145. { deep: true }
  146. );
  147. async function processSliceConfig() {
  148. if (!store.currentTask) return;
  149. let markResult = store.currentTask.markResult;
  150. if (hasMarkResultToRender) {
  151. // check if have MarkResult for currentTask
  152. if (!markResult) return;
  153. }
  154. const images = [];
  155. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  156. // 错误的搞法,张莹坚持要用
  157. const sliceNum = store.currentTask.sliceUrls.length;
  158. if (store.currentTask.sliceConfig.some((v) => v.i > sliceNum)) {
  159. console.warn("裁切图设置的数量小于该学生的总图片数量");
  160. }
  161. store.currentTask.sliceConfig = store.currentTask.sliceConfig.filter(
  162. (v) => v.i <= sliceNum
  163. );
  164. for (const sliceConfig of store.currentTask.sliceConfig) {
  165. const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
  166. const image = await loadImage(url);
  167. images[sliceConfig.i] = image;
  168. const { x, y, w, h } = sliceConfig;
  169. x < 0 && (sliceConfig.x = 0);
  170. y < 0 && (sliceConfig.y = 0);
  171. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  172. // 选择整图时,w/h 为0
  173. sliceConfig.w = image.naturalWidth;
  174. sliceConfig.h = image.naturalHeight;
  175. }
  176. if (x <= 1 && y <= 1 && sliceConfig.w <= 1 && sliceConfig.h <= 1) {
  177. sliceConfig.x = image.naturalWidth * x;
  178. sliceConfig.y = image.naturalHeight * y;
  179. sliceConfig.w = image.naturalWidth * w;
  180. sliceConfig.h = image.naturalHeight * h;
  181. }
  182. }
  183. theFinalHeight = store.currentTask.sliceConfig
  184. .map((v) => v.h)
  185. .reduce((acc, v) => (acc += v));
  186. maxSliceWidth = Math.max(...store.currentTask.sliceConfig.map((v) => v.w));
  187. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  188. let accumTopHeight = 0;
  189. let accumBottomHeight = 0;
  190. const trackLists = hasMarkResultToRender
  191. ? markResult.trackList
  192. : store.currentTask.questionList.map((q) => q.trackList).flat();
  193. const tagLists = hasMarkResultToRender
  194. ? markResult.specialTagList ?? []
  195. : store.currentTask.specialTagList ?? [];
  196. const tempSliceImagesWithTrackList: Array<SliceImage> = [];
  197. for (const sliceConfig of store.currentTask.sliceConfig) {
  198. accumBottomHeight += sliceConfig.h;
  199. const url = store.currentTask.sliceUrls[sliceConfig.i - 1];
  200. const indexInSliceUrls = sliceConfig.i;
  201. const image = images[sliceConfig.i];
  202. const dataUrl = await getDataUrlForSliceConfig(
  203. image,
  204. sliceConfig,
  205. maxSliceWidth,
  206. url
  207. );
  208. const thisImageTrackList = trackLists.filter(
  209. (t) => t.offsetIndex === indexInSliceUrls
  210. );
  211. const thisImageTagList = tagLists.filter(
  212. (t) => t.offsetIndex === indexInSliceUrls
  213. );
  214. const sliceImageRendered = await loadImage(dataUrl);
  215. tempSliceImagesWithTrackList.push({
  216. url: dataUrl,
  217. indexInSliceUrls: sliceConfig.i,
  218. // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
  219. trackList: thisImageTrackList.filter(
  220. (t) =>
  221. t.positionY >= accumTopHeight / theFinalHeight &&
  222. t.positionY < accumBottomHeight / theFinalHeight
  223. ),
  224. tagList: thisImageTagList.filter(
  225. (t) =>
  226. t.positionY >= accumTopHeight / theFinalHeight &&
  227. t.positionY < accumBottomHeight / theFinalHeight
  228. ),
  229. // originalImageWidth: image.naturalWidth,
  230. // originalImageHeight: image.naturalHeight,
  231. sliceImageWidth: sliceImageRendered.naturalWidth,
  232. sliceImageHeight: sliceImageRendered.naturalHeight,
  233. dx: sliceConfig.x,
  234. dy: sliceConfig.y,
  235. accumTopHeight,
  236. effectiveWidth: sliceConfig.w,
  237. });
  238. accumTopHeight = accumBottomHeight;
  239. }
  240. // 测试是否所有的track和tag都在待渲染的tempSliceImagesWithTrackList中
  241. const numOfTrackAndTagInData = trackLists.length + tagLists.length;
  242. const numOfTrackAndTagInTempSlice = tempSliceImagesWithTrackList
  243. .map((v) => v.trackList.length + v.tagList.length)
  244. .reduce((p, c) => p + c);
  245. if (numOfTrackAndTagInData !== numOfTrackAndTagInTempSlice) {
  246. console.warn({ tagLists, trackLists, tempSliceImagesWithTrackList });
  247. void message.warn("渲染轨迹数量与实际数量不一致");
  248. }
  249. // console.log("render: ", store.currentTask.secretNumber);
  250. if (sliceImagesWithTrackList.length === 0) {
  251. // 初次渲染,不做动画
  252. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  253. // 没抽象好,这里不好做校验
  254. // const renderedTrackAndTagNumber = sliceImagesWithTrackList.map(s => s.trackList.length + s.tagList.length).reduce((p,c) => p+ c);
  255. // if(renderedTrackAndTagNumber === thisIma)
  256. } else {
  257. rotateBoard = 1;
  258. setTimeout(() => {
  259. sliceImagesWithTrackList.splice(0);
  260. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  261. setTimeout(() => {
  262. rotateBoard = 0;
  263. }, 300);
  264. }, 300);
  265. }
  266. }
  267. async function processSplitConfig() {
  268. if (!store.currentTask) return;
  269. let markResult = store.currentTask.markResult;
  270. if (hasMarkResultToRender) {
  271. // check if have MarkResult for currentTask
  272. if (!markResult) return;
  273. }
  274. const images = [];
  275. for (const url of store.currentTask.sliceUrls) {
  276. const image = await loadImage(url);
  277. images.push(image);
  278. }
  279. // 如果拒绝裁切,则保持整卷
  280. if (!store.setting.enableSplit) {
  281. store.setting.splitConfig = [0, 1];
  282. }
  283. // 裁切块,可能是一块,两块,三块... [start, width ...] => [0, 0.3] | [0, 0.55, 0.45, 0.55] | [0, 0.35, 0.33, 0.35, 0.66, 0.35]
  284. // 要转变为 [[0, 0.3]] | [[0, 0.55], [0.45, 0.55]] | [[0, 0.35], [0.33, 0.35], [0.66, 0.35]]
  285. const splitConfigPairs = store.setting.splitConfig.reduce<[number, number][]>(
  286. (a, v, index) => {
  287. // 偶数位组成数组的第一位,奇数位组成数组的第二位
  288. index % 2 === 0 ? a.push([v, -1]) : (a.at(-1)![1] = v);
  289. return a;
  290. },
  291. []
  292. );
  293. // 最大的 splitConfig 的宽度
  294. const maxSplitConfig = Math.max(
  295. ...store.setting.splitConfig.filter((v, i) => i % 2)
  296. );
  297. maxSliceWidth =
  298. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  299. theFinalHeight =
  300. splitConfigPairs.length *
  301. images.reduce((acc, v) => (acc += v.naturalHeight), 0);
  302. // 高度比宽度大的图片不裁切
  303. const imagesOfBiggerHeight = images.filter(
  304. (v) => v.naturalHeight > v.naturalWidth
  305. );
  306. if (imagesOfBiggerHeight.length > 0) {
  307. maxSliceWidth = Math.max(
  308. maxSliceWidth,
  309. ...imagesOfBiggerHeight.map((v) => v.naturalWidth)
  310. );
  311. // 不裁切的图剪切多加的高度
  312. theFinalHeight -=
  313. imagesOfBiggerHeight.map((v) => v.naturalHeight).reduce((p, c) => p + c) *
  314. (splitConfigPairs.length - 1);
  315. }
  316. let accumTopHeight = 0;
  317. let accumBottomHeight = 0;
  318. const tempSliceImagesWithTrackList: SliceImage[] = [];
  319. const trackLists = hasMarkResultToRender
  320. ? markResult.trackList
  321. : (store.currentTask.questionList || []).map((q) => q.trackList).flat();
  322. const tagLists = hasMarkResultToRender
  323. ? markResult.specialTagList ?? []
  324. : store.currentTask.specialTagList ?? [];
  325. for (const url of store.currentTask.sliceUrls) {
  326. for (const config of splitConfigPairs) {
  327. const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
  328. const image = images[indexInSliceUrls - 1];
  329. let shouldBreak = false;
  330. let [splitConfigStart, splitConfigEnd] = config;
  331. if (image.naturalHeight > image.naturalWidth) {
  332. splitConfigStart = 0;
  333. splitConfigEnd = 1;
  334. shouldBreak = true;
  335. }
  336. accumBottomHeight += image.naturalHeight;
  337. const dataUrl = await getDataUrlForSplitConfig(
  338. image,
  339. [splitConfigStart, splitConfigEnd],
  340. maxSliceWidth,
  341. url
  342. );
  343. const thisImageTrackList = trackLists.filter(
  344. (t) => t.offsetIndex === indexInSliceUrls
  345. );
  346. const thisImageTagList = tagLists.filter(
  347. (t) => t.offsetIndex === indexInSliceUrls
  348. );
  349. const sliceImageRendered = await loadImage(dataUrl);
  350. tempSliceImagesWithTrackList.push({
  351. url: dataUrl,
  352. indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
  353. trackList: thisImageTrackList.filter(
  354. (t) =>
  355. t.positionY >= accumTopHeight / theFinalHeight &&
  356. t.positionY < accumBottomHeight / theFinalHeight
  357. ),
  358. tagList: thisImageTagList.filter(
  359. (t) =>
  360. t.positionY >= accumTopHeight / theFinalHeight &&
  361. t.positionY < accumBottomHeight / theFinalHeight
  362. ),
  363. // originalImageWidth: image.naturalWidth,
  364. // originalImageHeight: image.naturalHeight,
  365. sliceImageWidth: sliceImageRendered.naturalWidth,
  366. sliceImageHeight: sliceImageRendered.naturalHeight,
  367. dx: image.naturalWidth * splitConfigStart,
  368. dy: 0,
  369. accumTopHeight,
  370. effectiveWidth: image.naturalWidth * splitConfigEnd,
  371. });
  372. accumTopHeight = accumBottomHeight;
  373. // 如果本图高比宽大,不该裁切,则跳过多次裁切
  374. if (shouldBreak) {
  375. break;
  376. }
  377. }
  378. }
  379. // 测试是否所有的track和tag都在待渲染的tempSliceImagesWithTrackList中
  380. const numOfTrackAndTagInData = trackLists.length + tagLists.length;
  381. const numOfTrackAndTagInTempSlice = tempSliceImagesWithTrackList
  382. .map((v) => v.trackList.length + v.tagList.length)
  383. .reduce((p, c) => p + c);
  384. if (numOfTrackAndTagInData !== numOfTrackAndTagInTempSlice) {
  385. console.warn({ tagLists, trackLists, tempSliceImagesWithTrackList });
  386. void message.warn("渲染轨迹数量与实际数量不一致");
  387. }
  388. rotateBoard = 1;
  389. addTimeout(() => {
  390. sliceImagesWithTrackList.splice(0);
  391. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  392. addTimeout(() => {
  393. rotateBoard = 0;
  394. }, 300);
  395. }, 300);
  396. }
  397. const deleteSpecialtag = (item, tag) => {
  398. const findInd = (tagList, curTag) => {
  399. return tagList.findIndex(
  400. (itemTag) =>
  401. itemTag.tagName === curTag.tagName &&
  402. itemTag.offsetX === curTag.offsetX &&
  403. itemTag.offsetY === curTag.offsetY
  404. );
  405. };
  406. const tagIndex = findInd(item.tagList, tag);
  407. if (tagIndex === -1) return;
  408. item.tagList.splice(tagIndex, 1);
  409. const stagIndex = findInd(
  410. store.currentTaskEnsured.markResult.specialTagList,
  411. tag
  412. );
  413. if (stagIndex === -1) return;
  414. store.currentTaskEnsured.markResult.specialTagList.splice(tagIndex, 1);
  415. };
  416. // should not render twice at the same time
  417. let renderLock = false;
  418. const renderPaperAndMark = async () => {
  419. // console.log("renderPagerAndMark=>store.curTask:", store.currentTask);
  420. if (!store.currentTask) return;
  421. if (!store.isScanImage) return;
  422. if (renderLock) {
  423. console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
  424. await new Promise((res) => setTimeout(res, 1000));
  425. await renderPaperAndMark();
  426. return;
  427. }
  428. // check if have MarkResult for currentTask
  429. let markResult = store.currentTask.markResult;
  430. if (hasMarkResultToRender && !markResult) {
  431. return;
  432. }
  433. renderLock = true;
  434. try {
  435. store.globalMask = true;
  436. const hasSliceConfig = store.currentTask.sliceConfig?.length;
  437. if (hasSliceConfig) {
  438. await processSliceConfig();
  439. } else {
  440. await processSplitConfig();
  441. }
  442. // 研究生考试需要停留在上次阅卷的位置,所以注释掉下面的代码
  443. // await new Promise((res) => setTimeout(res, 700));
  444. // const container = <HTMLDivElement>(
  445. // document.querySelector(".mark-body-container")
  446. // );
  447. // addTimeout(() => {
  448. // container?.scrollTo({
  449. // top: 0,
  450. // left: 0,
  451. // behavior: "smooth",
  452. // });
  453. // }, 10);
  454. } catch (error) {
  455. sliceImagesWithTrackList.splice(0);
  456. console.trace("render error ", error);
  457. // 图片加载出错,自动加载下一个任务
  458. emit("error");
  459. } finally {
  460. renderLock = false;
  461. store.globalMask = false;
  462. }
  463. };
  464. // watchEffect(renderPaperAndMark);
  465. // 在阻止渲染的情况下,watchEffect收集不到 store.currentTask 的依赖,会导致本组件不再更新
  466. watch(
  467. () => store.currentTask,
  468. () => {
  469. setTimeout(renderPaperAndMark, 50);
  470. }
  471. );
  472. //#endregion : 计算裁切图和裁切图上的分数轨迹和特殊标记轨迹
  473. //#region : 放大缩小和之后的滚动
  474. const answerPaperScale = $computed(() => {
  475. // 放大、缩小不影响页面之前的滚动条定位
  476. let percentWidth = 0;
  477. let percentTop = 0;
  478. const container = document.querySelector(".mark-body-container");
  479. if (container) {
  480. const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
  481. percentWidth = scrollLeft / scrollWidth;
  482. percentTop = scrollTop / scrollHeight;
  483. }
  484. addTimeout(() => {
  485. if (container) {
  486. const { scrollWidth, scrollHeight } = container;
  487. container.scrollTo({
  488. left: scrollWidth * percentWidth,
  489. top: scrollHeight * percentTop,
  490. });
  491. }
  492. }, 10);
  493. const scale = store.setting.uiSetting["answer.paper.scale"];
  494. return scale * 100 + "%";
  495. });
  496. //#endregion : 放大缩小和之后的滚动
  497. //#region : 显示评分状态和清除轨迹
  498. let markStatus = $ref("");
  499. if (hasMarkResultToRender) {
  500. watch(
  501. () => store.currentTask,
  502. () => {
  503. markStatus = store.getMarkStatus;
  504. }
  505. );
  506. // 清除分数轨迹
  507. watchEffect(() => {
  508. for (const track of store.removeScoreTracks) {
  509. for (const sliceImage of sliceImagesWithTrackList) {
  510. sliceImage.trackList = sliceImage.trackList.filter(
  511. (t) =>
  512. !(
  513. t.mainNumber === track.mainNumber &&
  514. t.subNumber === track.subNumber &&
  515. t.number === track.number
  516. )
  517. );
  518. }
  519. }
  520. // 清除后,删除,否则会影响下次切换
  521. store.removeScoreTracks.splice(0);
  522. });
  523. // 清除特殊标记轨迹
  524. watchEffect(() => {
  525. if (!store.currentTask) return;
  526. for (const sliceImage of sliceImagesWithTrackList) {
  527. sliceImage.tagList = sliceImage.tagList.filter((t) =>
  528. store.currentTaskEnsured.markResult?.specialTagList.find(
  529. (st) =>
  530. st.offsetIndex === t.offsetIndex &&
  531. st.offsetX === t.offsetX &&
  532. st.offsetY === t.offsetY
  533. )
  534. );
  535. }
  536. if (store.currentTaskEnsured.markResult?.specialTagList.length === 0) {
  537. for (const sliceImage of sliceImagesWithTrackList) {
  538. sliceImage.tagList = [];
  539. }
  540. }
  541. });
  542. }
  543. //#endregion : 显示评分状态和清除轨迹
  544. //#region : 评分
  545. const innerMakeTrack = (event: MouseEvent, item: SliceImage) => {
  546. makeTrack(event, item, maxSliceWidth, theFinalHeight);
  547. };
  548. //#endregion : 评分
  549. //#region : 显示大图,供查看和翻转
  550. const showBigImage = (event: MouseEvent) => {
  551. event.preventDefault();
  552. // console.log(event);
  553. let viewer: Viewer = null as unknown as Viewer;
  554. viewer && viewer.destroy();
  555. viewer = new Viewer(event.target as HTMLElement, {
  556. // inline: true,
  557. viewed() {
  558. viewer.zoomTo(1);
  559. },
  560. hidden() {
  561. viewer.destroy();
  562. },
  563. zIndex: 1000000,
  564. });
  565. viewer.show();
  566. };
  567. //#endregion : 显示大图,供查看和翻转
  568. // onRenderTriggered(({ key, target, type }) => {
  569. // console.log({ key, target, type });
  570. // });
  571. let topKB = $ref(10);
  572. const topKBStyle = $computed(() => topKB + "%");
  573. let leftKB = $ref(10);
  574. const leftKBStyle = $computed(() => leftKB + "%");
  575. function moveCicle(event: KeyboardEvent) {
  576. // query mark-body-container and body to calc max/min topKB and max leftKB
  577. if (event.key === "k") {
  578. if (topKB > 1) topKB--;
  579. }
  580. if (event.key === "j") {
  581. if (topKB < 99) topKB++;
  582. }
  583. if (event.key === "h") {
  584. if (leftKB > 1) leftKB--;
  585. }
  586. if (event.key === "l") {
  587. if (leftKB < 99) leftKB++;
  588. }
  589. if (event.key === "c") {
  590. topKB = 50;
  591. leftKB = 50;
  592. }
  593. }
  594. function giveScoreCicle(event: KeyboardEvent) {
  595. // console.log(event.key);
  596. event.preventDefault();
  597. // console.log(store.currentScore);
  598. // 接收currentScore间隔时间外才会进入此事件
  599. if (event.key === " " && typeof store.currentScore === "number") {
  600. // topKB--;
  601. const circleElement = document.querySelector(".kb-circle");
  602. let { top, left } = circleElement.getBoundingClientRect();
  603. top = top + 45;
  604. left = left + 45;
  605. // console.log(top, left);
  606. // getBoundingClientRect().top left
  607. // capture => to the specific image
  608. const me = new MouseEvent("click", {
  609. bubbles: true,
  610. cancelable: true,
  611. view: window,
  612. clientY: top,
  613. clientX: left,
  614. });
  615. const eles = document.elementsFromPoint(left, top);
  616. // console.log(eles);
  617. let ele: Element;
  618. // if (eles[0].className === "kb-circle") {
  619. // if (eles[1].tagName == "IMG") {
  620. // ele = eles[1];
  621. // }
  622. // } else
  623. if (eles[0].tagName == "IMG") {
  624. ele = eles[0];
  625. }
  626. if (ele) {
  627. ele.dispatchEvent(me);
  628. }
  629. }
  630. }
  631. onMounted(() => {
  632. document.addEventListener("keypress", moveCicle);
  633. document.addEventListener("keypress", giveScoreCicle);
  634. });
  635. onUnmounted(() => {
  636. document.removeEventListener("keypress", moveCicle);
  637. document.removeEventListener("keypress", giveScoreCicle);
  638. });
  639. // setInterval(() => {
  640. // _topKB++;
  641. // console.log(topKB);
  642. // }, 1000);
  643. //#region autoScroll自动跳转
  644. let oldFirstScoreContainer: HTMLDivElement | null;
  645. watch(
  646. () => store.currentTask,
  647. () => {
  648. if (store.setting.autoScroll) {
  649. // 给任务清理和动画留一点时间
  650. oldFirstScoreContainer =
  651. document.querySelector<HTMLDivElement>(".score-container");
  652. oldFirstScoreContainer?.scrollIntoView({ behavior: "smooth" });
  653. addTimeout(scrollToFirstScore, 1000);
  654. } else {
  655. const container = document.querySelector<HTMLDivElement>(
  656. ".mark-body-container"
  657. );
  658. container?.scrollTo({ top: 0, left: 0, behavior: "smooth" });
  659. }
  660. }
  661. );
  662. function scrollToFirstScore() {
  663. if (renderLock) {
  664. window.requestAnimationFrame(scrollToFirstScore);
  665. }
  666. addTimeout(() => {
  667. const firstScore =
  668. document.querySelector<HTMLDivElement>(".score-container");
  669. firstScore?.scrollIntoView({ behavior: "smooth" });
  670. }, 1000);
  671. }
  672. //#endregion
  673. </script>
  674. <style scoped>
  675. .double-triangle {
  676. background-color: #ef7c78;
  677. width: 30px;
  678. height: 6px;
  679. clip-path: polygon(0 0, 0 6px, 50% 0, 100% 0, 100% 6px, 50% 0);
  680. position: absolute;
  681. bottom: -5px;
  682. }
  683. @keyframes rotate {
  684. 0% {
  685. transform: rotateY(0deg);
  686. opacity: 1;
  687. }
  688. 50% {
  689. transform: rotateY(90deg);
  690. }
  691. 100% {
  692. transform: rotateY(0deg);
  693. opacity: 0;
  694. }
  695. }
  696. .kb-circle {
  697. position: fixed;
  698. width: 90px;
  699. height: 90px;
  700. border: 1px solid #ff5050;
  701. border-radius: 50%;
  702. /* margin-left: -45px;
  703. margin-top: -45px; */
  704. /* transform: translate(-50%, -50%); */
  705. /* margin-left: 50%;
  706. margin-top: 10%; */
  707. top: v-bind(topKBStyle);
  708. left: v-bind(leftKBStyle);
  709. /* c to center circle
  710. jikl
  711. 斜线移动
  712. space上分,方便连续给分
  713. shift加速?
  714. */
  715. /*
  716. getBoundingClientRect().top left
  717. capture => to the specific image
  718. new MouseEvent("click", {
  719. bubbles: true,
  720. cancelable: true,
  721. view: window,
  722. clientX
  723. clientY
  724. });
  725. */
  726. /* to click through div */
  727. pointer-events: none;
  728. /* display: grid; */
  729. }
  730. .kb-circle .text {
  731. font-size: 2rem;
  732. color: #ff5050;
  733. margin-top: 10px;
  734. display: block;
  735. text-align: center;
  736. width: 100%;
  737. line-height: 90px;
  738. }
  739. </style>