MarkBody.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <template>
  2. <CommonMarkBody
  3. :useMarkResult="true"
  4. uniquePropName="libraryId"
  5. :store="store"
  6. :makeTrack="makeTrack"
  7. @error="$emit('error')"
  8. />
  9. <div class="cursor">
  10. <div class="cursor-border">
  11. <span class="text">{{
  12. store.currentSpecialTag || store.currentScore
  13. }}</span>
  14. </div>
  15. </div>
  16. </template>
  17. <script lang="ts">
  18. import {
  19. defineComponent,
  20. onMounted,
  21. onUnmounted,
  22. watch,
  23. watchEffect,
  24. } from "vue";
  25. import { store } from "./store";
  26. import MarkDrawTrack from "./MarkDrawTrack.vue";
  27. import { ModeEnum, SliceImage, SpecialTag, Track } from "@/types";
  28. import { useTimers } from "@/setups/useTimers";
  29. import { isNumber } from "lodash";
  30. // @ts-ignore
  31. import CustomCursor from "custom-cursor.js";
  32. import CommonMarkBody from "./CommonMarkBody.vue";
  33. export default defineComponent({
  34. name: "MarkBody",
  35. components: { MarkDrawTrack, CommonMarkBody },
  36. emits: ["error"],
  37. setup(props, { emit }) {
  38. const { addTimeout } = useTimers();
  39. watch(
  40. () => store.minimapScrollTo,
  41. () => {
  42. const container = document.querySelector(
  43. ".mark-body-container"
  44. ) as HTMLDivElement;
  45. addTimeout(() => {
  46. if (container) {
  47. const { scrollHeight } = container;
  48. container.scrollTo({
  49. top: scrollHeight * store.minimapScrollTo,
  50. });
  51. }
  52. }, 10);
  53. }
  54. );
  55. const makeScoreTrack = (
  56. event: MouseEvent,
  57. item: SliceImage,
  58. maxSliceWidth: number,
  59. theFinalHeight: number
  60. ) => {
  61. // console.log(item);
  62. if (!store.currentQuestion || typeof store.currentScore === "undefined")
  63. return;
  64. const target = event.target as HTMLImageElement;
  65. const track = {} as Track;
  66. track.mainNumber = store.currentQuestion?.mainNumber;
  67. track.subNumber = store.currentQuestion?.subNumber;
  68. track.score = store.currentScore;
  69. track.offsetIndex = item.indexInSliceUrls;
  70. track.offsetX = Math.round(
  71. event.offsetX * (target.naturalWidth / target.width) + item.dx
  72. );
  73. track.offsetY = Math.round(
  74. event.offsetY * (target.naturalHeight / target.height) + item.dy
  75. );
  76. track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
  77. track.positionY =
  78. (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
  79. if (track.offsetX > item.effectiveWidth + item.dx) {
  80. console.log("不在有效宽度内,轨迹不生效");
  81. return;
  82. }
  83. if (
  84. item.trackList.some((t) => {
  85. return (
  86. Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
  87. Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
  88. 500
  89. );
  90. })
  91. ) {
  92. console.log("两个轨迹相距过近");
  93. return;
  94. }
  95. // 是否保留当前的轨迹分
  96. const ifKeepScore =
  97. Math.round(
  98. store.currentQuestion.maxScore * 100 -
  99. (store.currentQuestion.score || 0) * 100 -
  100. store.currentScore * 2 * 100
  101. ) / 100;
  102. if (
  103. (ifKeepScore < 0 && store.currentScore > 0) ||
  104. Math.round(ifKeepScore * 100) %
  105. Math.round(store.currentQuestion.intervalScore * 100) !==
  106. 0
  107. ) {
  108. store.currentScore = undefined;
  109. }
  110. const markResult = store.currentMarkResult;
  111. if (markResult) {
  112. const maxNumber =
  113. markResult.trackList.length === 0
  114. ? 0
  115. : Math.max(...markResult.trackList.map((t) => t.number));
  116. track.number = maxNumber + 1;
  117. // console.log(
  118. // maxNumber,
  119. // track.number,
  120. // markResult.trackList.map((t) => t.number),
  121. // Math.max(...markResult.trackList.map((t) => t.number))
  122. // );
  123. markResult.trackList = [...markResult.trackList, track];
  124. }
  125. item.trackList.push(track);
  126. };
  127. const makeSpecialTagTrack = (
  128. event: MouseEvent,
  129. item: SliceImage,
  130. maxSliceWidth: number,
  131. theFinalHeight: number
  132. ) => {
  133. // console.log(item);
  134. if (!store.currentTask || typeof store.currentSpecialTag === "undefined")
  135. return;
  136. const target = event.target as HTMLImageElement;
  137. const track = {} as SpecialTag;
  138. track.tagName = store.currentSpecialTag;
  139. track.offsetIndex = item.indexInSliceUrls;
  140. track.offsetX = Math.round(
  141. event.offsetX * (target.naturalWidth / target.width) + item.dx
  142. );
  143. track.offsetY = Math.round(
  144. event.offsetY * (target.naturalHeight / target.height) + item.dy
  145. );
  146. track.positionX = (track.offsetX - item.dx) / maxSliceWidth;
  147. track.positionY =
  148. (track.offsetY - item.dy + item.accumTopHeight) / theFinalHeight;
  149. if (track.offsetX > item.effectiveWidth + item.dx) {
  150. console.log("不在有效宽度内,轨迹不生效");
  151. return;
  152. }
  153. if (
  154. item.tagList.some((t) => {
  155. return (
  156. Math.pow(Math.abs(t.offsetX - track.offsetX), 2) +
  157. Math.pow(Math.abs(t.offsetY - track.offsetY), 2) <
  158. 500
  159. );
  160. })
  161. ) {
  162. console.log("两个轨迹相距过近");
  163. return;
  164. }
  165. const markResult = store.currentMarkResult;
  166. if (markResult) {
  167. markResult.specialTagList.push(track);
  168. }
  169. item.tagList.push(track);
  170. };
  171. const makeTrack = (
  172. event: MouseEvent,
  173. item: SliceImage,
  174. maxSliceWidth: number,
  175. theFinalHeight: number
  176. ) => {
  177. if (
  178. store.setting.uiSetting["specialTag.modal"] &&
  179. store.currentSpecialTag
  180. ) {
  181. makeSpecialTagTrack(event, item, maxSliceWidth, theFinalHeight);
  182. } else {
  183. makeScoreTrack(event, item, maxSliceWidth, theFinalHeight);
  184. }
  185. };
  186. // 轨迹模式下,添加轨迹,更新分数
  187. watch(
  188. () => store.currentMarkResult?.trackList,
  189. () => {
  190. if (store.setting.mode !== ModeEnum.TRACK) return;
  191. const markResult = store.currentMarkResult;
  192. if (markResult) {
  193. const cq = store.currentQuestion;
  194. // 当无轨迹时,不更新;无轨迹时,将分数置null
  195. if (cq) {
  196. if (markResult.trackList.length > 0) {
  197. const cqTrackList = markResult.trackList.filter(
  198. (v) =>
  199. v.mainNumber === cq.mainNumber && v.subNumber === cq.subNumber
  200. );
  201. if (cqTrackList.length > 0) {
  202. cq.score =
  203. cqTrackList
  204. .map((v) => v.score)
  205. .reduce((acc, v) => (acc += Math.round(v * 100)), 0) / 100;
  206. } else {
  207. cq.score = null;
  208. }
  209. } else {
  210. // TODO: 不需要?如果此行代码生效,则无法清除最后一道题的分数 此时的场景是回评普通模式评的分,需要看见
  211. // cq.score = cq.__origScore;
  212. }
  213. }
  214. // renderPaperAndMark();
  215. }
  216. },
  217. { deep: true }
  218. );
  219. // question.score更新后,自动关联markResult.scoreList和markResult.markerScore
  220. watchEffect(() => {
  221. const markResult = store.currentMarkResult;
  222. if (markResult && store.currentTask) {
  223. const scoreList = store.currentTask.questionList.map((q) => q.score);
  224. markResult.scoreList = [...(scoreList as number[])];
  225. markResult.markerScore =
  226. (markResult.scoreList.filter((s) => isNumber(s)) as number[]).reduce(
  227. (acc, v) => (acc += Math.round(v * 100)),
  228. 0
  229. ) / 100;
  230. }
  231. });
  232. watch(
  233. () => store.setting.mode,
  234. () => {
  235. const shouldHide = store.setting.mode === ModeEnum.COMMON;
  236. if (shouldHide) {
  237. // console.log("hide cursor", theCursor);
  238. theCursor && theCursor.destroy();
  239. } else {
  240. if (document.querySelector(".cursor")) {
  241. // console.log("show cursor", theCursor);
  242. // theCursor && theCursor.enable();
  243. theCursor = new CustomCursor(".cursor", {
  244. focusElements: [
  245. {
  246. selector: ".mark-body-container",
  247. focusClass: "cursor--focused-view",
  248. },
  249. ],
  250. }).initialize();
  251. }
  252. }
  253. }
  254. );
  255. let theCursor = null as any;
  256. onMounted(() => {
  257. if (store.setting.mode === ModeEnum.TRACK) {
  258. theCursor = new CustomCursor(".cursor", {
  259. focusElements: [
  260. {
  261. selector: ".mark-body-container",
  262. focusClass: "cursor--focused-view",
  263. },
  264. ],
  265. }).initialize();
  266. }
  267. });
  268. onUnmounted(() => {
  269. theCursor && theCursor.destroy();
  270. });
  271. return {
  272. store,
  273. makeTrack,
  274. };
  275. },
  276. // renderTriggered({ key, target, type }) {
  277. // console.log({ key, target, type });
  278. // },
  279. });
  280. </script>
  281. <style scoped>
  282. .hide-cursor {
  283. display: none !important;
  284. }
  285. .cursor {
  286. color: #ff5050;
  287. display: none;
  288. pointer-events: none;
  289. -webkit-user-select: none;
  290. -moz-user-select: none;
  291. -ms-user-select: none;
  292. user-select: none;
  293. top: 0;
  294. left: 0;
  295. position: fixed;
  296. will-change: transform;
  297. z-index: 1000;
  298. }
  299. .cursor-border {
  300. position: absolute;
  301. box-sizing: border-box;
  302. align-items: center;
  303. border: 1px solid #ff5050;
  304. border-radius: 50%;
  305. display: flex;
  306. justify-content: center;
  307. height: 0px;
  308. width: 0px;
  309. left: 0;
  310. top: 0;
  311. transform: translate(-50%, -50%);
  312. transition: all 360ms cubic-bezier(0.23, 1, 0.32, 1);
  313. }
  314. .cursor.cursor--initialized {
  315. display: block;
  316. }
  317. .cursor .text {
  318. font-size: 2rem;
  319. opacity: 0;
  320. transition: opacity 80ms cubic-bezier(0.23, 1, 0.32, 1);
  321. }
  322. .cursor.cursor--off-screen {
  323. opacity: 0;
  324. }
  325. .cursor.cursor--focused .cursor-border,
  326. .cursor.cursor--focused-view .cursor-border {
  327. width: 90px;
  328. height: 90px;
  329. }
  330. .cursor.cursor--focused-view .text {
  331. opacity: 1;
  332. transition: opacity 360ms cubic-bezier(0.23, 1, 0.32, 1);
  333. }
  334. </style>