select.html 13.3 KB
<html lang="en"><head>
  <meta charset="UTF-8">
  <title>OCR Mapping with Manual Select Tool</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  <style>
    body { font-family: sans-serif; background: #f5f5f5; }
    #app { display: flex; gap: 20px; padding: 20px; }
    .left-panel {
      width: 500px; background: #fff; padding: 15px;
      border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1);
    }
    .form-group { margin-bottom: 15px; }
    .form-group label { font-weight: bold; display: block; margin-bottom: 5px; }
    .form-group input { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
    .right-panel { flex: 1; position: relative; background: #eee; border-radius: 8px; overflow: hidden; user-select: none; }
    .pdf-container { position: relative; display: inline-block; }
    .bbox {
      position: absolute;
      border: 2px solid #ff5252;
      /*background-color: rgba(255, 82, 82, 0.2);*/
      cursor: pointer;
    }
    .bbox.active {
      /*border-color: #2196F3;*/
      background-color: rgb(33 243 132 / 30%);
    }
    select {
      position: absolute;
      z-index: 10;
      background: #fff;
      border: 1px solid #ccc;
    }
    .select-box {
      position: absolute;
      /*border: 2px dashed #2196F3;*/
      background-color: rgba(33, 150, 243, 0.2);
      pointer-events: none;
      z-index: 5;
    }
    .delete-btn {
      position: absolute;
      bottom: -10px;
      right: -10px;
      background: #ff4d4d;
      color: #fff;
      border: none;
      border-radius: 50%;
      cursor: pointer;
      font-size: 14px;
      padding: 3px 6px;
      z-index: 20;
    }
  </style>

</head>
<body>
<div id="app">

  <!-- Right: PDF viewer + select tool -->
    <div class="right-panel" >
        <div class="pdf-container" ref="pdfContainer"
             @mousedown="startSelect"
             @mousemove="onSelect"
             @mouseup="endSelect">
            <img
                ref="pdfImage"
                :src="pdfImageUrl"
                @load="onImageLoad"
                style="width: 100%; height: auto;pointer-events: none;"
            />


      <!-- Vùng kéo chọn -->
      <div v-if="selectBox.show" class="select-box"
           :style="{ left: selectBox.x + 'px', top: selectBox.y + 'px', width: selectBox.width + 'px', height: selectBox.height + 'px' }"></div>

      <!-- Vẽ bbox OCR -->
      <div
              v-for="(item, index) in ocrData"
              :key="index"
              v-if="!item.isDeleted"
              class="bbox"
              :class="{ active: index === activeIndex }"
              :data-field="item.field"
              :style="getBoxStyle(item, index)"
              @click="selectingIndex = index">

      <button v-if="item.isManual && item.showDelete"
              class="delete-btn"
              @click.stop="deleteBox(index)">🗑</button>
      </div>


      <!-- Dropdown OCR -->
      <select v-if="selectingIndex !== null"
              :style="getSelectStyle(ocrData[selectingIndex])"
              v-model="ocrData[selectingIndex].field"
              @change="applyMapping"
      >
        <option disabled value="">-- Chọn trường dữ liệu --</option>
        <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
      </select>

      <!-- Dropdown thủ công -->
      <select v-if="selectBox.showDropdown"
              :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }"
              v-model="manualField"
              @change="applyManualMapping"
              @click.stop
      >
        <option disabled value="">-- Chọn trường dữ liệu --</option>
        <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
      </select>
    </div>
  </div>

    <!-- Left: Form inputs -->
    <div class="left-panel">
        <div v-for="field in fieldOptions" :key="field.value" class="form-group">
            <label>{{ field.label }}</label>
            <input v-model="formData[field.value]" @focus="highlightField(field.value)">
        </div>
    </div>

</div>


<script>
  new Vue({
    el: '#app',
    data() {
      return {
        pdfImageUrl: "",
        selectingIndex: null,
        isMappingManually: false,
        isSelecting: false,
        activeField: null,
        manualField: "",
        formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" },
        fieldOptions: [
          { value: "export_date", label: "Ngày xuất" },
          { value: "order_code", label: "Mã đơn hàng" },
          { value: "customer", label: "Khách hàng" },
          { value: "address", label: "Địa chỉ" },
          { value: "staff", label: "Nhân viên" }
        ],
        ocrData: [],
        selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 },
        manualIndex: null
      }
    },
    mounted() {
      this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python
      this.initData();
    },
    methods: {
      deleteBox(index) {
        const item = this.ocrData[index];
        if (item.isManual) {
          const manualBbox = item.bbox;

          // Hiện lại border các box OCR gốc nằm trong vùng thủ công
          this.ocrData.forEach(o => {
            if (!o.isManual && this.isBoxInside(o.bbox, manualBbox)) {
              o.hideBorder = false;
            }
          });

          // Đánh dấu xoá vùng thủ công
          this.ocrData[index].isDeleted = true;
          this.ocrData[index].showDelete = false;

          // Reset trạng thái nếu đây là vùng đang chọn
          if (this.manualIndex === index) {
            this.isMappingManually = false;
            this.selectBox.show = false;
            this.selectBox.showDropdown = false;
            this.manualField = "";
            this.manualIndex = null;
          }
        }
      },

      async initData() {
        await this.loadOCRData();
      },
      async loadOCRData() {
        const res = await fetch("/public/image/data_picking_detail_1754967679.json");
        this.ocrData = await res.json();
      },
      onImageLoad() {
        const img = this.$refs.pdfImage;
        this.imageWidth = img.naturalWidth;
        this.imageHeight = img.naturalHeight;
      },
      getBoxStyle(item) {
        if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {};

        const [x1, y1, x2, y2] = item.bbox;
        const displayedWidth = this.$refs.pdfImage.clientWidth;
        const displayedHeight = this.$refs.pdfImage.clientHeight;

        const scaleX = displayedWidth / this.imageWidth;
        const scaleY = displayedHeight / this.imageHeight;

        return {
          position: 'absolute',
          left: `${Math.round(x1 * scaleX)}px`,
          top: `${Math.round(y1 * scaleY)}px`,
          width: `${Math.round((x2 - x1) * scaleX)}px`,
          height: `${Math.round((y2 - y1) * scaleY)}px`,
          border: item.hideBorder ? 'none' : '2px solid ' + (this.activeField === item.field ? '#199601' : '#ff5252'),
          //backgroundColor: item.hideBorder ? 'transparent' : (this.activeField === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
          boxSizing: 'border-box',
          cursor: 'pointer',
          zIndex: item.isManual ? 30 : 10
        };
      },

      highlightField(field) {
        this.activeField = field;
      },

      startSelect(e) {
        if (this.isMappingManually || e.button !== 0) return;
        this.isSelecting = true;
        const rect = this.$refs.pdfContainer.getBoundingClientRect();
        this.selectBox.startX = e.clientX - rect.left;
        this.selectBox.startY = e.clientY - rect.top;
        this.selectBox.x = this.selectBox.startX;
        this.selectBox.y = this.selectBox.startY;
        this.selectBox.width = 0;
        this.selectBox.height = 0;
        this.selectBox.show = true;
        this.selectBox.showDropdown = false;
        this.manualField = "";
      },

      onSelect(e) {
        if (!this.isSelecting) return;
        const rect = this.$refs.pdfContainer.getBoundingClientRect();
        const currentX = e.clientX - rect.left;
        const currentY = e.clientY - rect.top;
        this.selectBox.x = Math.min(currentX, this.selectBox.startX);
        this.selectBox.y = Math.min(currentY, this.selectBox.startY);
        this.selectBox.width = Math.abs(currentX - this.selectBox.startX);
        this.selectBox.height = Math.abs(currentY - this.selectBox.startY);
      },

      endSelect(e) {
        if (!this.isSelecting) return;
        this.isSelecting = false;

        if (this.selectBox.width < 10 || this.selectBox.height < 10) {
          this.selectBox.show = false;
          return;
        }

        // displayed coords (như hiện tại, dùng để hiển thị select overlay)
        const dispX1 = this.selectBox.x;
        const dispY1 = this.selectBox.y;
        const dispX2 = this.selectBox.x + this.selectBox.width;
        const dispY2 = this.selectBox.y + this.selectBox.height;

        // scale: displayed -> original
        const displayedWidth = this.$refs.pdfImage.clientWidth;
        const displayedHeight = this.$refs.pdfImage.clientHeight;
        const scaleX = this.imageWidth / displayedWidth;
        const scaleY = this.imageHeight / displayedHeight;

        // bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData
        const origBbox = [
          Math.round(dispX1 * scaleX),
          Math.round(dispY1 * scaleY),
          Math.round(dispX2 * scaleX),
          Math.round(dispY2 * scaleY)
        ];

        // Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc)
        this.ocrData.forEach(item => {
          if (!item.isManual && this.isBoxInside(item.bbox, origBbox)) {
            item.hideBorder = true;
          }
        });

        // Thêm box thủ công (lưu theo coords gốc)
        this.ocrData.push({
          text: "",
          bbox: origBbox,
          field: "",
          isManual: true,
          showDelete: true,
          isDeleted: false,
          hideBorder: false
        });

        this.manualIndex = this.ocrData.length - 1;
        this.isMappingManually = true;
        this.selectBox.showDropdown = true;

        e.stopPropagation();
        e.preventDefault();
      }
      ,
      applyMapping() {
        const item = this.ocrData[this.selectingIndex];

        if (item && item.isManual) {
          this.manualIndex = this.selectingIndex;
          this.manualField = item.field; // đảm bảo sync field hiện tại
          this.applyManualMapping();
          return;
        }

        if (item.field) {
          this.formData[item.field] = item.text;
          this.activeField = item.field;
        }
        this.selectingIndex = null;
      },
      applyManualMapping() {
        if (!this.manualField) return;
        const manualIndex = this.manualIndex;
        const newBbox = this.ocrData[manualIndex].bbox;

        let combinedText = [];
        this.ocrData.forEach(item => {
          if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) {
            const partial = this.getPartialText(item.text, item.bbox, newBbox);
            if (partial) combinedText.push(partial);
            // combinedText.push(item.text.trim());
          }
        });

        const finalText = combinedText.join(" ");
        this.ocrData[manualIndex].field = this.manualField;
        this.formData[this.manualField] = finalText;
        this.activeField = this.manualField;

        console.log('manualField',this.manualField,this.manualIndex)
        // Reset trạng thái chọn
        this.isMappingManually = false;
        this.selectBox.show = false;
        this.selectBox.showDropdown = false;
        // this.manualField = "";
        // this.manualIndex = null;
      },

      isBoxInside(inner, outer) {
        return !(
                inner[2] < outer[0] || // box bên trái vùng chọn
                inner[0] > outer[2] || // box bên phải vùng chọn
                inner[3] < outer[1] || // box phía trên vùng chọn
                inner[1] > outer[3]    // box phía dưới vùng chọn
        );
      },

      getPartialText(text, bbox, selectBbox) {
        const [x1, y1, x2, y2] = bbox;
        const [sx1, sy1, sx2, sy2] = selectBbox;

        // Chiều rộng box OCR
        const boxWidth = x2 - x1;

        // Vị trí start và end tương đối trong text
        let startRatio = Math.max(0, (sx1 - x1) / boxWidth);
        let endRatio   = Math.min(1, (sx2 - x1) / boxWidth);

        const startIndex = Math.floor(startRatio * text.length);
        const endIndex   = Math.ceil(endRatio * text.length);

        return text.substring(startIndex, endIndex).trim();
      },
      getSelectStyle(item) {
        if (!this.imageWidth) return { position: 'absolute' };

        const [x1, y1, x2, y2] = item.bbox;
        const displayedWidth = this.$refs.pdfImage.clientWidth;
        const displayedHeight = this.$refs.pdfImage.clientHeight;
        const scaleX = displayedWidth / this.imageWidth;
        const scaleY = displayedHeight / this.imageHeight;

        return {
          position: 'absolute',
          left: `${Math.round(x1 * scaleX)}px`,
          top: `${Math.round(y2 * scaleY)}px`,
          zIndex: 9999
        };
      }

    }
  });

</script>


</body></html>