|
@@ -0,0 +1,519 @@
|
|
|
+<template>
|
|
|
+ <div ref="elRef" class="scan-image">
|
|
|
+ <div
|
|
|
+ class="img-body"
|
|
|
+ :style="imageStyle"
|
|
|
+ v-ele-move-directive.prevent.stop="{
|
|
|
+ moveElement: onMoveImg,
|
|
|
+ emitOriginLeftTop: true,
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ ref="imgRef"
|
|
|
+ v-if="curPage"
|
|
|
+ :src="curPage.sheetUri"
|
|
|
+ alt="原图"
|
|
|
+ @load="initImageSize"
|
|
|
+ />
|
|
|
+ <div class="img-recogs">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in recogBlocks"
|
|
|
+ :key="index"
|
|
|
+ :class="[
|
|
|
+ 'recog-block',
|
|
|
+ {
|
|
|
+ 'is-active': curRecogBlock?.index === item.index,
|
|
|
+ },
|
|
|
+ ]"
|
|
|
+ :style="item.fillAreaStyle"
|
|
|
+ @click="onAreaClick(item)"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ v-for="(option, oindex) in item.fillOptionStyles"
|
|
|
+ :key="oindex"
|
|
|
+ :style="option"
|
|
|
+ class="recog-item"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="img-guide">
|
|
|
+ <div class="img-guide-icon is-left" @click="onPrev"><LeftOutlined /></div>
|
|
|
+ <div class="img-guide-icon is-right" @click="onNext">
|
|
|
+ <RightOutlined />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="img-actions">
|
|
|
+ <ul>
|
|
|
+ <li @click="onZoomIn"><ZoomInOutlined /></li>
|
|
|
+ <li @click="onZoomOut"><ZoomOutOutlined /></li>
|
|
|
+ <li @click="onZoomNormal">1:1</li>
|
|
|
+ <li @click="onSetRecogStyle"><BgColorsOutlined /></li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ <import-btn
|
|
|
+ v-if="!cantChangeImg"
|
|
|
+ upload-url="/api/admin/scan/answer/sheet/update"
|
|
|
+ :format="['jpg', 'png', 'jpeg']"
|
|
|
+ :upload-data="updateSheetData"
|
|
|
+ @upload-success="updateSheetSuccess"
|
|
|
+ :min-size="3 * 1024"
|
|
|
+ >
|
|
|
+ <a-tooltip placement="top">
|
|
|
+ <template #title>
|
|
|
+ <span>替换图片</span>
|
|
|
+ </template>
|
|
|
+ <a-button class="img-change">
|
|
|
+ <template #icon><PictureFilled /></template>
|
|
|
+ </a-button>
|
|
|
+ </a-tooltip>
|
|
|
+ </import-btn>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- FillAreaSetDialog -->
|
|
|
+ <FillAreaSetDialog ref="fillAreaSetDialogRef" @modified="parseRecogBlocks" />
|
|
|
+ <!-- RecogEditDialog -->
|
|
|
+ <RecogEditDialog
|
|
|
+ v-if="curRecogBlock"
|
|
|
+ ref="recogEditDialogRef"
|
|
|
+ :recog-data="curRecogBlock"
|
|
|
+ @confirm="onRecogEditConfirm"
|
|
|
+ @close="clearCurBlock"
|
|
|
+ />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import {
|
|
|
+ ZoomInOutlined,
|
|
|
+ ZoomOutOutlined,
|
|
|
+ BgColorsOutlined,
|
|
|
+ LeftOutlined,
|
|
|
+ RightOutlined,
|
|
|
+ PictureFilled,
|
|
|
+} from "@ant-design/icons-vue";
|
|
|
+import { message } from "ant-design-vue";
|
|
|
+import {
|
|
|
+ saveTemporaryImgViewConfig,
|
|
|
+ getTemporaryImgViewConfig,
|
|
|
+} from "@/utils/index";
|
|
|
+import { computed, nextTick, ref, unref, watch } from "vue";
|
|
|
+import { useRoute } from "vue-router";
|
|
|
+import { objAssign, getSliceFileUrl, getBoxImageSize } from "@/utils/tool";
|
|
|
+import { vEleMoveDirective } from "@/directives/eleMove";
|
|
|
+import {
|
|
|
+ parseRecogData,
|
|
|
+ parseDetailSize,
|
|
|
+ RecognizeArea,
|
|
|
+ RecogBlock,
|
|
|
+} from "@/utils/recog/recog";
|
|
|
+import { useUserStore, useDataCheckStore } from "@/store";
|
|
|
+import { abc } from "@/constants/enumerate";
|
|
|
+import useUpload from "../useUpload";
|
|
|
+
|
|
|
+import FillAreaSetDialog from "./FillAreaSetDialog.vue";
|
|
|
+import RecogEditDialog from "./RecogEditDialog.vue";
|
|
|
+import ImportBtn from "@/components/ImportBtn/index.vue";
|
|
|
+import { debounce } from "lodash-es";
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: "ScanImage",
|
|
|
+});
|
|
|
+
|
|
|
+const props = withDefaults(defineProps<{ cantChangeImg?: boolean }>(), {
|
|
|
+ cantChangeImg: false,
|
|
|
+});
|
|
|
+
|
|
|
+const route = useRoute();
|
|
|
+const emit = defineEmits(["next", "prev"]);
|
|
|
+
|
|
|
+const userStore = useUserStore();
|
|
|
+const dataCheckStore = useDataCheckStore();
|
|
|
+
|
|
|
+const { save } = useUpload();
|
|
|
+
|
|
|
+const curPage = computed(() => dataCheckStore.curPage);
|
|
|
+const updateSheetData = computed(() => {
|
|
|
+ if (!curPage.value) return {};
|
|
|
+
|
|
|
+ return {
|
|
|
+ paperId: curPage.value.paperId,
|
|
|
+ pageIndex: curPage.value.pageIndex + 1,
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+const elRef = ref();
|
|
|
+const imgRef = ref();
|
|
|
+const imageSize = ref({
|
|
|
+ width: 0,
|
|
|
+ height: 0,
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ scale: 1,
|
|
|
+});
|
|
|
+const saveImageSizeToSession = debounce(() => {
|
|
|
+ saveTemporaryImgViewConfig(route.path, imageSize.value);
|
|
|
+}, 500);
|
|
|
+watch(
|
|
|
+ imageSize,
|
|
|
+ () => {
|
|
|
+ saveImageSizeToSession();
|
|
|
+ },
|
|
|
+ { deep: true }
|
|
|
+);
|
|
|
+
|
|
|
+const imageStyle = computed(() => {
|
|
|
+ return {
|
|
|
+ width: `${imageSize.value.width}px`,
|
|
|
+ height: `${imageSize.value.height}px`,
|
|
|
+ top: `${imageSize.value.top}px`,
|
|
|
+ left: `${imageSize.value.left}px`,
|
|
|
+ transform: `scale(${imageSize.value.scale})`,
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+function initImageSize() {
|
|
|
+ const imgDom = imgRef.value as HTMLImageElement;
|
|
|
+ const elDom = elRef.value as HTMLDivElement;
|
|
|
+
|
|
|
+ const imgSize = getBoxImageSize({
|
|
|
+ box: {
|
|
|
+ width: elDom.clientWidth,
|
|
|
+ height: elDom.clientHeight,
|
|
|
+ },
|
|
|
+ img: {
|
|
|
+ width: imgDom.naturalWidth,
|
|
|
+ height: imgDom.naturalHeight,
|
|
|
+ },
|
|
|
+ rotate: 0,
|
|
|
+ });
|
|
|
+
|
|
|
+ imageSize.value =
|
|
|
+ getTemporaryImgViewConfig(route.path) ||
|
|
|
+ objAssign(imageSize.value, imgSize);
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ updateRecogList();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function getNumberResult(
|
|
|
+ result: Array<string | boolean>,
|
|
|
+ sources: Array<string | boolean>
|
|
|
+) {
|
|
|
+ const nResult: number[] = [];
|
|
|
+ result.forEach((item) => {
|
|
|
+ const index = sources.indexOf(item);
|
|
|
+ nResult[index] = 1;
|
|
|
+ });
|
|
|
+ return Array.from(nResult).map((item) => item || 0);
|
|
|
+}
|
|
|
+
|
|
|
+// recog data
|
|
|
+const recogList = ref<RecognizeArea[]>([]);
|
|
|
+function updateRecogList() {
|
|
|
+ recogList.value = [] as RecognizeArea[];
|
|
|
+
|
|
|
+ if (!dataCheckStore.curPage) return;
|
|
|
+ const regdata = dataCheckStore.curPage.recogData;
|
|
|
+ if (!regdata) return;
|
|
|
+
|
|
|
+ let index = 0;
|
|
|
+ const ABC = abc.split("");
|
|
|
+ regdata.question.forEach((gGroup) => {
|
|
|
+ gGroup.fill_result.forEach((qRecog) => {
|
|
|
+ const result = dataCheckStore.curPage?.question?.result[index] || "";
|
|
|
+ qRecog.index = ++index;
|
|
|
+
|
|
|
+ const questionResult = result ? result.split("") : [];
|
|
|
+
|
|
|
+ const recogItem = parseDetailSize(
|
|
|
+ qRecog,
|
|
|
+ "question",
|
|
|
+ qRecog.index,
|
|
|
+ getNumberResult(questionResult, ABC),
|
|
|
+ result === "#"
|
|
|
+ );
|
|
|
+ recogList.value.push(recogItem);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ parseRecogBlocks();
|
|
|
+}
|
|
|
+// recogBlocks
|
|
|
+const recogBlocks = ref<RecogBlock[]>([]);
|
|
|
+const curRecogBlock = ref<RecogBlock | null>(null);
|
|
|
+function parseRecogBlocks() {
|
|
|
+ const imgDom = imgRef.value as HTMLImageElement;
|
|
|
+ const rate = imgDom.clientWidth / imgDom.naturalWidth;
|
|
|
+
|
|
|
+ const { unfillColor, unfillShow, fillColor, fillShow, borderWidth } =
|
|
|
+ userStore.recogFillSet;
|
|
|
+ const curBorderWidth = Math.max(1, borderWidth * rate);
|
|
|
+
|
|
|
+ recogBlocks.value = unref(recogList.value).map((item) => {
|
|
|
+ const fillAreaStyle = {
|
|
|
+ position: "absolute",
|
|
|
+ left: `${item.fillArea.x * rate}px`,
|
|
|
+ top: `${item.fillArea.y * rate}px`,
|
|
|
+ width: `${item.fillArea.w * rate}px`,
|
|
|
+ height: `${item.fillArea.h * rate}px`,
|
|
|
+ zIndex: 9,
|
|
|
+ };
|
|
|
+ const fillOptionStyles = item.optionSizes
|
|
|
+ .map((op) => {
|
|
|
+ const opStyle = {
|
|
|
+ position: "absolute",
|
|
|
+ left: `${op.x * rate}px`,
|
|
|
+ top: `${op.y * rate}px`,
|
|
|
+ width: `${op.w * rate}px`,
|
|
|
+ height: `${op.h * rate}px`,
|
|
|
+ zIndex: 9,
|
|
|
+ border: "",
|
|
|
+ };
|
|
|
+
|
|
|
+ if (op.filled && fillShow) {
|
|
|
+ opStyle.border = `${curBorderWidth}px solid ${fillColor}`;
|
|
|
+ return opStyle;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!op.filled && unfillShow) {
|
|
|
+ opStyle.border = `${curBorderWidth}px solid ${unfillColor}`;
|
|
|
+ return opStyle;
|
|
|
+ }
|
|
|
+
|
|
|
+ return opStyle;
|
|
|
+ })
|
|
|
+ .filter((item) => item);
|
|
|
+
|
|
|
+ const nitem: RecogBlock = {
|
|
|
+ ...item,
|
|
|
+ fillAreaStyle,
|
|
|
+ fillOptionStyles,
|
|
|
+ areaImg: "",
|
|
|
+ };
|
|
|
+
|
|
|
+ return nitem;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// area click
|
|
|
+const recogEditDialogRef = ref();
|
|
|
+async function onAreaClick(data: RecogBlock) {
|
|
|
+ if (!curPage.value) return;
|
|
|
+ curRecogBlock.value = data;
|
|
|
+ curRecogBlock.value.areaImg = await getSliceFileUrl(
|
|
|
+ curPage.value.sheetUri,
|
|
|
+ data.fillArea
|
|
|
+ );
|
|
|
+ nextTick(() => {
|
|
|
+ recogEditDialogRef.value?.open();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function onRecogEditConfirm(result: string[]) {
|
|
|
+ if (!curRecogBlock.value || !dataCheckStore.curPage) return;
|
|
|
+
|
|
|
+ const data = curRecogBlock.value;
|
|
|
+
|
|
|
+ if (data.type === "question") {
|
|
|
+ const index = data.index - 1;
|
|
|
+ dataCheckStore.curPage.question.result.splice(index, 1, result.join(""));
|
|
|
+ curRecogBlock.value.result = result;
|
|
|
+ await save();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function clearCurBlock() {
|
|
|
+ curRecogBlock.value = null;
|
|
|
+}
|
|
|
+
|
|
|
+// img action
|
|
|
+function onZoomIn() {
|
|
|
+ const scale = imageSize.value.scale;
|
|
|
+ if (scale >= 2) return;
|
|
|
+
|
|
|
+ imageSize.value.scale = Math.min(2, scale * 1.2);
|
|
|
+}
|
|
|
+function onZoomOut() {
|
|
|
+ const scale = imageSize.value.scale;
|
|
|
+ if (scale <= 1) return;
|
|
|
+
|
|
|
+ imageSize.value.scale = Math.max(1, scale * 0.8);
|
|
|
+}
|
|
|
+function onZoomNormal() {
|
|
|
+ initImageSize();
|
|
|
+ imageSize.value.scale = 1;
|
|
|
+}
|
|
|
+
|
|
|
+interface PosSize {
|
|
|
+ left: number;
|
|
|
+ top: number;
|
|
|
+}
|
|
|
+function onMoveImg({ left, top }: PosSize) {
|
|
|
+ imageSize.value.left = left;
|
|
|
+ imageSize.value.top = top;
|
|
|
+}
|
|
|
+
|
|
|
+function onPrev() {
|
|
|
+ emit("prev");
|
|
|
+}
|
|
|
+function onNext() {
|
|
|
+ emit("next");
|
|
|
+}
|
|
|
+
|
|
|
+// change image
|
|
|
+function updateSheetSuccess(data: { uri: string }) {
|
|
|
+ if (!curPage.value) return;
|
|
|
+ dataCheckStore.modifySheetUri({
|
|
|
+ paperIndex: curPage.value.paperIndex,
|
|
|
+ pageIndex: curPage.value.pageIndex,
|
|
|
+ uri: data.uri,
|
|
|
+ });
|
|
|
+ message.success("上传成功!");
|
|
|
+}
|
|
|
+
|
|
|
+// set recog style
|
|
|
+const fillAreaSetDialogRef = ref();
|
|
|
+function onSetRecogStyle() {
|
|
|
+ fillAreaSetDialogRef.value?.open();
|
|
|
+}
|
|
|
+
|
|
|
+// 监听question.result,同步修改客观题的识别结果
|
|
|
+watch(
|
|
|
+ () => dataCheckStore.curPage?.question?.result,
|
|
|
+ (val) => {
|
|
|
+ if (!val) return;
|
|
|
+ updateRecogList();
|
|
|
+
|
|
|
+ if (!curRecogBlock.value) return;
|
|
|
+
|
|
|
+ const curBlock = curRecogBlock.value;
|
|
|
+ const block = recogBlocks.value.find(
|
|
|
+ (item) => item.type === curBlock.type && curBlock.index
|
|
|
+ );
|
|
|
+ if (!block) return;
|
|
|
+ curRecogBlock.value = block;
|
|
|
+ },
|
|
|
+ {
|
|
|
+ deep: true,
|
|
|
+ }
|
|
|
+);
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+.scan-image {
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+ height: 100%;
|
|
|
+
|
|
|
+ .img-guide {
|
|
|
+ &-icon {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ width: 28px;
|
|
|
+ height: 32px;
|
|
|
+ margin-top: -16px;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid @border-color1;
|
|
|
+ line-height: 32px;
|
|
|
+ text-align: center;
|
|
|
+ z-index: 9;
|
|
|
+ cursor: pointer;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background-color: #e8f3ff;
|
|
|
+ border-color: @brand-color;
|
|
|
+ color: @brand-color;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.is-left {
|
|
|
+ left: 12px;
|
|
|
+ }
|
|
|
+ &.is-right {
|
|
|
+ right: 12px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .img-change {
|
|
|
+ position: absolute;
|
|
|
+ top: 12px;
|
|
|
+ right: 12px;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ line-height: 32px;
|
|
|
+ background: #e8f3ff;
|
|
|
+ padding: 0;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid #bedaff;
|
|
|
+ color: #4080ff;
|
|
|
+ text-align: center;
|
|
|
+ z-index: 9;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ opacity: 0.8;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .img-actions {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 12px;
|
|
|
+ right: 12px;
|
|
|
+ background: rgba(89, 89, 89, 0.6);
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 4px 8px;
|
|
|
+ z-index: 9;
|
|
|
+
|
|
|
+ li {
|
|
|
+ display: inline-block;
|
|
|
+ vertical-align: middle;
|
|
|
+ width: 26px;
|
|
|
+ height: 26px;
|
|
|
+ border-radius: 6px;
|
|
|
+ line-height: 26px;
|
|
|
+ text-align: center;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 16px;
|
|
|
+ cursor: pointer;
|
|
|
+ &:not(:last-child) {
|
|
|
+ margin-right: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: rgba(89, 89, 89, 0.6);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .img-body {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ .recog-block {
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background-color: rgba(241, 214, 110, 0.3);
|
|
|
+ }
|
|
|
+ &.is-active {
|
|
|
+ background-color: rgba(241, 214, 110, 0.3);
|
|
|
+ &::after {
|
|
|
+ content: "";
|
|
|
+ display: block;
|
|
|
+ position: absolute;
|
|
|
+ z-index: 1;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ border: 1px dashed #000;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &.readonly {
|
|
|
+ pointer-events: none;
|
|
|
+ cursor: default !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|