|
@@ -0,0 +1,360 @@
|
|
|
+<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" src="./data/paper.jpg" alt="p" @load="initImageSize" />
|
|
|
+ <div class="img-recogs">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in recogBlocks"
|
|
|
+ :key="index"
|
|
|
+ class="recog-block"
|
|
|
+ :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"><LeftOutlined /></div>
|
|
|
+ <div class="img-guide-icon is-right"><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>
|
|
|
+ <div class="img-change" @click="onChangeImage"><PictureFilled /></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- FillAreaSetDialog -->
|
|
|
+ <FillAreaSetDialog ref="fillAreaSetDialogRef" @modified="parseRecogBlocks" />
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import {
|
|
|
+ ZoomInOutlined,
|
|
|
+ ZoomOutOutlined,
|
|
|
+ BgColorsOutlined,
|
|
|
+ LeftOutlined,
|
|
|
+ RightOutlined,
|
|
|
+ PictureFilled,
|
|
|
+} from "@ant-design/icons-vue";
|
|
|
+import { computed, nextTick, ref } from "vue";
|
|
|
+import { objAssign } from "@/utils/tool";
|
|
|
+import { vEleMoveDirective } from "@/directives/eleMove";
|
|
|
+import { RecognizeArea } from "@/utils/recog/recog";
|
|
|
+import { useUserStore } from "@/store";
|
|
|
+
|
|
|
+import FillAreaSetDialog from "./FillAreaSetDialog.vue";
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: "ScanImage",
|
|
|
+});
|
|
|
+
|
|
|
+interface ImageRecogData extends RecognizeArea {
|
|
|
+ [k: string]: any;
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ imgSrc: string;
|
|
|
+ recogData: ImageRecogData[];
|
|
|
+}>();
|
|
|
+
|
|
|
+const emit = defineEmits(["area-click"]);
|
|
|
+
|
|
|
+const userStore = useUserStore();
|
|
|
+
|
|
|
+const elRef = ref();
|
|
|
+const imgRef = ref();
|
|
|
+const imageSize = ref({
|
|
|
+ width: 0,
|
|
|
+ height: 0,
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ scale: 1,
|
|
|
+});
|
|
|
+
|
|
|
+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}, ${imageSize.value.scale})`,
|
|
|
+ };
|
|
|
+});
|
|
|
+
|
|
|
+function initImageSize() {
|
|
|
+ const imgDom = imgRef.value as HTMLImageElement;
|
|
|
+ const elDom = elRef.value as HTMLDivElement;
|
|
|
+
|
|
|
+ const imgSize = getImageSizePos({
|
|
|
+ win: {
|
|
|
+ width: elDom.clientWidth,
|
|
|
+ height: elDom.clientHeight,
|
|
|
+ },
|
|
|
+ img: {
|
|
|
+ width: imgDom.naturalWidth,
|
|
|
+ height: imgDom.naturalHeight,
|
|
|
+ },
|
|
|
+ rotate: 0,
|
|
|
+ });
|
|
|
+ imageSize.value = objAssign(imageSize.value, imgSize);
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ parseRecogBlocks();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+interface AreaSize {
|
|
|
+ width: number;
|
|
|
+ height: number;
|
|
|
+}
|
|
|
+function getImageSizePos({
|
|
|
+ win,
|
|
|
+ img,
|
|
|
+ rotate,
|
|
|
+}: {
|
|
|
+ win: AreaSize;
|
|
|
+ img: AreaSize;
|
|
|
+ rotate: number;
|
|
|
+}) {
|
|
|
+ const imageSize = {
|
|
|
+ width: 0,
|
|
|
+ height: 0,
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ };
|
|
|
+ const isHorizontal = !!(rotate % 180);
|
|
|
+
|
|
|
+ const rateWin = isHorizontal
|
|
|
+ ? win.height / win.width
|
|
|
+ : win.width / win.height;
|
|
|
+ const hwin = isHorizontal
|
|
|
+ ? {
|
|
|
+ width: win.height,
|
|
|
+ height: win.width,
|
|
|
+ }
|
|
|
+ : win;
|
|
|
+
|
|
|
+ const rateImg = img.width / img.height;
|
|
|
+
|
|
|
+ if (rateImg <= rateWin) {
|
|
|
+ imageSize.height = Math.min(hwin.height, img.height);
|
|
|
+ imageSize.width = Math.floor((imageSize.height * img.width) / img.height);
|
|
|
+ } else {
|
|
|
+ imageSize.width = Math.min(hwin.width, img.width);
|
|
|
+ imageSize.height = Math.floor((imageSize.width * img.height) / img.width);
|
|
|
+ }
|
|
|
+ imageSize.left = (win.width - imageSize.width) / 2;
|
|
|
+ imageSize.top = (win.height - imageSize.height) / 2;
|
|
|
+ return imageSize;
|
|
|
+}
|
|
|
+
|
|
|
+// recogBlocks
|
|
|
+interface RecogBlock extends ImageRecogData {
|
|
|
+ fillAreaStyle: Record<string, any>;
|
|
|
+ fillOptionStyles: Array<Record<string, any>>;
|
|
|
+}
|
|
|
+
|
|
|
+const recogBlocks = ref<RecogBlock[]>([]);
|
|
|
+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 = props.recogData.map((item) => {
|
|
|
+ const nitem: RecogBlock = { ...item };
|
|
|
+
|
|
|
+ nitem.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,
|
|
|
+ };
|
|
|
+ nitem.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,
|
|
|
+ };
|
|
|
+
|
|
|
+ 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;
|
|
|
+ })
|
|
|
+ .filter((item) => item);
|
|
|
+
|
|
|
+ return nitem;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function onAreaClick(data: RecogBlock) {
|
|
|
+ console.log(data);
|
|
|
+
|
|
|
+ emit("area-click", data);
|
|
|
+}
|
|
|
+
|
|
|
+// 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;
|
|
|
+}
|
|
|
+
|
|
|
+// change image
|
|
|
+function onChangeImage() {
|
|
|
+ // TODO:
|
|
|
+}
|
|
|
+
|
|
|
+// set recog style
|
|
|
+const fillAreaSetDialogRef = ref();
|
|
|
+function onSetRecogStyle() {
|
|
|
+ fillAreaSetDialogRef.value?.open();
|
|
|
+}
|
|
|
+</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;
|
|
|
+ border-radius: 6px;
|
|
|
+ border: 1px solid #bedaff;
|
|
|
+ color: #4080ff;
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|