MarkBody.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <template>
  2. <div
  3. ref="dragContainer"
  4. class="mark-body-container tw-flex-auto tw-p-2 tw-pt-0"
  5. @scroll="viewScroll"
  6. >
  7. <div v-if="!store.currentTask" class="tw-text-center">
  8. {{ store.message }}
  9. </div>
  10. <div v-else :style="{ width: answerPaperScale }" class="tw-pt-2">
  11. <div
  12. v-for="(item, index) in sliceImagesWithTrackList"
  13. :key="index"
  14. class="single-image-container"
  15. :style="{
  16. width: item.width,
  17. }"
  18. >
  19. <img :src="item.url" draggable="false" />
  20. <MarkDrawTrack
  21. :trackList="item.trackList"
  22. :specialTagList="item.tagList"
  23. :sliceImageHeight="item.originalImageHeight"
  24. :sliceImageWidth="item.originalImageWidth"
  25. :dx="0"
  26. :dy="0"
  27. />
  28. <hr class="image-seperator" />
  29. </div>
  30. </div>
  31. <ZoomPaper v-if="store.isScanImage && sliceImagesWithTrackList.length" />
  32. </div>
  33. </template>
  34. <script setup lang="ts">
  35. import { reactive, watch } from "vue";
  36. import { store } from "@/store/store";
  37. import MarkDrawTrack from "@/features/mark/MarkDrawTrack.vue";
  38. import type { SpecialTag, Track, ColorMap } from "@/types";
  39. import { useTimers } from "@/setups/useTimers";
  40. import { loadImage, addHeaderTrackColorAttr } from "@/utils/utils";
  41. import { dragImage } from "@/features/mark/use/draggable";
  42. import ZoomPaper from "@/components/ZoomPaper.vue";
  43. interface SliceImage {
  44. url: string;
  45. trackList: Array<Track>;
  46. tagList: Array<SpecialTag>;
  47. originalImageWidth: number;
  48. originalImageHeight: number;
  49. width: string; // 图片在整个图片列表里面的宽度比例
  50. }
  51. const { origImageUrls = "sliceUrls" } = defineProps<{
  52. origImageUrls?: "sheetUrls" | "sliceUrls";
  53. }>();
  54. const emit = defineEmits(["error", "getIsMultComments", "getScrollStatus"]);
  55. const { dragContainer } = dragImage();
  56. const viewScroll = () => {
  57. if (
  58. dragContainer.value.scrollTop + dragContainer.value.offsetHeight + 50 >=
  59. dragContainer.value.scrollHeight
  60. ) {
  61. emit("getScrollStatus");
  62. }
  63. };
  64. const { addTimeout } = useTimers();
  65. let sliceImagesWithTrackList: SliceImage[] = reactive([]);
  66. let maxImageWidth = 0;
  67. function addTrackColorAttr(tList: Track[]): Track[] {
  68. let markerIds: (number | undefined)[] = tList
  69. .map((v) => v.markerId)
  70. .filter((x) => !!x);
  71. markerIds = Array.from(new Set(markerIds));
  72. // markerIds.sort();
  73. let colorMap: ColorMap = {};
  74. for (let i = 0; i < markerIds.length; i++) {
  75. const mId: any = markerIds[i];
  76. if (i == 0) {
  77. colorMap[mId + ""] = "#F53F3F";
  78. } else if (i == 1) {
  79. colorMap[mId + ""] = "#165DFF";
  80. } else if (i == 2) {
  81. colorMap[mId + ""] = "#FAAD14";
  82. } else if (i > 2) {
  83. colorMap[mId + ""] = "gray";
  84. }
  85. }
  86. if (Object.keys(colorMap).length > 1) {
  87. emit("getIsMultComments", true);
  88. }
  89. tList = tList.map((item: Track) => {
  90. item.color = colorMap[item.markerId + ""] || "gray";
  91. item.isByMultMark = markerIds.length > 1;
  92. return item;
  93. });
  94. return tList;
  95. }
  96. function addTagColorAttr(tList: SpecialTag[]): SpecialTag[] {
  97. let markerIds: (number | undefined)[] = tList
  98. .map((v) => v.markerId)
  99. .filter((x) => !!x);
  100. markerIds = Array.from(new Set(markerIds));
  101. // markerIds.sort();
  102. let colorMap: ColorMap = {};
  103. for (let i = 0; i < markerIds.length; i++) {
  104. const mId: any = markerIds[i];
  105. if (i == 0) {
  106. colorMap[mId + ""] = "#F53F3F";
  107. } else if (i == 1) {
  108. colorMap[mId + ""] = "#165DFF";
  109. } else if (i == 2) {
  110. colorMap[mId + ""] = "#FAAD14";
  111. } else if (i > 2) {
  112. colorMap[mId + ""] = "gray";
  113. }
  114. }
  115. tList = tList.map((item: SpecialTag) => {
  116. item.color = colorMap[item.markerId + ""] || "gray";
  117. item.isByMultMark = markerIds.length > 1;
  118. return item;
  119. });
  120. return tList;
  121. }
  122. async function processImage() {
  123. if (!store.currentTask) return;
  124. const images = [];
  125. const urls = store.currentTask[origImageUrls] || [];
  126. for (const url of urls) {
  127. const image = await loadImage(url);
  128. images.push(image);
  129. }
  130. maxImageWidth = Math.max(...images.map((i) => i.naturalWidth));
  131. for (const url of urls) {
  132. const indexInSliceUrls = urls.indexOf(url) + 1;
  133. const image = images[indexInSliceUrls - 1];
  134. const trackLists = (store.currentTask.questionList || [])
  135. // .map((q) => q.trackList)
  136. .map((q) => {
  137. let tList = q.trackList;
  138. return q.headerTrack?.length
  139. ? addHeaderTrackColorAttr(q.headerTrack)
  140. : addTrackColorAttr(tList);
  141. })
  142. .flat();
  143. const thisImageTrackList = trackLists.filter(
  144. (t) => t.offsetIndex === indexInSliceUrls
  145. );
  146. const thisImageTagList = store.currentTask.headerTagList?.length
  147. ? addHeaderTrackColorAttr(
  148. (store.currentTask.headerTagList || []).filter(
  149. (t) => t.offsetIndex === indexInSliceUrls
  150. )
  151. )
  152. : addTagColorAttr(
  153. (store.currentTask.specialTagList || []).filter(
  154. (t) => t.offsetIndex === indexInSliceUrls
  155. )
  156. );
  157. sliceImagesWithTrackList.push({
  158. url,
  159. trackList: thisImageTrackList,
  160. tagList: thisImageTagList,
  161. originalImageWidth: image.naturalWidth,
  162. originalImageHeight: image.naturalHeight,
  163. width: (image.naturalWidth / maxImageWidth) * 100 + "%",
  164. });
  165. }
  166. }
  167. // should not render twice at the same time
  168. let renderLock = false;
  169. const renderPaperAndMark = async () => {
  170. if (renderLock) {
  171. console.log("上个任务还未渲染完毕,稍等一秒再尝试渲染");
  172. await new Promise((res) => setTimeout(res, 1000));
  173. await renderPaperAndMark();
  174. return;
  175. }
  176. renderLock = true;
  177. sliceImagesWithTrackList.splice(0);
  178. if (!store.currentTask) {
  179. renderLock = false;
  180. return;
  181. }
  182. try {
  183. store.globalMask = true;
  184. await processImage();
  185. } catch (error) {
  186. sliceImagesWithTrackList.splice(0);
  187. console.log("render error ", error);
  188. // 图片加载出错,自动加载下一个任务
  189. emit("error");
  190. } finally {
  191. await new Promise((res) => setTimeout(res, 500));
  192. store.globalMask = false;
  193. renderLock = false;
  194. }
  195. };
  196. watch(() => store.currentTask, renderPaperAndMark);
  197. watch(
  198. (): (number | undefined)[] => [
  199. store.minimapScrollToX,
  200. store.minimapScrollToY,
  201. ],
  202. () => {
  203. const container = document.querySelector<HTMLDivElement>(
  204. ".mark-body-container"
  205. );
  206. addTimeout(() => {
  207. if (
  208. container &&
  209. typeof store.minimapScrollToX === "number" &&
  210. typeof store.minimapScrollToY === "number"
  211. ) {
  212. const { scrollWidth, scrollHeight } = container;
  213. container.scrollTo({
  214. top: scrollHeight * store.minimapScrollToY,
  215. left: scrollWidth * store.minimapScrollToX,
  216. behavior: "smooth",
  217. });
  218. }
  219. }, 10);
  220. }
  221. );
  222. const answerPaperScale = $computed(() => {
  223. // 放大、缩小不影响页面之前的滚动条定位
  224. let percentWidth = 0;
  225. let percentTop = 0;
  226. const container = document.querySelector(".mark-body-container");
  227. if (container) {
  228. const { scrollLeft, scrollTop, scrollWidth, scrollHeight } = container;
  229. percentWidth = scrollLeft / scrollWidth;
  230. percentTop = scrollTop / scrollHeight;
  231. }
  232. addTimeout(() => {
  233. if (container) {
  234. const { scrollWidth, scrollHeight } = container;
  235. container.scrollTo({
  236. left: scrollWidth * percentWidth,
  237. top: scrollHeight * percentTop,
  238. });
  239. }
  240. }, 10);
  241. const scale = store.setting.uiSetting["answer.paper.scale"];
  242. return scale * 100 + "%";
  243. });
  244. </script>
  245. <style scoped>
  246. .mark-body-container {
  247. height: calc(100vh - 56px);
  248. overflow: auto;
  249. background-color: var(--app-container-bg-color);
  250. background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
  251. linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
  252. linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
  253. linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
  254. background-size: 20px 20px;
  255. background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
  256. transform: inherit;
  257. cursor: grab;
  258. user-select: none;
  259. }
  260. .mark-body-container img {
  261. width: 100%;
  262. }
  263. .single-image-container {
  264. position: relative;
  265. }
  266. .image-seperator {
  267. border: 2px solid rgba(120, 120, 120, 0.1);
  268. }
  269. </style>