MarkBody.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <template>
  2. <div class="mark-body-container tw-flex-auto tw-p-2" ref="dragContainer">
  3. <a-spin
  4. :spinning="rendering"
  5. size="large"
  6. tip="Loading..."
  7. style="margin-top: 50px"
  8. >
  9. <div v-if="!store.currentTask" class="tw-text-center">
  10. {{ store.message }}
  11. </div>
  12. <div v-else :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. :track-list="item.trackList"
  21. :special-tag-list="item.tagList"
  22. :original-image="item.originalImage"
  23. :slice-image="item.sliceImage"
  24. :dx="item.dx"
  25. :dy="item.dy"
  26. />
  27. <hr class="image-seperator" />
  28. </div>
  29. </div>
  30. </a-spin>
  31. </div>
  32. </template>
  33. <script lang="ts">
  34. import { computed, defineComponent, reactive, ref, watchEffect } from "vue";
  35. import { store } from "./store";
  36. import filters from "@/filters";
  37. import MarkDrawTrack from "./MarkDrawTrack.vue";
  38. import { SpecialTag, Track } from "@/types";
  39. import { useTimers } from "@/setups/useTimers";
  40. import {
  41. getDataUrlForSliceConfig,
  42. getDataUrlForSplitConfig,
  43. loadImage,
  44. } from "@/utils/utils";
  45. import { dragImage } from "@/features/mark/use/draggable";
  46. interface SliceImage {
  47. url: string;
  48. indexInSliceUrls: number;
  49. trackList: Array<Track>;
  50. tagList: Array<SpecialTag>;
  51. originalImage: HTMLImageElement;
  52. sliceImage: HTMLImageElement;
  53. dx: number;
  54. dy: number;
  55. accumTopHeight: number;
  56. effectiveWidth: number;
  57. }
  58. // should not render twice at the same time
  59. let __lock = false;
  60. let __currentStudentId = -1; // save __currentStudentIdof lock
  61. export default defineComponent({
  62. name: "MarkBody",
  63. components: { MarkDrawTrack },
  64. emits: ["error"],
  65. setup(props, { emit }) {
  66. const { dragContainer } = dragImage();
  67. const { addTimeout } = useTimers();
  68. function hasSliceConfig() {
  69. return store.currentTask?.sliceConfig?.length;
  70. }
  71. let rendering = ref(false);
  72. let sliceImagesWithTrackList: Array<SliceImage> = reactive([]);
  73. let maxSliceWidth = 0; // 最大的裁切块宽度,图片容器以此为准
  74. let theFinalHeight = 0; // 最终宽度,用来定位轨迹在第几张图片,不包括image-seperator高度
  75. async function getImageUsingDataUrl(
  76. dataUrl: string
  77. ): Promise<HTMLImageElement> {
  78. return new Promise((resolve) => {
  79. const image = new Image();
  80. image.src = dataUrl;
  81. image.onload = function () {
  82. resolve(image);
  83. };
  84. });
  85. }
  86. async function processSliceConfig() {
  87. if (!store.currentTask) return;
  88. const images = [];
  89. const urls = [];
  90. // 必须要先加载一遍,把“选择整图”的宽高重置后,再算总高度
  91. for (const sliceConfig of store.currentTask.sliceConfig) {
  92. const url = filters.toCompleteUrlWithFileServer(
  93. store.setting.fileServer,
  94. store.currentTask.sliceUrls[sliceConfig.i - 1]
  95. );
  96. const image = await loadImage(url);
  97. images.push(image);
  98. urls.push(url);
  99. if (sliceConfig.w === 0 && sliceConfig.h === 0) {
  100. // 选择整图时,w/h 为0
  101. sliceConfig.w = image.naturalWidth;
  102. sliceConfig.h = image.naturalHeight;
  103. }
  104. }
  105. theFinalHeight = store.currentTask.sliceConfig
  106. .map((v) => v.h)
  107. .reduce((acc, v) => (acc += v));
  108. maxSliceWidth = Math.max(
  109. ...store.currentTask.sliceConfig.map((v) => v.w)
  110. );
  111. // 用来保存sliceImage在整个图片容器中(不包括image-seperator)的高度范围
  112. let accumTopHeight = 0;
  113. let accumBottomHeight = 0;
  114. const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
  115. for (const sliceConfig of store.currentTask.sliceConfig) {
  116. accumBottomHeight += sliceConfig.h;
  117. const url = filters.toCompleteUrlWithFileServer(
  118. store.setting.fileServer,
  119. store.currentTask.sliceUrls[sliceConfig.i - 1]
  120. );
  121. const indexInSliceUrls = sliceConfig.i;
  122. const image = images[indexInSliceUrls - 1];
  123. const dataUrl = getDataUrlForSliceConfig(
  124. image,
  125. sliceConfig,
  126. maxSliceWidth,
  127. url
  128. );
  129. const trackLists = store.currentTask.questionList
  130. .map((q) => q.trackList)
  131. .reduce((acc, t) => {
  132. acc = acc.concat(t);
  133. return acc;
  134. }, [] as Array<Track>);
  135. const thisImageTrackList = trackLists.filter(
  136. (t) => t.offsetIndex === indexInSliceUrls
  137. );
  138. const thisImageTagList = (
  139. store.currentTask.specialTagList ?? []
  140. ).filter((t) => t.offsetIndex === indexInSliceUrls);
  141. const sliceImage = await getImageUsingDataUrl(dataUrl);
  142. tempSliceImagesWithTrackList.push({
  143. url: dataUrl,
  144. indexInSliceUrls,
  145. // 通过positionY来定位是第几张slice的还原,并过滤出相应的track
  146. trackList: thisImageTrackList.filter(
  147. (t) =>
  148. t.positionY >= accumTopHeight / theFinalHeight &&
  149. t.positionY < accumBottomHeight / theFinalHeight
  150. ),
  151. tagList: thisImageTagList.filter(
  152. (t) =>
  153. t.positionY >= accumTopHeight / theFinalHeight &&
  154. t.positionY < accumBottomHeight / theFinalHeight
  155. ),
  156. originalImage: image,
  157. sliceImage,
  158. dx: sliceConfig.x,
  159. dy: sliceConfig.y,
  160. accumTopHeight,
  161. effectiveWidth: sliceConfig.w,
  162. });
  163. accumTopHeight = accumBottomHeight;
  164. }
  165. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  166. }
  167. async function processSplitConfig() {
  168. if (!store.currentTask) return;
  169. const images = [];
  170. for (const url of store.currentTask.sliceUrls) {
  171. const image = await loadImage(
  172. filters.toCompleteUrlWithFileServer(store.setting.fileServer, url)
  173. );
  174. images.push(image);
  175. }
  176. const splitConfigPairs = store.setting.splitConfig
  177. .map((v, index, ary) => (index % 2 === 0 ? [v, ary[index + 1]] : false))
  178. .filter((v) => v) as unknown as Array<[number, number]>;
  179. const maxSplitConfig = Math.max(...store.setting.splitConfig);
  180. maxSliceWidth =
  181. Math.max(...images.map((v) => v.naturalWidth)) * maxSplitConfig;
  182. theFinalHeight =
  183. splitConfigPairs.length *
  184. images.reduce((acc, v) => (acc += v.naturalHeight), 0);
  185. let accumTopHeight = 0;
  186. let accumBottomHeight = 0;
  187. const tempSliceImagesWithTrackList = [] as Array<SliceImage>;
  188. for (const url of store.currentTask.sliceUrls) {
  189. for (const config of splitConfigPairs) {
  190. const indexInSliceUrls = store.currentTask.sliceUrls.indexOf(url) + 1;
  191. const image = images[indexInSliceUrls - 1];
  192. accumBottomHeight += image.naturalHeight;
  193. const dataUrl = getDataUrlForSplitConfig(
  194. image,
  195. config,
  196. maxSliceWidth,
  197. url
  198. );
  199. const trackLists = store.currentTask.questionList
  200. .map((q) => q.trackList)
  201. .reduce((acc, t) => {
  202. acc = acc.concat(t);
  203. return acc;
  204. }, [] as Array<Track>);
  205. const thisImageTrackList = trackLists.filter(
  206. (t) => t.offsetIndex === indexInSliceUrls
  207. );
  208. const thisImageTagList = (
  209. store.currentTask.specialTagList ?? []
  210. ).filter((t) => t.offsetIndex === indexInSliceUrls);
  211. const sliceImage = await getImageUsingDataUrl(dataUrl);
  212. tempSliceImagesWithTrackList.push({
  213. url: dataUrl,
  214. indexInSliceUrls: store.currentTask.sliceUrls.indexOf(url) + 1,
  215. trackList: thisImageTrackList.filter(
  216. (t) =>
  217. t.positionY >= accumTopHeight / theFinalHeight &&
  218. t.positionY < accumBottomHeight / theFinalHeight
  219. ),
  220. tagList: thisImageTagList.filter(
  221. (t) =>
  222. t.positionY >= accumTopHeight / theFinalHeight &&
  223. t.positionY < accumBottomHeight / theFinalHeight
  224. ),
  225. originalImage: image,
  226. sliceImage,
  227. dx: image.naturalWidth * config[0],
  228. dy: 0,
  229. accumTopHeight,
  230. effectiveWidth: image.naturalWidth * config[1],
  231. });
  232. accumTopHeight = accumBottomHeight;
  233. }
  234. }
  235. sliceImagesWithTrackList.push(...tempSliceImagesWithTrackList);
  236. }
  237. const renderPaperAndMark = async () => {
  238. if (__lock) {
  239. if (store.currentTask?.studentId === __currentStudentId) {
  240. console.log("重复渲染,返回");
  241. return;
  242. }
  243. console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
  244. await new Promise((res) => setTimeout(res, 1000));
  245. await renderPaperAndMark();
  246. return;
  247. }
  248. __lock = true;
  249. __currentStudentId = store.currentTask?.studentId ?? -1;
  250. sliceImagesWithTrackList.splice(0);
  251. if (!store.currentTask) {
  252. __lock = false;
  253. return;
  254. }
  255. try {
  256. rendering.value = true;
  257. if (hasSliceConfig()) {
  258. await processSliceConfig();
  259. } else {
  260. await processSplitConfig();
  261. }
  262. } catch (error) {
  263. sliceImagesWithTrackList.splice(0);
  264. console.log("render error ", error);
  265. // 图片加载出错,自动加载下一个任务
  266. emit("error");
  267. } finally {
  268. __lock = false;
  269. rendering.value = false;
  270. }
  271. };
  272. watchEffect(renderPaperAndMark);
  273. const answerPaperScale = computed(() => {
  274. // 放大、缩小不影响页面之前的滚动条定位
  275. let percentWidth = 0;
  276. let percentTop = 0;
  277. const container = document.querySelector(
  278. ".mark-body-container"
  279. ) as HTMLDivElement;
  280. if (container) {
  281. const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
  282. percentWidth = scrollLeft / scrollWidth;
  283. percentTop = scrollTop / scrollHeight;
  284. }
  285. addTimeout(() => {
  286. if (container) {
  287. const { scrollWidth, scrollHeight } = container;
  288. container.scrollTo({
  289. left: scrollWidth * percentWidth,
  290. top: scrollHeight * percentTop,
  291. });
  292. }
  293. }, 10);
  294. const scale = store.setting.uiSetting["answer.paper.scale"];
  295. return scale * 100 + "%";
  296. });
  297. return {
  298. dragContainer,
  299. store,
  300. rendering,
  301. sliceImagesWithTrackList,
  302. answerPaperScale,
  303. };
  304. },
  305. });
  306. </script>
  307. <style scoped>
  308. .mark-body-container {
  309. height: calc(100vh - 41px);
  310. overflow: scroll;
  311. background-size: 8px 8px;
  312. background-image: linear-gradient(to right, #e7e7e7 4px, transparent 4px),
  313. linear-gradient(to bottom, transparent 4px, #e7e7e7 4px);
  314. }
  315. .mark-body-container img {
  316. width: 100%;
  317. }
  318. .single-image-container {
  319. position: relative;
  320. }
  321. .image-seperator {
  322. border: 2px solid rgba(120, 120, 120, 0.1);
  323. }
  324. </style>