index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <template>
  2. <div ref="elRef" class="scan-image">
  3. <div
  4. class="img-body"
  5. :style="imageStyle"
  6. v-ele-move-directive.prevent.stop="{
  7. moveElement: onMoveImg,
  8. emitOriginLeftTop: true,
  9. }"
  10. >
  11. <img
  12. ref="imgRef"
  13. v-if="curPage"
  14. :src="getFileUrl(curPage.sheetUri)"
  15. alt="原图"
  16. @load="initImageSize"
  17. />
  18. <div class="img-recogs">
  19. <div
  20. v-for="(item, index) in recogBlocks"
  21. :key="index"
  22. :class="[
  23. 'recog-block',
  24. { 'is-active': curRecogBlock?.index === item.index },
  25. ]"
  26. :style="item.fillAreaStyle"
  27. @click="onAreaClick(item)"
  28. >
  29. <div
  30. v-for="(option, oindex) in item.fillOptionStyles"
  31. :key="oindex"
  32. :style="option"
  33. class="recog-item"
  34. ></div>
  35. </div>
  36. </div>
  37. </div>
  38. <div class="img-guide">
  39. <div class="img-guide-icon is-left" @click="onPrev"><LeftOutlined /></div>
  40. <div class="img-guide-icon is-right" @click="onNext">
  41. <RightOutlined />
  42. </div>
  43. </div>
  44. <div class="img-actions">
  45. <ul>
  46. <li @click="onZoomIn"><ZoomInOutlined /></li>
  47. <li @click="onZoomOut"><ZoomOutOutlined /></li>
  48. <li @click="onZoomNormal">1:1</li>
  49. <li @click="onSetRecogStyle"><BgColorsOutlined /></li>
  50. </ul>
  51. </div>
  52. <import-btn
  53. v-if="!cantChangeImg"
  54. upload-url="/api/admin/scan/answer/sheet/update"
  55. :format="['jpg', 'png', 'jpeg']"
  56. :upload-data="updateSheetData"
  57. @upload-success="updateSheetSuccess"
  58. :min-size="3 * 1024"
  59. >
  60. <a-tooltip placement="top">
  61. <template #title>
  62. <span>替换图片</span>
  63. </template>
  64. <a-button class="img-change">
  65. <template #icon><PictureFilled /></template>
  66. </a-button>
  67. </a-tooltip>
  68. </import-btn>
  69. </div>
  70. <!-- FillAreaSetDialog -->
  71. <FillAreaSetDialog ref="fillAreaSetDialogRef" @modified="parseRecogBlocks" />
  72. <!-- RecogEditDialog -->
  73. <RecogEditDialog
  74. v-if="curRecogBlock"
  75. ref="recogEditDialogRef"
  76. :recog-data="curRecogBlock"
  77. @confirm="onRecogEditConfirm"
  78. @close="clearCurBlock"
  79. />
  80. </template>
  81. <script setup lang="ts">
  82. import {
  83. ZoomInOutlined,
  84. ZoomOutOutlined,
  85. BgColorsOutlined,
  86. LeftOutlined,
  87. RightOutlined,
  88. PictureFilled,
  89. } from "@ant-design/icons-vue";
  90. import { message } from "ant-design-vue";
  91. import {
  92. saveTemporaryImgViewConfig,
  93. getTemporaryImgViewConfig,
  94. } from "@/utils/index";
  95. import { computed, nextTick, ref, unref, watch } from "vue";
  96. import { useRoute } from "vue-router";
  97. import {
  98. objAssign,
  99. getFileUrl,
  100. getSliceFileUrl,
  101. getBoxImageSize,
  102. } from "@/utils/tool";
  103. import { vEleMoveDirective } from "@/directives/eleMove";
  104. import {
  105. parseRecogData,
  106. parseDetailSize,
  107. RecognizeArea,
  108. RecogBlock,
  109. } from "@/utils/recog/recog";
  110. import { useUserStore, useDataCheckStore } from "@/store";
  111. import { abc } from "@/constants/enumerate";
  112. import FillAreaSetDialog from "./FillAreaSetDialog.vue";
  113. import RecogEditDialog from "./RecogEditDialog.vue";
  114. import ImportBtn from "@/components/ImportBtn/index.vue";
  115. import { debounce } from "lodash-es";
  116. defineOptions({
  117. name: "ScanImage",
  118. });
  119. const props = withDefaults(defineProps<{ cantChangeImg?: boolean }>(), {
  120. cantChangeImg: false,
  121. });
  122. const route = useRoute();
  123. const emit = defineEmits(["next", "prev"]);
  124. const userStore = useUserStore();
  125. const dataCheckStore = useDataCheckStore();
  126. const curPage = computed(() => dataCheckStore.curPage);
  127. const updateSheetData = computed(() => {
  128. if (!curPage.value) return {};
  129. return {
  130. paperId: curPage.value.paperId,
  131. pageIndex: curPage.value.pageIndex + 1,
  132. };
  133. });
  134. const elRef = ref();
  135. const imgRef = ref();
  136. const imageSize = ref({
  137. width: 0,
  138. height: 0,
  139. top: 0,
  140. left: 0,
  141. scale: 1,
  142. });
  143. const saveImageSizeToSession = debounce(() => {
  144. saveTemporaryImgViewConfig(route.path, imageSize.value);
  145. }, 500);
  146. watch(
  147. imageSize,
  148. () => {
  149. saveImageSizeToSession();
  150. },
  151. { deep: true }
  152. );
  153. const imageStyle = computed(() => {
  154. return {
  155. width: `${imageSize.value.width}px`,
  156. height: `${imageSize.value.height}px`,
  157. top: `${imageSize.value.top}px`,
  158. left: `${imageSize.value.left}px`,
  159. transform: `scale(${imageSize.value.scale})`,
  160. };
  161. });
  162. function initImageSize() {
  163. const imgDom = imgRef.value as HTMLImageElement;
  164. const elDom = elRef.value as HTMLDivElement;
  165. const imgSize = getBoxImageSize({
  166. box: {
  167. width: elDom.clientWidth,
  168. height: elDom.clientHeight,
  169. },
  170. img: {
  171. width: imgDom.naturalWidth,
  172. height: imgDom.naturalHeight,
  173. },
  174. rotate: 0,
  175. });
  176. imageSize.value =
  177. getTemporaryImgViewConfig(route.path) ||
  178. objAssign(imageSize.value, imgSize);
  179. nextTick(() => {
  180. updateRecogList();
  181. });
  182. }
  183. function getNumberResult(
  184. result: Array<string | boolean>,
  185. sources: Array<string | boolean>
  186. ) {
  187. const nResult: number[] = [];
  188. result.forEach((item) => {
  189. const index = sources.indexOf(item);
  190. nResult[index] = 1;
  191. });
  192. return Array.from(nResult).map((item) => item || 0);
  193. }
  194. // recog data
  195. const recogList = ref<RecognizeArea[]>([]);
  196. function updateRecogList() {
  197. recogList.value = [] as RecognizeArea[];
  198. if (!dataCheckStore.curPage) return;
  199. const regdata = parseRecogData(dataCheckStore.curPage.recogData);
  200. if (!regdata) return;
  201. let index = 0;
  202. const ABC = abc.split("");
  203. regdata.question.forEach((gGroup) => {
  204. gGroup.fill_result.forEach((qRecog) => {
  205. const result = dataCheckStore.curPage?.question?.result[index] || "";
  206. qRecog.index = ++index;
  207. const questionResult = result ? result.split("") : [];
  208. const recogItem = parseDetailSize(
  209. qRecog,
  210. "question",
  211. qRecog.index,
  212. getNumberResult(questionResult, ABC),
  213. result === "#"
  214. );
  215. recogList.value.push(recogItem);
  216. });
  217. });
  218. parseRecogBlocks();
  219. }
  220. // recogBlocks
  221. const recogBlocks = ref<RecogBlock[]>([]);
  222. const curRecogBlock = ref<RecogBlock | null>(null);
  223. function parseRecogBlocks() {
  224. const imgDom = imgRef.value as HTMLImageElement;
  225. const rate = imgDom.clientWidth / imgDom.naturalWidth;
  226. const { unfillColor, unfillShow, fillColor, fillShow, borderWidth } =
  227. userStore.recogFillSet;
  228. const curBorderWidth = Math.max(1, borderWidth * rate);
  229. recogBlocks.value = unref(recogList.value).map((item) => {
  230. const fillAreaStyle = {
  231. position: "absolute",
  232. left: `${item.fillArea.x * rate}px`,
  233. top: `${item.fillArea.y * rate}px`,
  234. width: `${item.fillArea.w * rate}px`,
  235. height: `${item.fillArea.h * rate}px`,
  236. zIndex: 9,
  237. };
  238. const fillOptionStyles = item.optionSizes
  239. .map((op) => {
  240. const opStyle = {
  241. position: "absolute",
  242. left: `${op.x * rate}px`,
  243. top: `${op.y * rate}px`,
  244. width: `${op.w * rate}px`,
  245. height: `${op.h * rate}px`,
  246. zIndex: 9,
  247. border: "",
  248. };
  249. if (op.filled && fillShow) {
  250. opStyle.border = `${curBorderWidth}px solid ${fillColor}`;
  251. return opStyle;
  252. }
  253. if (!op.filled && unfillShow) {
  254. opStyle.border = `${curBorderWidth}px solid ${unfillColor}`;
  255. return opStyle;
  256. }
  257. return opStyle;
  258. })
  259. .filter((item) => item);
  260. const nitem: RecogBlock = {
  261. ...item,
  262. fillAreaStyle,
  263. fillOptionStyles,
  264. areaImg: "",
  265. };
  266. return nitem;
  267. });
  268. }
  269. // area click
  270. const recogEditDialogRef = ref();
  271. async function onAreaClick(data: RecogBlock) {
  272. if (!curPage.value) return;
  273. curRecogBlock.value = data;
  274. curRecogBlock.value.areaImg = await getSliceFileUrl(
  275. curPage.value.sheetUri,
  276. data.fillArea
  277. );
  278. nextTick(() => {
  279. recogEditDialogRef.value?.open();
  280. });
  281. }
  282. async function onRecogEditConfirm(result: string[]) {
  283. if (!curRecogBlock.value || !dataCheckStore.curPage) return;
  284. const data = curRecogBlock.value;
  285. if (data.type === "question") {
  286. const index = data.index - 1;
  287. dataCheckStore.curPage.question.result.splice(index, 1, result.join(""));
  288. await dataCheckStore.updateField({
  289. field: "QUESTION",
  290. value: JSON.stringify(dataCheckStore.curPage.question),
  291. });
  292. curRecogBlock.value.result = result;
  293. }
  294. }
  295. function clearCurBlock() {
  296. curRecogBlock.value = null;
  297. }
  298. // img action
  299. function onZoomIn() {
  300. const scale = imageSize.value.scale;
  301. if (scale >= 2) return;
  302. imageSize.value.scale = Math.min(2, scale * 1.2);
  303. }
  304. function onZoomOut() {
  305. const scale = imageSize.value.scale;
  306. if (scale <= 1) return;
  307. imageSize.value.scale = Math.max(1, scale * 0.8);
  308. }
  309. function onZoomNormal() {
  310. initImageSize();
  311. imageSize.value.scale = 1;
  312. }
  313. interface PosSize {
  314. left: number;
  315. top: number;
  316. }
  317. function onMoveImg({ left, top }: PosSize) {
  318. imageSize.value.left = left;
  319. imageSize.value.top = top;
  320. }
  321. function onPrev() {
  322. emit("prev");
  323. }
  324. function onNext() {
  325. emit("next");
  326. }
  327. // change image
  328. function updateSheetSuccess(data: { uri: string }) {
  329. if (!curPage.value) return;
  330. dataCheckStore.modifySheetUri({
  331. paperIndex: curPage.value.paperIndex,
  332. pageIndex: curPage.value.pageIndex,
  333. uri: data.uri,
  334. });
  335. message.success("上传成功!");
  336. }
  337. // set recog style
  338. const fillAreaSetDialogRef = ref();
  339. function onSetRecogStyle() {
  340. fillAreaSetDialogRef.value?.open();
  341. }
  342. // 监听question.result,同步修改客观题的识别结果
  343. watch(
  344. () => dataCheckStore.curPage?.question?.result,
  345. (val) => {
  346. if (!val) return;
  347. updateRecogList();
  348. if (!curRecogBlock.value) return;
  349. const curBlock = curRecogBlock.value;
  350. const block = recogBlocks.value.find(
  351. (item) => item.type === curBlock.type && curBlock.index
  352. );
  353. if (!block) return;
  354. curRecogBlock.value = block;
  355. },
  356. {
  357. deep: true,
  358. }
  359. );
  360. </script>
  361. <style lang="less" scoped>
  362. .scan-image {
  363. overflow: hidden;
  364. position: relative;
  365. height: 100%;
  366. .img-guide {
  367. &-icon {
  368. position: absolute;
  369. top: 50%;
  370. width: 28px;
  371. height: 32px;
  372. margin-top: -16px;
  373. background: #ffffff;
  374. border-radius: 6px;
  375. border: 1px solid @border-color1;
  376. line-height: 32px;
  377. text-align: center;
  378. z-index: 9;
  379. cursor: pointer;
  380. &:hover {
  381. background-color: #e8f3ff;
  382. border-color: @brand-color;
  383. color: @brand-color;
  384. }
  385. &.is-left {
  386. left: 12px;
  387. }
  388. &.is-right {
  389. right: 12px;
  390. }
  391. }
  392. }
  393. .img-change {
  394. position: absolute;
  395. top: 12px;
  396. right: 12px;
  397. width: 32px;
  398. height: 32px;
  399. line-height: 32px;
  400. background: #e8f3ff;
  401. padding: 0;
  402. border-radius: 6px;
  403. border: 1px solid #bedaff;
  404. color: #4080ff;
  405. text-align: center;
  406. z-index: 9;
  407. &:hover {
  408. opacity: 0.8;
  409. }
  410. }
  411. .img-actions {
  412. position: absolute;
  413. bottom: 12px;
  414. right: 12px;
  415. background: rgba(89, 89, 89, 0.6);
  416. border-radius: 8px;
  417. padding: 4px 8px;
  418. z-index: 9;
  419. li {
  420. display: inline-block;
  421. vertical-align: middle;
  422. width: 26px;
  423. height: 26px;
  424. border-radius: 6px;
  425. line-height: 26px;
  426. text-align: center;
  427. color: #fff;
  428. font-size: 16px;
  429. cursor: pointer;
  430. &:not(:last-child) {
  431. margin-right: 4px;
  432. }
  433. &:hover {
  434. background: rgba(89, 89, 89, 0.6);
  435. }
  436. }
  437. }
  438. .img-body {
  439. position: absolute;
  440. z-index: 2;
  441. }
  442. .recog-block {
  443. cursor: pointer;
  444. &:hover {
  445. background-color: rgba(241, 214, 110, 0.3);
  446. }
  447. &.is-active {
  448. background-color: rgba(241, 214, 110, 0.3);
  449. &::after {
  450. content: "";
  451. display: block;
  452. position: absolute;
  453. z-index: 1;
  454. top: 0;
  455. left: 0;
  456. right: 0;
  457. bottom: 0;
  458. border: 1px dashed #000;
  459. }
  460. }
  461. }
  462. }
  463. </style>