Showing
1 changed file
with
367 additions
and
357 deletions
| 1 | <html lang="en"><head> | 1 | <html lang="en"><head> |
| 2 | - <meta charset="UTF-8"> | 2 | + <meta charset="UTF-8"> |
| 3 | - <title>OCR Mapping with Manual Select Tool</title> | 3 | + <title>OCR Mapping with Manual Select Tool</title> |
| 4 | - <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> | 4 | + <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> |
| 5 | - <style> | 5 | + <style> |
| 6 | - body { font-family: sans-serif; background: #f5f5f5; } | 6 | + body { font-family: sans-serif; background: #f5f5f5; } |
| 7 | - #app { display: flex; gap: 20px; padding: 20px; } | 7 | + #app { display: flex; gap: 20px; padding: 20px; } |
| 8 | - .left-panel { | 8 | + .left-panel { |
| 9 | - width: 500px; background: #fff; padding: 15px; | 9 | + width: 500px; background: #fff; padding: 15px; |
| 10 | - border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1); | 10 | + border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1); |
| 11 | - } | 11 | + } |
| 12 | - .form-group { margin-bottom: 15px; } | 12 | + .form-group { margin-bottom: 15px; } |
| 13 | - .form-group label { font-weight: bold; display: block; margin-bottom: 5px; } | 13 | + .form-group label { font-weight: bold; display: block; margin-bottom: 5px; } |
| 14 | - .form-group input { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; } | 14 | + .form-group input { width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px; } |
| 15 | - .right-panel { flex: 1; position: relative; background: #eee; border-radius: 8px; overflow: hidden; user-select: none; } | 15 | + .right-panel { flex: 1; position: relative; background: #eee; border-radius: 8px; overflow: hidden; user-select: none; } |
| 16 | - .pdf-container { position: relative; display: inline-block; } | 16 | + .pdf-container { position: relative; display: inline-block; } |
| 17 | - .bbox { | 17 | + .bbox { |
| 18 | - position: absolute; | 18 | + position: absolute; |
| 19 | - border: 2px solid #ff5252; | 19 | + border: 2px solid #ff5252; |
| 20 | - /*background-color: rgba(255, 82, 82, 0.2);*/ | 20 | + /*background-color: rgba(255, 82, 82, 0.2);*/ |
| 21 | - cursor: pointer; | 21 | + cursor: pointer; |
| 22 | - } | 22 | + } |
| 23 | - .bbox.active { | 23 | + .bbox.active { |
| 24 | - /*border-color: #2196F3;*/ | 24 | + /*border-color: #2196F3;*/ |
| 25 | - background-color: rgb(33 243 132 / 30%); | 25 | + background-color: rgb(33 243 132 / 30%); |
| 26 | - } | 26 | + } |
| 27 | - select { | 27 | + select { |
| 28 | - position: absolute; | 28 | + position: absolute; |
| 29 | - z-index: 10; | 29 | + z-index: 10; |
| 30 | - background: #fff; | 30 | + background: #fff; |
| 31 | - border: 1px solid #ccc; | 31 | + border: 1px solid #ccc; |
| 32 | - } | 32 | + } |
| 33 | - .select-box { | 33 | + .select-box { |
| 34 | - position: absolute; | 34 | + position: absolute; |
| 35 | - /*border: 2px dashed #2196F3;*/ | 35 | + /*border: 2px dashed #2196F3;*/ |
| 36 | - background-color: rgba(33, 150, 243, 0.2); | 36 | + background-color: rgba(33, 150, 243, 0.2); |
| 37 | - pointer-events: none; | 37 | + pointer-events: none; |
| 38 | - z-index: 5; | 38 | + z-index: 5; |
| 39 | - } | 39 | + } |
| 40 | - .delete-btn { | 40 | + .delete-btn { |
| 41 | - position: absolute; | 41 | + position: absolute; |
| 42 | - bottom: -10px; | 42 | + bottom: -10px; |
| 43 | - right: -10px; | 43 | + right: -10px; |
| 44 | - background: #ff4d4d; | 44 | + background: #ff4d4d; |
| 45 | - color: #fff; | 45 | + color: #fff; |
| 46 | - border: none; | 46 | + border: none; |
| 47 | - border-radius: 50%; | 47 | + border-radius: 50%; |
| 48 | - cursor: pointer; | 48 | + cursor: pointer; |
| 49 | - font-size: 14px; | 49 | + font-size: 14px; |
| 50 | - padding: 3px 6px; | 50 | + padding: 3px 6px; |
| 51 | - z-index: 20; | 51 | + z-index: 20; |
| 52 | - } | 52 | + } |
| 53 | - </style> | 53 | + </style> |
| 54 | 54 | ||
| 55 | </head> | 55 | </head> |
| 56 | <body> | 56 | <body> |
| 57 | <div id="app"> | 57 | <div id="app"> |
| 58 | 58 | ||
| 59 | - <!-- Right: PDF viewer + select tool --> | 59 | + <!-- Right: PDF viewer + select tool --> |
| 60 | <div class="right-panel" > | 60 | <div class="right-panel" > |
| 61 | <div class="pdf-container" ref="pdfContainer" | 61 | <div class="pdf-container" ref="pdfContainer" |
| 62 | @mousedown="startSelect" | 62 | @mousedown="startSelect" |
| ... | @@ -70,49 +70,49 @@ | ... | @@ -70,49 +70,49 @@ |
| 70 | /> | 70 | /> |
| 71 | 71 | ||
| 72 | 72 | ||
| 73 | - <!-- Vùng kéo chọn --> | 73 | + <!-- Vùng kéo chọn --> |
| 74 | - <div v-if="selectBox.show" class="select-box" | 74 | + <div v-if="selectBox.show" class="select-box" |
| 75 | - :style="{ left: selectBox.x + 'px', top: selectBox.y + 'px', width: selectBox.width + 'px', height: selectBox.height + 'px' }"></div> | 75 | + :style="{ left: selectBox.x + 'px', top: selectBox.y + 'px', width: selectBox.width + 'px', height: selectBox.height + 'px' }"></div> |
| 76 | - | 76 | + |
| 77 | - <!-- Vẽ bbox OCR --> | 77 | + <!-- Vẽ bbox OCR --> |
| 78 | - <div | 78 | + <div |
| 79 | - v-for="(item, index) in ocrData" | 79 | + v-for="(item, index) in ocrData" |
| 80 | - :key="index" | 80 | + :key="index" |
| 81 | - v-if="!item.isDeleted" | 81 | + v-if="!item.isDeleted" |
| 82 | - class="bbox" | 82 | + class="bbox" |
| 83 | - :class="{ active: index === activeIndex }" | 83 | + :class="{ active: index === activeIndex }" |
| 84 | - :data-field="item.field" | 84 | + :data-field="item.field" |
| 85 | - :style="getBoxStyle(item, index)" | 85 | + :style="getBoxStyle(item, index)" |
| 86 | - @click="selectingIndex = index"> | 86 | + @click="selectingIndex = index"> |
| 87 | - | 87 | + |
| 88 | - <button v-if="item.isManual && item.showDelete" | 88 | + <button v-if="item.isManual && item.showDelete" |
| 89 | - class="delete-btn" | 89 | + class="delete-btn" |
| 90 | - @click.stop="deleteBox(index)">🗑</button> | 90 | + @click.stop="deleteBox(index)">🗑</button> |
| 91 | - </div> | 91 | + </div> |
| 92 | - | 92 | + |
| 93 | - | 93 | + |
| 94 | - <!-- Dropdown OCR --> | 94 | + <!-- Dropdown OCR --> |
| 95 | - <select v-if="selectingIndex !== null" | 95 | + <select v-if="selectingIndex !== null" |
| 96 | - :style="getSelectStyle(ocrData[selectingIndex])" | 96 | + :style="getSelectStyle(ocrData[selectingIndex])" |
| 97 | - v-model="ocrData[selectingIndex].field" | 97 | + v-model="ocrData[selectingIndex].field" |
| 98 | - @change="applyMapping" | 98 | + @change="applyMapping" |
| 99 | - > | 99 | + > |
| 100 | - <option disabled value="">-- Chọn trường dữ liệu --</option> | 100 | + <option disabled value="">-- Chọn trường dữ liệu --</option> |
| 101 | - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> | 101 | + <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> |
| 102 | - </select> | 102 | + </select> |
| 103 | - | 103 | + |
| 104 | - <!-- Dropdown thủ công --> | 104 | + <!-- Dropdown thủ công --> |
| 105 | - <select v-if="selectBox.showDropdown" | 105 | + <select v-if="selectBox.showDropdown" |
| 106 | - :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }" | 106 | + :style="{ left: selectBox.x + 'px', top: (selectBox.y + selectBox.height) + 'px' }" |
| 107 | - v-model="manualField" | 107 | + v-model="manualField" |
| 108 | - @change="applyManualMapping" | 108 | + @change="applyManualMapping" |
| 109 | - @click.stop | 109 | + @click.stop |
| 110 | - > | 110 | + > |
| 111 | - <option disabled value="">-- Chọn trường dữ liệu --</option> | 111 | + <option disabled value="">-- Chọn trường dữ liệu --</option> |
| 112 | - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> | 112 | + <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> |
| 113 | - </select> | 113 | + </select> |
| 114 | + </div> | ||
| 114 | </div> | 115 | </div> |
| 115 | - </div> | ||
| 116 | 116 | ||
| 117 | <!-- Left: Form inputs --> | 117 | <!-- Left: Form inputs --> |
| 118 | <div class="left-panel"> | 118 | <div class="left-panel"> |
| ... | @@ -126,272 +126,282 @@ | ... | @@ -126,272 +126,282 @@ |
| 126 | 126 | ||
| 127 | 127 | ||
| 128 | <script> | 128 | <script> |
| 129 | - new Vue({ | 129 | + new Vue({ |
| 130 | - el: '#app', | 130 | + el: '#app', |
| 131 | - data() { | 131 | + data() { |
| 132 | - return { | 132 | + return { |
| 133 | - pdfImageUrl: "", | 133 | + pdfImageUrl: "", |
| 134 | - selectingIndex: null, | 134 | + selectingIndex: null, |
| 135 | - isMappingManually: false, | 135 | + isMappingManually: false, |
| 136 | - isSelecting: false, | 136 | + isSelecting: false, |
| 137 | - activeField: null, | 137 | + activeIndex: null, |
| 138 | - manualField: "", | 138 | + manualField: "", |
| 139 | - formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" }, | 139 | + formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" }, |
| 140 | - fieldOptions: [ | 140 | + fieldOptions: [ |
| 141 | - { value: "export_date", label: "Ngày xuất" }, | 141 | + { value: "export_date", label: "Ngày xuất" }, |
| 142 | - { value: "order_code", label: "Mã đơn hàng" }, | 142 | + { value: "order_code", label: "Mã đơn hàng" }, |
| 143 | - { value: "customer", label: "Khách hàng" }, | 143 | + { value: "customer", label: "Khách hàng" }, |
| 144 | - { value: "address", label: "Địa chỉ" }, | 144 | + { value: "address", label: "Địa chỉ" }, |
| 145 | - { value: "staff", label: "Nhân viên" } | 145 | + { value: "staff", label: "Nhân viên" } |
| 146 | - ], | 146 | + ], |
| 147 | - ocrData: [], | 147 | + ocrData: [], |
| 148 | - selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 }, | 148 | + selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 }, |
| 149 | - manualIndex: null | 149 | + manualIndex: null |
| 150 | - } | 150 | + } |
| 151 | - }, | 151 | + }, |
| 152 | - mounted() { | 152 | + mounted() { |
| 153 | - this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python | 153 | + this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python |
| 154 | - this.initData(); | 154 | + this.initData(); |
| 155 | - }, | 155 | + }, |
| 156 | - methods: { | 156 | + methods: { |
| 157 | - deleteBox(index) { | 157 | + deleteBox(index) { |
| 158 | - const item = this.ocrData[index]; | 158 | + const item = this.ocrData[index]; |
| 159 | - if (item.isManual) { | 159 | + if (item.isManual) { |
| 160 | - const manualBbox = item.bbox; | 160 | + const manualBbox = item.bbox; |
| 161 | - | 161 | + |
| 162 | - // Hiện lại border các box OCR gốc nằm trong vùng thủ công | 162 | + // Hiện lại border các box OCR gốc nằm trong vùng thủ công |
| 163 | - this.ocrData.forEach(o => { | 163 | + this.ocrData.forEach(o => { |
| 164 | - if (!o.isManual && this.isBoxInside(o.bbox, manualBbox)) { | 164 | + if (!o.isManual && this.isBoxInside(o.bbox, manualBbox)) { |
| 165 | - o.hideBorder = false; | 165 | + o.hideBorder = false; |
| 166 | + } | ||
| 167 | + }); | ||
| 168 | + | ||
| 169 | + // Đánh dấu xoá vùng thủ công | ||
| 170 | + this.ocrData[index].isDeleted = true; | ||
| 171 | + this.ocrData[index].showDelete = false; | ||
| 172 | + | ||
| 173 | + // Reset trạng thái nếu đây là vùng đang chọn | ||
| 174 | + if (this.manualIndex === index) { | ||
| 175 | + this.isMappingManually = false; | ||
| 176 | + this.selectBox.show = false; | ||
| 177 | + this.selectBox.showDropdown = false; | ||
| 178 | + this.manualField = ""; | ||
| 179 | + this.manualIndex = null; | ||
| 180 | + } | ||
| 181 | + } | ||
| 182 | + }, | ||
| 183 | + | ||
| 184 | + async initData() { | ||
| 185 | + await this.loadOCRData(); | ||
| 186 | + }, | ||
| 187 | + async loadOCRData() { | ||
| 188 | + const res = await fetch("/public/image/data_picking_detail_1754967679.json"); | ||
| 189 | + this.ocrData = await res.json(); | ||
| 190 | + }, | ||
| 191 | + onImageLoad() { | ||
| 192 | + const img = this.$refs.pdfImage; | ||
| 193 | + this.imageWidth = img.naturalWidth; | ||
| 194 | + this.imageHeight = img.naturalHeight; | ||
| 195 | + }, | ||
| 196 | + getBoxStyle(item, index) { | ||
| 197 | + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {}; | ||
| 198 | + | ||
| 199 | + const [x1, y1, x2, y2] = item.bbox; | ||
| 200 | + const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 201 | + const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 202 | + | ||
| 203 | + const scaleX = displayedWidth / this.imageWidth; | ||
| 204 | + const scaleY = displayedHeight / this.imageHeight; | ||
| 205 | + | ||
| 206 | + return { | ||
| 207 | + position: 'absolute', | ||
| 208 | + left: `${Math.round(x1 * scaleX)}px`, | ||
| 209 | + top: `${Math.round(y1 * scaleY)}px`, | ||
| 210 | + width: `${Math.round((x2 - x1) * scaleX)}px`, | ||
| 211 | + height: `${Math.round((y2 - y1) * scaleY)}px`, | ||
| 212 | + border: item.hideBorder ? 'none' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'), | ||
| 213 | + //backgroundColor: item.hideBorder ? 'transparent' : (this.activeIndex === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'), | ||
| 214 | + boxSizing: 'border-box', | ||
| 215 | + cursor: 'pointer', | ||
| 216 | + zIndex: item.isManual ? 30 : 10 | ||
| 217 | + }; | ||
| 218 | + }, | ||
| 219 | + | ||
| 220 | + highlightField(field) { | ||
| 221 | + // tìm box gần nhất match field này | ||
| 222 | + let idx = -1; | ||
| 223 | + for (let i = this.ocrData.length - 1; i >= 0; i--) { | ||
| 224 | + const it = this.ocrData[i]; | ||
| 225 | + if (!it.isDeleted && it.field === field) { | ||
| 226 | + idx = i; | ||
| 227 | + break; | ||
| 228 | + } | ||
| 229 | + } | ||
| 230 | + this.activeIndex = idx; // nếu không tìm thấy thì = -1 | ||
| 231 | + }, | ||
| 232 | + | ||
| 233 | + startSelect(e) { | ||
| 234 | + if (this.isMappingManually || e.button !== 0) return; | ||
| 235 | + this.isSelecting = true; | ||
| 236 | + const rect = this.$refs.pdfContainer.getBoundingClientRect(); | ||
| 237 | + this.selectBox.startX = e.clientX - rect.left; | ||
| 238 | + this.selectBox.startY = e.clientY - rect.top; | ||
| 239 | + this.selectBox.x = this.selectBox.startX; | ||
| 240 | + this.selectBox.y = this.selectBox.startY; | ||
| 241 | + this.selectBox.width = 0; | ||
| 242 | + this.selectBox.height = 0; | ||
| 243 | + this.selectBox.show = true; | ||
| 244 | + this.selectBox.showDropdown = false; | ||
| 245 | + this.manualField = ""; | ||
| 246 | + }, | ||
| 247 | + | ||
| 248 | + onSelect(e) { | ||
| 249 | + if (!this.isSelecting) return; | ||
| 250 | + const rect = this.$refs.pdfContainer.getBoundingClientRect(); | ||
| 251 | + const currentX = e.clientX - rect.left; | ||
| 252 | + const currentY = e.clientY - rect.top; | ||
| 253 | + this.selectBox.x = Math.min(currentX, this.selectBox.startX); | ||
| 254 | + this.selectBox.y = Math.min(currentY, this.selectBox.startY); | ||
| 255 | + this.selectBox.width = Math.abs(currentX - this.selectBox.startX); | ||
| 256 | + this.selectBox.height = Math.abs(currentY - this.selectBox.startY); | ||
| 257 | + }, | ||
| 258 | + | ||
| 259 | + endSelect(e) { | ||
| 260 | + if (!this.isSelecting) return; | ||
| 261 | + this.isSelecting = false; | ||
| 262 | + | ||
| 263 | + if (this.selectBox.width < 10 || this.selectBox.height < 10) { | ||
| 264 | + this.selectBox.show = false; | ||
| 265 | + return; | ||
| 266 | + } | ||
| 267 | + | ||
| 268 | + // displayed coords (như hiện tại, dùng để hiển thị select overlay) | ||
| 269 | + const dispX1 = this.selectBox.x; | ||
| 270 | + const dispY1 = this.selectBox.y; | ||
| 271 | + const dispX2 = this.selectBox.x + this.selectBox.width; | ||
| 272 | + const dispY2 = this.selectBox.y + this.selectBox.height; | ||
| 273 | + | ||
| 274 | + // scale: displayed -> original | ||
| 275 | + const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 276 | + const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 277 | + const scaleX = this.imageWidth / displayedWidth; | ||
| 278 | + const scaleY = this.imageHeight / displayedHeight; | ||
| 279 | + | ||
| 280 | + // bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData | ||
| 281 | + const origBbox = [ | ||
| 282 | + Math.round(dispX1 * scaleX), | ||
| 283 | + Math.round(dispY1 * scaleY), | ||
| 284 | + Math.round(dispX2 * scaleX), | ||
| 285 | + Math.round(dispY2 * scaleY) | ||
| 286 | + ]; | ||
| 287 | + | ||
| 288 | + // Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc) | ||
| 289 | + this.ocrData.forEach(item => { | ||
| 290 | + if (!item.isManual && this.isBoxInside(item.bbox, origBbox)) { | ||
| 291 | + item.hideBorder = true; | ||
| 292 | + } | ||
| 293 | + }); | ||
| 294 | + | ||
| 295 | + // Thêm box thủ công (lưu theo coords gốc) | ||
| 296 | + this.ocrData.push({ | ||
| 297 | + text: "", | ||
| 298 | + bbox: origBbox, | ||
| 299 | + field: "", | ||
| 300 | + isManual: true, | ||
| 301 | + showDelete: true, | ||
| 302 | + isDeleted: false, | ||
| 303 | + hideBorder: false | ||
| 304 | + }); | ||
| 305 | + | ||
| 306 | + this.manualIndex = this.ocrData.length - 1; | ||
| 307 | + this.isMappingManually = true; | ||
| 308 | + this.selectBox.showDropdown = true; | ||
| 309 | + | ||
| 310 | + e.stopPropagation(); | ||
| 311 | + e.preventDefault(); | ||
| 312 | + } | ||
| 313 | + , | ||
| 314 | + applyMapping() { | ||
| 315 | + const item = this.ocrData[this.selectingIndex]; | ||
| 316 | + | ||
| 317 | + if (item && item.isManual) { | ||
| 318 | + this.manualIndex = this.selectingIndex; | ||
| 319 | + this.manualField = item.field || ""; | ||
| 320 | + this.applyManualMapping(); | ||
| 321 | + return; | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + if (item.field) { | ||
| 325 | + this.formData[item.field] = item.text; | ||
| 326 | + this.activeIndex = this.selectingIndex; | ||
| 327 | + } | ||
| 328 | + this.selectingIndex = null; | ||
| 329 | + }, | ||
| 330 | + applyManualMapping() { | ||
| 331 | + if (!this.manualField) return; | ||
| 332 | + const manualIndex = this.manualIndex; | ||
| 333 | + const newBbox = this.ocrData[manualIndex].bbox; | ||
| 334 | + | ||
| 335 | + let combinedText = []; | ||
| 336 | + this.ocrData.forEach(item => { | ||
| 337 | + if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) { | ||
| 338 | + const partial = this.getPartialText(item.text, item.bbox, newBbox); | ||
| 339 | + if (partial) combinedText.push(partial); | ||
| 340 | + // combinedText.push(item.text.trim()); | ||
| 341 | + } | ||
| 342 | + }); | ||
| 343 | + | ||
| 344 | + const finalText = combinedText.join(" "); | ||
| 345 | + this.ocrData[manualIndex].field = this.manualField; | ||
| 346 | + this.formData[this.manualField] = finalText; | ||
| 347 | + this.activeIndex = manualIndex; | ||
| 348 | + | ||
| 349 | + console.log('manualField', this.manualField, this.manualIndex) | ||
| 350 | + // Reset trạng thái chọn | ||
| 351 | + this.isMappingManually = false; | ||
| 352 | + this.selectBox.show = false; | ||
| 353 | + this.selectBox.showDropdown = false; | ||
| 354 | + // this.manualField = ""; | ||
| 355 | + // this.manualIndex = null; | ||
| 356 | + }, | ||
| 357 | + | ||
| 358 | + isBoxInside(inner, outer) { | ||
| 359 | + return !( | ||
| 360 | + inner[2] < outer[0] || // box bên trái vùng chọn | ||
| 361 | + inner[0] > outer[2] || // box bên phải vùng chọn | ||
| 362 | + inner[3] < outer[1] || // box phía trên vùng chọn | ||
| 363 | + inner[1] > outer[3] // box phía dưới vùng chọn | ||
| 364 | + ); | ||
| 365 | + }, | ||
| 366 | + | ||
| 367 | + getPartialText(text, bbox, selectBbox) { | ||
| 368 | + const [x1, y1, x2, y2] = bbox; | ||
| 369 | + const [sx1, sy1, sx2, sy2] = selectBbox; | ||
| 370 | + | ||
| 371 | + // Chiều rộng box OCR | ||
| 372 | + const boxWidth = x2 - x1; | ||
| 373 | + | ||
| 374 | + // Vị trí start và end tương đối trong text | ||
| 375 | + let startRatio = Math.max(0, (sx1 - x1) / boxWidth); | ||
| 376 | + let endRatio = Math.min(1, (sx2 - x1) / boxWidth); | ||
| 377 | + | ||
| 378 | + const startIndex = Math.floor(startRatio * text.length); | ||
| 379 | + const endIndex = Math.ceil(endRatio * text.length); | ||
| 380 | + | ||
| 381 | + return text.substring(startIndex, endIndex).trim(); | ||
| 382 | + }, | ||
| 383 | + getSelectStyle(item) { | ||
| 384 | + if (!this.imageWidth) return {position: 'absolute'}; | ||
| 385 | + | ||
| 386 | + const [x1, y1, x2, y2] = item.bbox; | ||
| 387 | + const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 388 | + const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 389 | + const scaleX = displayedWidth / this.imageWidth; | ||
| 390 | + const scaleY = displayedHeight / this.imageHeight; | ||
| 391 | + | ||
| 392 | + return { | ||
| 393 | + position: 'absolute', | ||
| 394 | + left: `${Math.round(x1 * scaleX)}px`, | ||
| 395 | + top: `${Math.round(y2 * scaleY)}px`, | ||
| 396 | + zIndex: 9999 | ||
| 397 | + }; | ||
| 166 | } | 398 | } |
| 167 | - }); | ||
| 168 | - | ||
| 169 | - // Đánh dấu xoá vùng thủ công | ||
| 170 | - this.ocrData[index].isDeleted = true; | ||
| 171 | - this.ocrData[index].showDelete = false; | ||
| 172 | - | ||
| 173 | - // Reset trạng thái nếu đây là vùng đang chọn | ||
| 174 | - if (this.manualIndex === index) { | ||
| 175 | - this.isMappingManually = false; | ||
| 176 | - this.selectBox.show = false; | ||
| 177 | - this.selectBox.showDropdown = false; | ||
| 178 | - this.manualField = ""; | ||
| 179 | - this.manualIndex = null; | ||
| 180 | - } | ||
| 181 | - } | ||
| 182 | - }, | ||
| 183 | - | ||
| 184 | - async initData() { | ||
| 185 | - await this.loadOCRData(); | ||
| 186 | - }, | ||
| 187 | - async loadOCRData() { | ||
| 188 | - const res = await fetch("/public/image/data_picking_detail_1754967679.json"); | ||
| 189 | - this.ocrData = await res.json(); | ||
| 190 | - }, | ||
| 191 | - onImageLoad() { | ||
| 192 | - const img = this.$refs.pdfImage; | ||
| 193 | - this.imageWidth = img.naturalWidth; | ||
| 194 | - this.imageHeight = img.naturalHeight; | ||
| 195 | - }, | ||
| 196 | - getBoxStyle(item) { | ||
| 197 | - if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {}; | ||
| 198 | - | ||
| 199 | - const [x1, y1, x2, y2] = item.bbox; | ||
| 200 | - const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 201 | - const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 202 | - | ||
| 203 | - const scaleX = displayedWidth / this.imageWidth; | ||
| 204 | - const scaleY = displayedHeight / this.imageHeight; | ||
| 205 | - | ||
| 206 | - return { | ||
| 207 | - position: 'absolute', | ||
| 208 | - left: `${Math.round(x1 * scaleX)}px`, | ||
| 209 | - top: `${Math.round(y1 * scaleY)}px`, | ||
| 210 | - width: `${Math.round((x2 - x1) * scaleX)}px`, | ||
| 211 | - height: `${Math.round((y2 - y1) * scaleY)}px`, | ||
| 212 | - border: item.hideBorder ? 'none' : '2px solid ' + (this.activeField === item.field ? '#199601' : '#ff5252'), | ||
| 213 | - //backgroundColor: item.hideBorder ? 'transparent' : (this.activeField === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'), | ||
| 214 | - boxSizing: 'border-box', | ||
| 215 | - cursor: 'pointer', | ||
| 216 | - zIndex: item.isManual ? 30 : 10 | ||
| 217 | - }; | ||
| 218 | - }, | ||
| 219 | - | ||
| 220 | - highlightField(field) { | ||
| 221 | - this.activeField = field; | ||
| 222 | - }, | ||
| 223 | - | ||
| 224 | - startSelect(e) { | ||
| 225 | - if (this.isMappingManually || e.button !== 0) return; | ||
| 226 | - this.isSelecting = true; | ||
| 227 | - const rect = this.$refs.pdfContainer.getBoundingClientRect(); | ||
| 228 | - this.selectBox.startX = e.clientX - rect.left; | ||
| 229 | - this.selectBox.startY = e.clientY - rect.top; | ||
| 230 | - this.selectBox.x = this.selectBox.startX; | ||
| 231 | - this.selectBox.y = this.selectBox.startY; | ||
| 232 | - this.selectBox.width = 0; | ||
| 233 | - this.selectBox.height = 0; | ||
| 234 | - this.selectBox.show = true; | ||
| 235 | - this.selectBox.showDropdown = false; | ||
| 236 | - this.manualField = ""; | ||
| 237 | - }, | ||
| 238 | - | ||
| 239 | - onSelect(e) { | ||
| 240 | - if (!this.isSelecting) return; | ||
| 241 | - const rect = this.$refs.pdfContainer.getBoundingClientRect(); | ||
| 242 | - const currentX = e.clientX - rect.left; | ||
| 243 | - const currentY = e.clientY - rect.top; | ||
| 244 | - this.selectBox.x = Math.min(currentX, this.selectBox.startX); | ||
| 245 | - this.selectBox.y = Math.min(currentY, this.selectBox.startY); | ||
| 246 | - this.selectBox.width = Math.abs(currentX - this.selectBox.startX); | ||
| 247 | - this.selectBox.height = Math.abs(currentY - this.selectBox.startY); | ||
| 248 | - }, | ||
| 249 | - | ||
| 250 | - endSelect(e) { | ||
| 251 | - if (!this.isSelecting) return; | ||
| 252 | - this.isSelecting = false; | ||
| 253 | - | ||
| 254 | - if (this.selectBox.width < 10 || this.selectBox.height < 10) { | ||
| 255 | - this.selectBox.show = false; | ||
| 256 | - return; | ||
| 257 | - } | ||
| 258 | - | ||
| 259 | - // displayed coords (như hiện tại, dùng để hiển thị select overlay) | ||
| 260 | - const dispX1 = this.selectBox.x; | ||
| 261 | - const dispY1 = this.selectBox.y; | ||
| 262 | - const dispX2 = this.selectBox.x + this.selectBox.width; | ||
| 263 | - const dispY2 = this.selectBox.y + this.selectBox.height; | ||
| 264 | - | ||
| 265 | - // scale: displayed -> original | ||
| 266 | - const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 267 | - const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 268 | - const scaleX = this.imageWidth / displayedWidth; | ||
| 269 | - const scaleY = this.imageHeight / displayedHeight; | ||
| 270 | - | ||
| 271 | - // bbox ở hệ gốc (original image pixels) — dùng để so sánh với ocrData và lưu vào ocrData | ||
| 272 | - const origBbox = [ | ||
| 273 | - Math.round(dispX1 * scaleX), | ||
| 274 | - Math.round(dispY1 * scaleY), | ||
| 275 | - Math.round(dispX2 * scaleX), | ||
| 276 | - Math.round(dispY2 * scaleY) | ||
| 277 | - ]; | ||
| 278 | - | ||
| 279 | - // Ẩn border các box OCR gốc nằm giao nhau với vùng thủ công (dùng coords gốc) | ||
| 280 | - this.ocrData.forEach(item => { | ||
| 281 | - if (!item.isManual && this.isBoxInside(item.bbox, origBbox)) { | ||
| 282 | - item.hideBorder = true; | ||
| 283 | - } | ||
| 284 | - }); | ||
| 285 | - | ||
| 286 | - // Thêm box thủ công (lưu theo coords gốc) | ||
| 287 | - this.ocrData.push({ | ||
| 288 | - text: "", | ||
| 289 | - bbox: origBbox, | ||
| 290 | - field: "", | ||
| 291 | - isManual: true, | ||
| 292 | - showDelete: true, | ||
| 293 | - isDeleted: false, | ||
| 294 | - hideBorder: false | ||
| 295 | - }); | ||
| 296 | - | ||
| 297 | - this.manualIndex = this.ocrData.length - 1; | ||
| 298 | - this.isMappingManually = true; | ||
| 299 | - this.selectBox.showDropdown = true; | ||
| 300 | - | ||
| 301 | - e.stopPropagation(); | ||
| 302 | - e.preventDefault(); | ||
| 303 | - } | ||
| 304 | - , | ||
| 305 | - applyMapping() { | ||
| 306 | - const item = this.ocrData[this.selectingIndex]; | ||
| 307 | - | ||
| 308 | - if (item && item.isManual) { | ||
| 309 | - this.manualIndex = this.selectingIndex; | ||
| 310 | - this.manualField = item.field; // đảm bảo sync field hiện tại | ||
| 311 | - this.applyManualMapping(); | ||
| 312 | - return; | ||
| 313 | - } | ||
| 314 | 399 | ||
| 315 | - if (item.field) { | ||
| 316 | - this.formData[item.field] = item.text; | ||
| 317 | - this.activeField = item.field; | ||
| 318 | } | 400 | } |
| 319 | - this.selectingIndex = null; | 401 | + }); |
| 320 | - }, | ||
| 321 | - applyManualMapping() { | ||
| 322 | - if (!this.manualField) return; | ||
| 323 | - const manualIndex = this.manualIndex; | ||
| 324 | - const newBbox = this.ocrData[manualIndex].bbox; | ||
| 325 | - | ||
| 326 | - let combinedText = []; | ||
| 327 | - this.ocrData.forEach(item => { | ||
| 328 | - if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) { | ||
| 329 | - const partial = this.getPartialText(item.text, item.bbox, newBbox); | ||
| 330 | - if (partial) combinedText.push(partial); | ||
| 331 | - // combinedText.push(item.text.trim()); | ||
| 332 | - } | ||
| 333 | - }); | ||
| 334 | - | ||
| 335 | - const finalText = combinedText.join(" "); | ||
| 336 | - this.ocrData[manualIndex].field = this.manualField; | ||
| 337 | - this.formData[this.manualField] = finalText; | ||
| 338 | - this.activeField = this.manualField; | ||
| 339 | - | ||
| 340 | - console.log('manualField',this.manualField,this.manualIndex) | ||
| 341 | - // Reset trạng thái chọn | ||
| 342 | - this.isMappingManually = false; | ||
| 343 | - this.selectBox.show = false; | ||
| 344 | - this.selectBox.showDropdown = false; | ||
| 345 | - // this.manualField = ""; | ||
| 346 | - // this.manualIndex = null; | ||
| 347 | - }, | ||
| 348 | - | ||
| 349 | - isBoxInside(inner, outer) { | ||
| 350 | - return !( | ||
| 351 | - inner[2] < outer[0] || // box bên trái vùng chọn | ||
| 352 | - inner[0] > outer[2] || // box bên phải vùng chọn | ||
| 353 | - inner[3] < outer[1] || // box phía trên vùng chọn | ||
| 354 | - inner[1] > outer[3] // box phía dưới vùng chọn | ||
| 355 | - ); | ||
| 356 | - }, | ||
| 357 | - | ||
| 358 | - getPartialText(text, bbox, selectBbox) { | ||
| 359 | - const [x1, y1, x2, y2] = bbox; | ||
| 360 | - const [sx1, sy1, sx2, sy2] = selectBbox; | ||
| 361 | - | ||
| 362 | - // Chiều rộng box OCR | ||
| 363 | - const boxWidth = x2 - x1; | ||
| 364 | - | ||
| 365 | - // Vị trí start và end tương đối trong text | ||
| 366 | - let startRatio = Math.max(0, (sx1 - x1) / boxWidth); | ||
| 367 | - let endRatio = Math.min(1, (sx2 - x1) / boxWidth); | ||
| 368 | - | ||
| 369 | - const startIndex = Math.floor(startRatio * text.length); | ||
| 370 | - const endIndex = Math.ceil(endRatio * text.length); | ||
| 371 | - | ||
| 372 | - return text.substring(startIndex, endIndex).trim(); | ||
| 373 | - }, | ||
| 374 | - getSelectStyle(item) { | ||
| 375 | - if (!this.imageWidth) return { position: 'absolute' }; | ||
| 376 | - | ||
| 377 | - const [x1, y1, x2, y2] = item.bbox; | ||
| 378 | - const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 379 | - const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 380 | - const scaleX = displayedWidth / this.imageWidth; | ||
| 381 | - const scaleY = displayedHeight / this.imageHeight; | ||
| 382 | - | ||
| 383 | - return { | ||
| 384 | - position: 'absolute', | ||
| 385 | - left: `${Math.round(x1 * scaleX)}px`, | ||
| 386 | - top: `${Math.round(y2 * scaleY)}px`, | ||
| 387 | - zIndex: 9999 | ||
| 388 | - }; | ||
| 389 | - } | ||
| 390 | - | ||
| 391 | - } | ||
| 392 | - }); | ||
| 393 | 402 | ||
| 394 | </script> | 403 | </script> |
| 395 | 404 | ||
| 396 | 405 | ||
| 397 | -</body></html> | 406 | +</body> |
| 407 | +</html> | ... | ... |
-
Please register or sign in to post a comment