Showing
1 changed file
with
663 additions
and
46 deletions
| ... | @@ -21,8 +21,14 @@ | ... | @@ -21,8 +21,14 @@ |
| 21 | cursor: pointer; | 21 | cursor: pointer; |
| 22 | } | 22 | } |
| 23 | .bbox.active { | 23 | .bbox.active { |
| 24 | - /*border-color: #2196F3;*/ | 24 | + border-color: #199601 !important; |
| 25 | - background-color: rgb(33 243 132 / 30%); | 25 | + background-color: rgba(25, 150, 1, 0.4) !important; |
| 26 | + } | ||
| 27 | + | ||
| 28 | + @keyframes focusPulse { | ||
| 29 | + 0% { transform: scale(1); } | ||
| 30 | + 50% { transform: scale(1.05); } | ||
| 31 | + 100% { transform: scale(1); } | ||
| 26 | } | 32 | } |
| 27 | select { | 33 | select { |
| 28 | position: absolute; | 34 | position: absolute; |
| ... | @@ -54,8 +60,8 @@ | ... | @@ -54,8 +60,8 @@ |
| 54 | 60 | ||
| 55 | </head> | 61 | </head> |
| 56 | <body> | 62 | <body> |
| 63 | +<meta name="csrf-token" content="{{ csrf_token() }}"> | ||
| 57 | <div id="app"> | 64 | <div id="app"> |
| 58 | - | ||
| 59 | <!-- Right: PDF viewer + select tool --> | 65 | <!-- Right: PDF viewer + select tool --> |
| 60 | <div class="right-panel" > | 66 | <div class="right-panel" > |
| 61 | <div class="pdf-container" ref="pdfContainer" | 67 | <div class="pdf-container" ref="pdfContainer" |
| ... | @@ -83,7 +89,7 @@ | ... | @@ -83,7 +89,7 @@ |
| 83 | :class="{ active: index === activeIndex }" | 89 | :class="{ active: index === activeIndex }" |
| 84 | :data-field="item.field" | 90 | :data-field="item.field" |
| 85 | :style="getBoxStyle(item, index)" | 91 | :style="getBoxStyle(item, index)" |
| 86 | - @click="selectingIndex = index"> | 92 | + @click="onBoxClick(index)"> |
| 87 | 93 | ||
| 88 | <button v-if="item.isManual && item.showDelete" | 94 | <button v-if="item.isManual && item.showDelete" |
| 89 | class="delete-btn" | 95 | class="delete-btn" |
| ... | @@ -92,13 +98,13 @@ | ... | @@ -92,13 +98,13 @@ |
| 92 | 98 | ||
| 93 | 99 | ||
| 94 | <!-- Dropdown OCR --> | 100 | <!-- Dropdown OCR --> |
| 95 | - <select v-if="selectingIndex !== null" | 101 | + <select v-if="selectingIndex !== null && ocrData[selectingIndex]" |
| 96 | :style="getSelectStyle(ocrData[selectingIndex])" | 102 | :style="getSelectStyle(ocrData[selectingIndex])" |
| 97 | v-model="ocrData[selectingIndex].field" | 103 | v-model="ocrData[selectingIndex].field" |
| 98 | @change="applyMapping" | 104 | @change="applyMapping" |
| 99 | > | 105 | > |
| 100 | <option disabled value="">-- Chọn trường dữ liệu --</option> | 106 | <option disabled value="">-- Chọn trường dữ liệu --</option> |
| 101 | - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> | 107 | + <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option> |
| 102 | </select> | 108 | </select> |
| 103 | 109 | ||
| 104 | <!-- Dropdown thủ công --> | 110 | <!-- Dropdown thủ công --> |
| ... | @@ -109,7 +115,7 @@ | ... | @@ -109,7 +115,7 @@ |
| 109 | @click.stop | 115 | @click.stop |
| 110 | > | 116 | > |
| 111 | <option disabled value="">-- Chọn trường dữ liệu --</option> | 117 | <option disabled value="">-- Chọn trường dữ liệu --</option> |
| 112 | - <option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option> | 118 | + <option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option> |
| 113 | </select> | 119 | </select> |
| 114 | </div> | 120 | </div> |
| 115 | </div> | 121 | </div> |
| ... | @@ -117,9 +123,15 @@ | ... | @@ -117,9 +123,15 @@ |
| 117 | <!-- Left: Form inputs --> | 123 | <!-- Left: Form inputs --> |
| 118 | <div class="left-panel"> | 124 | <div class="left-panel"> |
| 119 | <div v-for="field in fieldOptions" :key="field.value" class="form-group"> | 125 | <div v-for="field in fieldOptions" :key="field.value" class="form-group"> |
| 120 | - <label>{{ field.label }}</label> | 126 | + <label>@{{ field.label }}</label> |
| 121 | - <input v-model="formData[field.value]" @focus="highlightField(field.value)"> | 127 | + <input v-model="formData[field.value]" |
| 128 | + @focus="highlightField(field.value)" | ||
| 129 | + @click="onInputClick(field.value)" | ||
| 130 | + @blur="onInputBlur(field.value)" | ||
| 131 | + :readonly="field.value === 'customer_name' && !hasCustomerNameXY" | ||
| 132 | + > | ||
| 122 | </div> | 133 | </div> |
| 134 | + <button @click="saveTemplate">💾Save</button> | ||
| 123 | </div> | 135 | </div> |
| 124 | 136 | ||
| 125 | </div> | 137 | </div> |
| ... | @@ -136,24 +148,232 @@ | ... | @@ -136,24 +148,232 @@ |
| 136 | isSelecting: false, | 148 | isSelecting: false, |
| 137 | activeIndex: null, | 149 | activeIndex: null, |
| 138 | manualField: "", | 150 | manualField: "", |
| 139 | - formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" }, | 151 | + formData: {}, |
| 140 | - fieldOptions: [ | 152 | + manualBoxData: {}, |
| 141 | - { value: "export_date", label: "Ngày xuất" }, | 153 | + fieldOptions: [], |
| 142 | - { value: "order_code", label: "Mã đơn hàng" }, | 154 | + customer_name_xy: '', |
| 143 | - { value: "customer", label: "Khách hàng" }, | 155 | + hasCustomerNameXY: false, |
| 144 | - { value: "address", label: "Địa chỉ" }, | ||
| 145 | - { value: "staff", label: "Nhân viên" } | ||
| 146 | - ], | ||
| 147 | ocrData: [], | 156 | ocrData: [], |
| 148 | selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 }, | 157 | selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 }, |
| 149 | manualIndex: null | 158 | manualIndex: null |
| 150 | } | 159 | } |
| 151 | }, | 160 | }, |
| 161 | + created() { | ||
| 162 | + // Chỉ tạo formData cho các field cần mapping | ||
| 163 | + this.fieldOptions | ||
| 164 | + .filter(f => f.value !== "template_name") | ||
| 165 | + .forEach(f => { | ||
| 166 | + this.$set(this.formData, f.value, ""); | ||
| 167 | + }); | ||
| 168 | + }, | ||
| 152 | mounted() { | 169 | mounted() { |
| 153 | - this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python | 170 | + this.loadOCRData(); |
| 154 | - this.initData(); | 171 | + |
| 172 | + // Thêm event listener để xóa focus khi click ra ngoài | ||
| 173 | + document.addEventListener('click', (e) => { | ||
| 174 | + // Nếu click không phải vào input hoặc box | ||
| 175 | + if (!e.target.closest('.left-panel') && !e.target.closest('.bbox')) { | ||
| 176 | + this.removeAllFocus(); | ||
| 177 | + } | ||
| 178 | + }); | ||
| 179 | + }, | ||
| 180 | + computed: { | ||
| 181 | + fieldData() { | ||
| 182 | + // Lọc bỏ template_name nếu không cần cho phần form mapping | ||
| 183 | + return this.fieldOptions.filter(f => f.value !== "template_name"); | ||
| 184 | + } | ||
| 155 | }, | 185 | }, |
| 156 | methods: { | 186 | methods: { |
| 187 | + removeManualBoxes() { | ||
| 188 | + this.ocrData = this.ocrData.filter(b => !b.isManual); | ||
| 189 | + this.activeIndex = null; | ||
| 190 | + }, | ||
| 191 | + // Map field cho box (không set active, chỉ dùng để load data từ DB) | ||
| 192 | + mapFieldToBox(index, fieldName, text = null) { | ||
| 193 | + if (index == null) return; | ||
| 194 | + | ||
| 195 | + // Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại | ||
| 196 | + this.ocrData = this.ocrData.filter((box, i) => { | ||
| 197 | + if (i === index) return true; // Giữ lại box hiện tại | ||
| 198 | + if (box.field === fieldName) { | ||
| 199 | + // Xóa box có field trùng lặp | ||
| 200 | + return false; | ||
| 201 | + } | ||
| 202 | + return true; // Giữ lại các box khác | ||
| 203 | + }); | ||
| 204 | + | ||
| 205 | + // Cập nhật lại index sau khi filter | ||
| 206 | + const newIndex = this.ocrData.findIndex(box => box.bbox === this.ocrData[index]?.bbox); | ||
| 207 | + if (newIndex === -1) return; | ||
| 208 | + | ||
| 209 | + // Nếu box này từng gán field khác thì bỏ reset flag và tọa độ liên quan. | ||
| 210 | + const prev = this.ocrData[newIndex].field; | ||
| 211 | + if (prev && prev !== fieldName) { | ||
| 212 | + if (prev === 'customer_name') { | ||
| 213 | + this.hasCustomerNameXY = false; | ||
| 214 | + this.customer_name_xy = ''; | ||
| 215 | + } | ||
| 216 | + this.ocrData[newIndex].field = null; | ||
| 217 | + this.ocrData[newIndex].field_xy = null; | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + // Gán field mới | ||
| 221 | + const bbox = this.ocrData[newIndex].bbox; // tọa độ OCR gốc [x1, y1, x2, y2] | ||
| 222 | + | ||
| 223 | + const x1 = bbox[0]; | ||
| 224 | + const y1 = bbox[1]; | ||
| 225 | + const w = bbox[2]; | ||
| 226 | + const h = bbox[3]; | ||
| 227 | + | ||
| 228 | + const xyStr = `${x1},${y1},${w},${h}`; | ||
| 229 | + | ||
| 230 | + this.ocrData[newIndex].field = fieldName; | ||
| 231 | + this.ocrData[newIndex].field_xy = xyStr; | ||
| 232 | + | ||
| 233 | + // Set text | ||
| 234 | + this.formData[fieldName] = (text !== null ? text : (this.ocrData[newIndex].text || '')).trim(); | ||
| 235 | + | ||
| 236 | + // KHÔNG set active index (không focus) | ||
| 237 | + | ||
| 238 | + // Nếu là customer_name | ||
| 239 | + if (fieldName === 'customer_name') { | ||
| 240 | + this.hasCustomerNameXY = true; | ||
| 241 | + this.customer_name_xy = xyStr; | ||
| 242 | + } | ||
| 243 | + }, | ||
| 244 | + | ||
| 245 | + assignFieldToBox(index, fieldName, text = null) { | ||
| 246 | + console.log(`Assigning field "${fieldName}" to box at index ${index} with text: "${text}"`); | ||
| 247 | + if (index == null) return; | ||
| 248 | + | ||
| 249 | + // 1. Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại | ||
| 250 | + this.ocrData = this.ocrData.filter((box, i) => { | ||
| 251 | + if (i === index) return true; // Giữ lại box hiện tại | ||
| 252 | + if (box.field === fieldName) { | ||
| 253 | + // Xóa box có field trùng lặp | ||
| 254 | + return false; | ||
| 255 | + } | ||
| 256 | + return true; // Giữ lại các box khác | ||
| 257 | + }); | ||
| 258 | + | ||
| 259 | + // 2. Cập nhật lại index sau khi filter | ||
| 260 | + const newIndex = this.ocrData.findIndex(box => box.bbox === this.ocrData[index]?.bbox); | ||
| 261 | + if (newIndex === -1) return; | ||
| 262 | + | ||
| 263 | + // 3. Nếu box này từng gán field khác thì bỏ | ||
| 264 | + const prev = this.ocrData[newIndex]?.field; | ||
| 265 | + if (prev && prev !== fieldName) { | ||
| 266 | + if (prev === 'customer_name') { | ||
| 267 | + this.hasCustomerNameXY = false; | ||
| 268 | + this.customer_name_xy = ''; | ||
| 269 | + } | ||
| 270 | + this.ocrData[newIndex].field = null; | ||
| 271 | + this.ocrData[newIndex].field_xy = null; | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + // 4. Gán field mới | ||
| 275 | + const bbox = this.ocrData[newIndex].bbox; // tọa độ OCR gốc [x1, y1, x2, y2] | ||
| 276 | + console.log('bbox', bbox); | ||
| 277 | + const [x1, y1, w, h] = bbox; | ||
| 278 | + const xyStr = `${x1},${y1},${w},${h}`; | ||
| 279 | + | ||
| 280 | + this.ocrData[newIndex].field = fieldName; | ||
| 281 | + this.ocrData[newIndex].field_xy = xyStr; | ||
| 282 | + | ||
| 283 | + // 5. Gán text | ||
| 284 | + const finalText = text !== null ? text : (this.ocrData[newIndex].text || ''); | ||
| 285 | + this.formData[fieldName] = finalText.trim(); | ||
| 286 | + console.log(`formData ${fieldName}`, this.formData[fieldName]); | ||
| 287 | + | ||
| 288 | + // Nếu trong manualBoxData tồn tại field này hoặc muốn tạo lại manual box từ ocrData | ||
| 289 | + if (this.manualBoxData[fieldName]) { | ||
| 290 | + // Lấy tọa độ + text từ box hiện tại để tạo manual box mới | ||
| 291 | + this.createManualBoxFromDB(fieldName, this.ocrData[newIndex].bbox, finalText); | ||
| 292 | + // Cập nhật manualBoxData | ||
| 293 | + this.manualBoxData[fieldName] = { | ||
| 294 | + coords: this.ocrData[newIndex].bbox, | ||
| 295 | + text: finalText | ||
| 296 | + }; | ||
| 297 | + } | ||
| 298 | + | ||
| 299 | + // 6. Active index | ||
| 300 | + this.activeIndex = newIndex; | ||
| 301 | + // 7. Nếu là customer_name | ||
| 302 | + if (fieldName === 'customer_name') { | ||
| 303 | + this.hasCustomerNameXY = true; | ||
| 304 | + this.customer_name_xy = xyStr; | ||
| 305 | + } | ||
| 306 | + }, | ||
| 307 | + | ||
| 308 | + | ||
| 309 | + async saveTemplate() { | ||
| 310 | + | ||
| 311 | + let customer_name = null; | ||
| 312 | + let customer_coords = null; | ||
| 313 | + let fields = []; | ||
| 314 | + if (this.manualBoxData.customer_name) { | ||
| 315 | + // Lấy từ manualBoxData nếu có | ||
| 316 | + customer_name = this.manualBoxData.customer_name.text; | ||
| 317 | + customer_coords = this.manualBoxData.customer_name.coords.join(','); | ||
| 318 | + fields = this.manualBoxData; | ||
| 319 | + } else { | ||
| 320 | + // Không có → tìm trong ocrData | ||
| 321 | + const found = this.ocrData.find(item => item.field === 'customer_name'); | ||
| 322 | + if (found) { | ||
| 323 | + customer_name = found.text; | ||
| 324 | + customer_coords = found.field_xy; | ||
| 325 | + } | ||
| 326 | + | ||
| 327 | + const fieldsByName = {}; | ||
| 328 | + this.ocrData.forEach(box => { | ||
| 329 | + if (box.field && !box.isDeleted) { | ||
| 330 | + // chỉ giữ 1 bản ghi cuối cùng cho mỗi field (box gần nhất) | ||
| 331 | + fieldsByName[box.field] = { | ||
| 332 | + text: box.field, | ||
| 333 | + coords: box.field_xy || '' | ||
| 334 | + }; | ||
| 335 | + } | ||
| 336 | + }); | ||
| 337 | + // // convert to array | ||
| 338 | + fields = (fieldsByName); | ||
| 339 | + } | ||
| 340 | + | ||
| 341 | + console.log('fields:', fields); | ||
| 342 | + | ||
| 343 | + if (!customer_coords) { | ||
| 344 | + alert("Bạn phải map customer_name (quét/select) trước khi lưu."); | ||
| 345 | + return; | ||
| 346 | + } | ||
| 347 | + | ||
| 348 | + const payload = { | ||
| 349 | + customer_name_text: customer_name || '', | ||
| 350 | + template_name: this.formData.template_name || this.formData.customer_name, | ||
| 351 | + customer_name_xy: customer_coords || [], | ||
| 352 | + fields: fields | ||
| 353 | + }; | ||
| 354 | + // console.log(fields); | ||
| 355 | + try { | ||
| 356 | + const res = await fetch('/ocr/save-template', { | ||
| 357 | + method: 'POST', | ||
| 358 | + headers: { | ||
| 359 | + 'Content-Type': 'application/json', | ||
| 360 | + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content') | ||
| 361 | + }, | ||
| 362 | + body: JSON.stringify(payload) | ||
| 363 | + }); | ||
| 364 | + const json = await res.json(); | ||
| 365 | + if (json.success) { | ||
| 366 | + alert(json.message); | ||
| 367 | + } else { | ||
| 368 | + alert('Save failed'); | ||
| 369 | + } | ||
| 370 | + } catch (err) { | ||
| 371 | + console.error(err); | ||
| 372 | + alert('Save error'); | ||
| 373 | + } | ||
| 374 | + }, | ||
| 375 | + | ||
| 376 | + | ||
| 157 | deleteBox(index) { | 377 | deleteBox(index) { |
| 158 | const item = this.ocrData[index]; | 378 | const item = this.ocrData[index]; |
| 159 | if (item.isManual) { | 379 | if (item.isManual) { |
| ... | @@ -181,20 +401,78 @@ | ... | @@ -181,20 +401,78 @@ |
| 181 | } | 401 | } |
| 182 | }, | 402 | }, |
| 183 | 403 | ||
| 184 | - async initData() { | 404 | + |
| 185 | - await this.loadOCRData(); | ||
| 186 | - }, | ||
| 187 | async loadOCRData() { | 405 | async loadOCRData() { |
| 188 | - const res = await fetch("/public/image/data_picking_detail_1754967679.json"); | 406 | + |
| 189 | - this.ocrData = await res.json(); | 407 | + try { |
| 408 | + const res = await fetch(`/ocr/data-list`); | ||
| 409 | + const data = await res.json(); | ||
| 410 | + | ||
| 411 | + if (data.error) { | ||
| 412 | + console.error('Error loading data:', data.error); | ||
| 413 | + return; | ||
| 414 | + } | ||
| 415 | + | ||
| 416 | + this.ocrData = data.ocrData; | ||
| 417 | + this.pdfImageUrl = data.pdfImageUrl; | ||
| 418 | + this.formData = data.formData; | ||
| 419 | + this.fieldOptions = data.fieldOptions; | ||
| 420 | + this.dataMapping = data.dataMapping; | ||
| 421 | + // Đợi image load xong trước khi xử lý | ||
| 422 | + if (this.$refs.pdfImage && this.$refs.pdfImage.complete) { | ||
| 423 | + this.processLoadedData(); | ||
| 424 | + } else { | ||
| 425 | + console.log('Image not loaded yet, waiting for onImageLoad'); | ||
| 426 | + // Image sẽ được xử lý trong onImageLoad | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + | ||
| 430 | + } catch (error) { | ||
| 431 | + console.error('Error in loadOCRData:', error); | ||
| 432 | + } | ||
| 433 | + }, | ||
| 434 | + | ||
| 435 | + // Xử lý data sau khi image đã load | ||
| 436 | + processLoadedData() { | ||
| 437 | + // Tự động map field cho các box OCR dựa trên formData đã load | ||
| 438 | + this.autoMapFieldsFromFormData(); | ||
| 439 | + // Kiểm tra và sửa lại tọa độ của các box manual | ||
| 440 | + | ||
| 441 | + // Force re-render để đảm bảo các box được vẽ | ||
| 442 | + this.$nextTick(() => { | ||
| 443 | + this.$forceUpdate(); | ||
| 444 | + }); | ||
| 445 | + }, | ||
| 446 | + | ||
| 447 | + // Tự động map field cho các box OCR dựa trên formData đã load từ DB | ||
| 448 | + autoMapFieldsFromFormData() { | ||
| 449 | + this.manualBoxData = {}; // reset | ||
| 450 | + Object.keys(this.dataMapping).forEach(fieldName => { | ||
| 451 | + const { text, coords } = this.dataMapping[fieldName]; | ||
| 452 | + // Ví dụ: tạo box từ dữ liệu | ||
| 453 | + //this.createManualBoxFromDB(fieldName, coords, text); | ||
| 454 | + | ||
| 455 | + this.manualBoxData[fieldName] = { text, coords }; | ||
| 456 | + }); | ||
| 457 | + | ||
| 190 | }, | 458 | }, |
| 459 | + | ||
| 191 | onImageLoad() { | 460 | onImageLoad() { |
| 192 | const img = this.$refs.pdfImage; | 461 | const img = this.$refs.pdfImage; |
| 193 | this.imageWidth = img.naturalWidth; | 462 | this.imageWidth = img.naturalWidth; |
| 194 | this.imageHeight = img.naturalHeight; | 463 | this.imageHeight = img.naturalHeight; |
| 464 | + // Nếu đã có data, xử lý ngay | ||
| 465 | + if (this.ocrData && this.ocrData.length > 0) { | ||
| 466 | + console.log('Image loaded and data exists, processing now'); | ||
| 467 | + this.processLoadedData(); | ||
| 468 | + } else { | ||
| 469 | + console.log('Image loaded but no data yet'); | ||
| 470 | + } | ||
| 195 | }, | 471 | }, |
| 196 | getBoxStyle(item, index) { | 472 | getBoxStyle(item, index) { |
| 197 | - if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {}; | 473 | + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) { |
| 474 | + return {}; | ||
| 475 | + } | ||
| 198 | 476 | ||
| 199 | const [x1, y1, x2, y2] = item.bbox; | 477 | const [x1, y1, x2, y2] = item.bbox; |
| 200 | const displayedWidth = this.$refs.pdfImage.clientWidth; | 478 | const displayedWidth = this.$refs.pdfImage.clientWidth; |
| ... | @@ -203,22 +481,60 @@ | ... | @@ -203,22 +481,60 @@ |
| 203 | const scaleX = displayedWidth / this.imageWidth; | 481 | const scaleX = displayedWidth / this.imageWidth; |
| 204 | const scaleY = displayedHeight / this.imageHeight; | 482 | const scaleY = displayedHeight / this.imageHeight; |
| 205 | 483 | ||
| 484 | + const left = Math.round(x1 * scaleX); | ||
| 485 | + const top = Math.round(y1 * scaleY); | ||
| 486 | + const width = Math.round((x2 - x1) * scaleX); | ||
| 487 | + const height = Math.round((y2 - y1) * scaleY); | ||
| 488 | + | ||
| 206 | return { | 489 | return { |
| 207 | position: 'absolute', | 490 | position: 'absolute', |
| 208 | - left: `${Math.round(x1 * scaleX)}px`, | 491 | + left: `${left}px`, |
| 209 | - top: `${Math.round(y1 * scaleY)}px`, | 492 | + top: `${top}px`, |
| 210 | - width: `${Math.round((x2 - x1) * scaleX)}px`, | 493 | + width: `${width}px`, |
| 211 | - height: `${Math.round((y2 - y1) * scaleY)}px`, | 494 | + height: `${height}px`, |
| 212 | - border: item.hideBorder ? 'none' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'), | 495 | + border: item.hideBorder ? '2px solid #ccc' : '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', | 496 | boxSizing: 'border-box', |
| 215 | cursor: 'pointer', | 497 | cursor: 'pointer', |
| 216 | zIndex: item.isManual ? 30 : 10 | 498 | zIndex: item.isManual ? 30 : 10 |
| 217 | }; | 499 | }; |
| 218 | }, | 500 | }, |
| 219 | - | ||
| 220 | highlightField(field) { | 501 | highlightField(field) { |
| 221 | - // tìm box gần nhất match field này | 502 | + let coords, text; |
| 503 | + let isFromDB = false; | ||
| 504 | + | ||
| 505 | + // Kiểm tra xem field này có phải từ DB không | ||
| 506 | + if (this.dataMapping && this.dataMapping[field]) { | ||
| 507 | + isFromDB = true; | ||
| 508 | + coords = this.dataMapping[field].coords; | ||
| 509 | + text = this.dataMapping[field].text; | ||
| 510 | + console.log(`Using dataMapping for field "${field}":`, coords, text); | ||
| 511 | + } else { | ||
| 512 | + // Kiểm tra xem ocrData đã có box nào với field này chưa | ||
| 513 | + const existingBox = this.ocrData.find(b => b.field === field && !b.isDeleted); | ||
| 514 | + if (existingBox) { | ||
| 515 | + coords = existingBox.bbox; | ||
| 516 | + text = existingBox.text; | ||
| 517 | + console.log(`Using existing box for field "${field}":`, coords, text); | ||
| 518 | + } else if (this.manualBoxData[field]) { | ||
| 519 | + // Nếu không có trong ocrData, dùng manualBoxData (tọa độ mới) | ||
| 520 | + coords = this.manualBoxData[field].coords; | ||
| 521 | + text = this.manualBoxData[field].text; | ||
| 522 | + console.log(`Using manualBoxData (new coordinates) for field "${field}":`, coords, text); | ||
| 523 | + } | ||
| 524 | + } | ||
| 525 | + | ||
| 526 | + // Nếu có coords thì tạo hoặc hiển thị lại box | ||
| 527 | + if (coords) { | ||
| 528 | + if (isFromDB) { | ||
| 529 | + // Tạo box manual từ DB (không có nút xóa) | ||
| 530 | + this.createManualBoxFromDB(field, coords, text); | ||
| 531 | + } else { | ||
| 532 | + // Hiển thị lại box manual đã quét chọn (có nút xóa) | ||
| 533 | + this.showManualBox(field, coords, text); | ||
| 534 | + } | ||
| 535 | + } | ||
| 536 | + | ||
| 537 | + // Tìm lại index của box để set active | ||
| 222 | let idx = -1; | 538 | let idx = -1; |
| 223 | for (let i = this.ocrData.length - 1; i >= 0; i--) { | 539 | for (let i = this.ocrData.length - 1; i >= 0; i--) { |
| 224 | const it = this.ocrData[i]; | 540 | const it = this.ocrData[i]; |
| ... | @@ -227,9 +543,131 @@ | ... | @@ -227,9 +543,131 @@ |
| 227 | break; | 543 | break; |
| 228 | } | 544 | } |
| 229 | } | 545 | } |
| 230 | - this.activeIndex = idx; // nếu không tìm thấy thì = -1 | 546 | + |
| 547 | + if (idx !== -1) { | ||
| 548 | + this.activeIndex = idx; | ||
| 549 | + this.scrollToBox(idx); | ||
| 550 | + // Reset selectingIndex để không hiển thị dropdown khi highlight từ input | ||
| 551 | + this.selectingIndex = null; | ||
| 552 | + } else { | ||
| 553 | + this.activeIndex = null; | ||
| 554 | + } | ||
| 231 | }, | 555 | }, |
| 232 | 556 | ||
| 557 | + | ||
| 558 | + // Scroll đến box tương ứng | ||
| 559 | + scrollToBox(index) { | ||
| 560 | + if (!this.$refs.pdfContainer || index < 0 || index >= this.ocrData.length) return; | ||
| 561 | + | ||
| 562 | + const item = this.ocrData[index]; | ||
| 563 | + if (!item || item.isDeleted) return; | ||
| 564 | + | ||
| 565 | + // Tính vị trí hiển thị của box | ||
| 566 | + const [x1, y1, x2, y2] = item.bbox; | ||
| 567 | + //const [x1, y1, x2, y2] = item.field_xy.split(',').map(Number); | ||
| 568 | + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return; | ||
| 569 | + | ||
| 570 | + const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 571 | + const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 572 | + const scaleX = displayedWidth / this.imageWidth; | ||
| 573 | + const scaleY = displayedHeight / this.imageHeight; | ||
| 574 | + | ||
| 575 | + const displayX = Math.round(x1 * scaleX); | ||
| 576 | + const displayY = Math.round(y1 * scaleY); | ||
| 577 | + | ||
| 578 | + // Scroll đến vị trí box | ||
| 579 | + const container = this.$refs.pdfContainer; | ||
| 580 | + const containerRect = container.getBoundingClientRect(); | ||
| 581 | + const scrollTop = container.scrollTop; | ||
| 582 | + const scrollLeft = container.scrollLeft; | ||
| 583 | + | ||
| 584 | + // Tính vị trí scroll để box nằm ở giữa viewport | ||
| 585 | + const targetScrollTop = scrollTop + displayY - (containerRect.height / 2); | ||
| 586 | + const targetScrollLeft = scrollLeft + displayX - (containerRect.width / 2); | ||
| 587 | + | ||
| 588 | + container.scrollTo({ | ||
| 589 | + top: Math.max(0, targetScrollTop), | ||
| 590 | + left: Math.max(0, targetScrollLeft), | ||
| 591 | + behavior: 'smooth' | ||
| 592 | + }); | ||
| 593 | + }, | ||
| 594 | + | ||
| 595 | + | ||
| 596 | + | ||
| 597 | + // Xử lý khi click vào input | ||
| 598 | + onInputClick(fieldName) { | ||
| 599 | + // Kiểm tra xem field này có data không | ||
| 600 | + const fieldValue = this.formData[fieldName]; | ||
| 601 | + if (fieldValue && fieldValue.trim()) { | ||
| 602 | + // Nếu có data từ DB, highlight và focus vào box tương ứng | ||
| 603 | + this.highlightField(fieldName); | ||
| 604 | + } | ||
| 605 | + }, | ||
| 606 | + | ||
| 607 | + // Xử lý khi click ra ngoài input (blur) | ||
| 608 | + onInputBlur(fieldName) { | ||
| 609 | + // Khi không focus vào input nào, xóa tất cả focus | ||
| 610 | + this.removeAllFocus(); | ||
| 611 | + }, | ||
| 612 | + | ||
| 613 | + // Xóa tất cả focus | ||
| 614 | + removeAllFocus() { | ||
| 615 | + // Reset active index (chỉ mất màu xanh, không xóa box) | ||
| 616 | + this.activeIndex = null; | ||
| 617 | + | ||
| 618 | + // Ẩn hoàn toàn các box manual được tạo từ DB (không phải quét chọn thủ công) | ||
| 619 | + this.ocrData.forEach(item => { | ||
| 620 | + if (item.isManual && !item.showDelete) { | ||
| 621 | + // Box manual từ DB (không có nút xóa) - ẩn hoàn toàn | ||
| 622 | + item.isDeleted = true; | ||
| 623 | + } | ||
| 624 | + }); | ||
| 625 | + | ||
| 626 | + // Đảm bảo tất cả box OCR đều hiển thị (chỉ ẩn border khi cần thiết) | ||
| 627 | + this.ocrData.forEach(item => { | ||
| 628 | + if (!item.isManual && item.hideBorder) { | ||
| 629 | + // Chỉ ẩn border cho box OCR nằm trong vùng manual | ||
| 630 | + item.hideBorder = true; | ||
| 631 | + } | ||
| 632 | + }); | ||
| 633 | + }, | ||
| 634 | + | ||
| 635 | + // Xử lý khi click vào box | ||
| 636 | + onBoxClick(index) { | ||
| 637 | + const item = this.ocrData[index]; | ||
| 638 | + | ||
| 639 | + // Kiểm tra xem field này có phải từ DB không | ||
| 640 | + const isFromDB = this.dataMapping && this.dataMapping[item.field]; | ||
| 641 | + | ||
| 642 | + // Kiểm tra xem data có được ghi đè không (so sánh với data gốc từ DB) | ||
| 643 | + const isDataOverridden = item.field && isFromDB && | ||
| 644 | + this.formData[item.field] !== this.dataMapping[item.field].text; | ||
| 645 | + | ||
| 646 | + if (item.isManual) { | ||
| 647 | + // Manual box | ||
| 648 | + if (isFromDB && !isDataOverridden) { | ||
| 649 | + // Manual box từ DB chưa ghi đè, KHÔNG cho chọn option | ||
| 650 | + this.activeIndex = index; | ||
| 651 | + this.selectingIndex = null; | ||
| 652 | + } else { | ||
| 653 | + // Manual box từ DB có data ghi đè HOẶC manual box bình thường, CHO PHÉP chọn option | ||
| 654 | + this.selectingIndex = index; | ||
| 655 | + } | ||
| 656 | + } else if (item.field) { | ||
| 657 | + // Box OCR có field | ||
| 658 | + if (isFromDB && !isDataOverridden) { | ||
| 659 | + // Box có field từ DB chưa ghi đè, KHÔNG cho chọn option | ||
| 660 | + this.activeIndex = index; | ||
| 661 | + this.selectingIndex = null; | ||
| 662 | + } else { | ||
| 663 | + // Box có field từ DB đã ghi đè HOẶC field mới, CHO PHÉP chọn option | ||
| 664 | + this.selectingIndex = index; | ||
| 665 | + } | ||
| 666 | + } else { | ||
| 667 | + // Box OCR thông thường (chưa có field), cho phép hiển thị dropdown | ||
| 668 | + this.selectingIndex = index; | ||
| 669 | + } | ||
| 670 | + }, | ||
| 233 | startSelect(e) { | 671 | startSelect(e) { |
| 234 | if (this.isMappingManually || e.button !== 0) return; | 672 | if (this.isMappingManually || e.button !== 0) return; |
| 235 | this.isSelecting = true; | 673 | this.isSelecting = true; |
| ... | @@ -271,7 +709,7 @@ | ... | @@ -271,7 +709,7 @@ |
| 271 | const dispX2 = this.selectBox.x + this.selectBox.width; | 709 | const dispX2 = this.selectBox.x + this.selectBox.width; |
| 272 | const dispY2 = this.selectBox.y + this.selectBox.height; | 710 | const dispY2 = this.selectBox.y + this.selectBox.height; |
| 273 | 711 | ||
| 274 | - // scale: displayed -> original | 712 | + // scale: displayed -> original (sửa lại để chính xác hơn) |
| 275 | const displayedWidth = this.$refs.pdfImage.clientWidth; | 713 | const displayedWidth = this.$refs.pdfImage.clientWidth; |
| 276 | const displayedHeight = this.$refs.pdfImage.clientHeight; | 714 | const displayedHeight = this.$refs.pdfImage.clientHeight; |
| 277 | const scaleX = this.imageWidth / displayedWidth; | 715 | const scaleX = this.imageWidth / displayedWidth; |
| ... | @@ -309,67 +747,147 @@ | ... | @@ -309,67 +747,147 @@ |
| 309 | 747 | ||
| 310 | e.stopPropagation(); | 748 | e.stopPropagation(); |
| 311 | e.preventDefault(); | 749 | e.preventDefault(); |
| 750 | + | ||
| 312 | } | 751 | } |
| 313 | , | 752 | , |
| 314 | applyMapping() { | 753 | applyMapping() { |
| 315 | const item = this.ocrData[this.selectingIndex]; | 754 | const item = this.ocrData[this.selectingIndex]; |
| 755 | + if (!item) return; | ||
| 316 | 756 | ||
| 317 | - if (item && item.isManual) { | 757 | + if (item.isManual) { |
| 758 | + // Nếu là manual box, chuyển sang chế độ manual mapping | ||
| 318 | this.manualIndex = this.selectingIndex; | 759 | this.manualIndex = this.selectingIndex; |
| 319 | this.manualField = item.field || ""; | 760 | this.manualField = item.field || ""; |
| 320 | this.applyManualMapping(); | 761 | this.applyManualMapping(); |
| 321 | return; | 762 | return; |
| 322 | } | 763 | } |
| 323 | 764 | ||
| 765 | + // Xử lý box OCR (có thể chưa có field hoặc đã có field) | ||
| 324 | if (item.field) { | 766 | if (item.field) { |
| 325 | - this.formData[item.field] = item.text; | 767 | + // Nếu box đã có field, cập nhật lại |
| 768 | + const oldField = item.field; | ||
| 769 | + | ||
| 770 | + // Cập nhật formData để hiển thị trong input | ||
| 771 | + this.formData[item.field] = item.text || ''; | ||
| 772 | + | ||
| 773 | + // Cập nhật manualBoxData với tọa độ mới (box OCR không có nút xóa) | ||
| 774 | + this.manualBoxData[item.field] = { | ||
| 775 | + coords: item.bbox, | ||
| 776 | + text: item.text || '', | ||
| 777 | + isFromOCR: true // Đánh dấu đây là box OCR, không phải quét chọn | ||
| 778 | + }; | ||
| 779 | + | ||
| 780 | + // Xóa dataMapping cũ để tránh focus về tọa độ cũ | ||
| 781 | + if (this.dataMapping && this.dataMapping[item.field]) { | ||
| 782 | + delete this.dataMapping[item.field]; | ||
| 783 | + } | ||
| 784 | + | ||
| 785 | + // Set active index | ||
| 326 | this.activeIndex = this.selectingIndex; | 786 | this.activeIndex = this.selectingIndex; |
| 787 | + | ||
| 788 | + console.log(`Updated field "${item.field}" for box OCR at index ${this.selectingIndex} with new coordinates`); | ||
| 327 | } | 789 | } |
| 790 | + | ||
| 328 | this.selectingIndex = null; | 791 | this.selectingIndex = null; |
| 329 | }, | 792 | }, |
| 330 | applyManualMapping() { | 793 | applyManualMapping() { |
| 331 | if (!this.manualField) return; | 794 | if (!this.manualField) return; |
| 332 | const manualIndex = this.manualIndex; | 795 | const manualIndex = this.manualIndex; |
| 796 | + | ||
| 333 | const newBbox = this.ocrData[manualIndex].bbox; | 797 | const newBbox = this.ocrData[manualIndex].bbox; |
| 334 | 798 | ||
| 335 | let combinedText = []; | 799 | let combinedText = []; |
| 800 | + let foundItems = []; | ||
| 801 | + | ||
| 802 | + // Tìm tất cả các box OCR nằm trong vùng manual | ||
| 336 | this.ocrData.forEach(item => { | 803 | this.ocrData.forEach(item => { |
| 337 | if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) { | 804 | if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) { |
| 338 | - const partial = this.getPartialText(item.text, item.bbox, newBbox); | 805 | + foundItems.push({ |
| 339 | - if (partial) combinedText.push(partial); | 806 | + text: item.text, |
| 340 | - // combinedText.push(item.text.trim()); | 807 | + bbox: item.bbox, |
| 808 | + index: this.ocrData.indexOf(item) | ||
| 809 | + }); | ||
| 341 | } | 810 | } |
| 342 | }); | 811 | }); |
| 343 | 812 | ||
| 813 | + // Sắp xếp các item theo vị trí (từ trái sang phải, từ trên xuống dưới) | ||
| 814 | + foundItems.sort((a, b) => { | ||
| 815 | + // Ưu tiên theo Y trước (hàng), sau đó theo X (cột) | ||
| 816 | + if (Math.abs(a.bbox[1] - b.bbox[1]) < 20) { // Cùng hàng (tolerance 20px) | ||
| 817 | + return a.bbox[0] - b.bbox[0]; // Sắp xếp theo X | ||
| 818 | + } | ||
| 819 | + return a.bbox[1] - b.bbox[1]; // Sắp xếp theo Y | ||
| 820 | + }); | ||
| 821 | + | ||
| 822 | + // Gộp text theo thứ tự đã sắp xếp | ||
| 823 | + foundItems.forEach(item => { | ||
| 824 | + combinedText.push(item.text.trim()); | ||
| 825 | + }); | ||
| 826 | + | ||
| 344 | const finalText = combinedText.join(" "); | 827 | const finalText = combinedText.join(" "); |
| 828 | + console.log('Combined text:', finalText); | ||
| 829 | + | ||
| 830 | + // Gán field và text cho box manual | ||
| 831 | + console.log(`Assigning manual field "${this.manualField}" to box at index ${manualIndex} with text: "${finalText}"`); | ||
| 832 | + | ||
| 833 | + // Gán field trực tiếp cho box manual | ||
| 345 | this.ocrData[manualIndex].field = this.manualField; | 834 | this.ocrData[manualIndex].field = this.manualField; |
| 346 | - this.formData[this.manualField] = finalText; | ||
| 347 | - this.activeIndex = manualIndex; | ||
| 348 | 835 | ||
| 349 | - console.log('manualField', this.manualField, this.manualIndex) | 836 | + // Cập nhật formData để hiển thị trong input |
| 837 | + this.formData[this.manualField] = finalText.trim(); | ||
| 838 | + | ||
| 839 | + // Cập nhật manualBoxData (box quét chọn có nút xóa) | ||
| 840 | + this.manualBoxData[this.manualField] = { | ||
| 841 | + coords: newBbox, | ||
| 842 | + text: finalText.trim(), | ||
| 843 | + isFromOCR: false // Đánh dấu đây là box quét chọn, không phải OCR | ||
| 844 | + }; | ||
| 845 | + | ||
| 846 | + // Xóa dataMapping cũ để tránh focus về tọa độ cũ | ||
| 847 | + if (this.dataMapping && this.dataMapping[this.manualField]) { | ||
| 848 | + delete this.dataMapping[this.manualField]; | ||
| 849 | + } | ||
| 850 | + | ||
| 350 | // Reset trạng thái chọn | 851 | // Reset trạng thái chọn |
| 351 | this.isMappingManually = false; | 852 | this.isMappingManually = false; |
| 352 | this.selectBox.show = false; | 853 | this.selectBox.show = false; |
| 353 | this.selectBox.showDropdown = false; | 854 | this.selectBox.showDropdown = false; |
| 354 | - // this.manualField = ""; | 855 | + this.manualField = ""; |
| 355 | - // this.manualIndex = null; | 856 | + this.manualIndex = null; |
| 356 | }, | 857 | }, |
| 357 | 858 | ||
| 358 | isBoxInside(inner, outer) { | 859 | isBoxInside(inner, outer) { |
| 359 | - return !( | 860 | + // inner: bbox của OCR item [x1, y1, x2, y2] |
| 861 | + // outer: bbox của vùng manual [x1, y1, x2, y2] | ||
| 862 | + | ||
| 863 | + // Kiểm tra xem box OCR có nằm hoàn toàn trong vùng manual không | ||
| 864 | + const isFullyInside = ( | ||
| 865 | + inner[0] >= outer[0] && // left edge | ||
| 866 | + inner[1] >= outer[1] && // top edge | ||
| 867 | + inner[2] <= outer[2] && // right edge | ||
| 868 | + inner[3] <= outer[3] // bottom edge | ||
| 869 | + ); | ||
| 870 | + | ||
| 871 | + // Kiểm tra xem box OCR có giao nhau với vùng manual không | ||
| 872 | + const isOverlapping = !( | ||
| 360 | inner[2] < outer[0] || // box bên trái vùng chọn | 873 | 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 | 874 | 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 | 875 | 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 | 876 | inner[1] > outer[3] // box phía dưới vùng chọn |
| 364 | ); | 877 | ); |
| 878 | + | ||
| 879 | + // Trả về true nếu box OCR nằm hoàn toàn trong hoặc giao nhau đáng kể | ||
| 880 | + return isFullyInside || isOverlapping; | ||
| 365 | }, | 881 | }, |
| 366 | 882 | ||
| 883 | + | ||
| 367 | getPartialText(text, bbox, selectBbox) { | 884 | getPartialText(text, bbox, selectBbox) { |
| 368 | const [x1, y1, x2, y2] = bbox; | 885 | const [x1, y1, x2, y2] = bbox; |
| 369 | const [sx1, sy1, sx2, sy2] = selectBbox; | 886 | const [sx1, sy1, sx2, sy2] = selectBbox; |
| 370 | 887 | ||
| 371 | // Chiều rộng box OCR | 888 | // Chiều rộng box OCR |
| 372 | const boxWidth = x2 - x1; | 889 | const boxWidth = x2 - x1; |
| 890 | + const boxHeight = y2 - y1; | ||
| 373 | 891 | ||
| 374 | // Vị trí start và end tương đối trong text | 892 | // Vị trí start và end tương đối trong text |
| 375 | let startRatio = Math.max(0, (sx1 - x1) / boxWidth); | 893 | let startRatio = Math.max(0, (sx1 - x1) / boxWidth); |
| ... | @@ -395,6 +913,105 @@ | ... | @@ -395,6 +913,105 @@ |
| 395 | top: `${Math.round(y2 * scaleY)}px`, | 913 | top: `${Math.round(y2 * scaleY)}px`, |
| 396 | zIndex: 9999 | 914 | zIndex: 9999 |
| 397 | }; | 915 | }; |
| 916 | + }, | ||
| 917 | + | ||
| 918 | + // Tạo box manual từ tọa độ trong DB | ||
| 919 | + createManualBoxFromDB(fieldName, coordinates, text) { | ||
| 920 | + if (!this.imageWidth || !this.imageHeight) { | ||
| 921 | + console.log('Cannot create manual box: Image not loaded'); | ||
| 922 | + return; | ||
| 923 | + } | ||
| 924 | + | ||
| 925 | + // Parse coordinates từ string "x1,y1,x2,y2" | ||
| 926 | + let coords; | ||
| 927 | + if (typeof coordinates === 'string') { | ||
| 928 | + coords = coordinates.split(',').map(Number); | ||
| 929 | + } else if (Array.isArray(coordinates)) { | ||
| 930 | + coords = coordinates; | ||
| 931 | + } else { | ||
| 932 | + console.error('Invalid coordinates format:', coordinates); | ||
| 933 | + return; | ||
| 934 | + } | ||
| 935 | + | ||
| 936 | + const [x1, y1, x2, y2] = coords; | ||
| 937 | + | ||
| 938 | + // Kiểm tra tọa độ có hợp lệ không | ||
| 939 | + if (x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1 && | ||
| 940 | + x2 <= this.imageWidth && y2 <= this.imageHeight) { | ||
| 941 | + | ||
| 942 | + // Xóa box cũ có cùng fieldName trước khi tạo mới (chỉ xóa manual box) | ||
| 943 | + this.ocrData = this.ocrData.filter(box => !(box.field === fieldName && box.isManual)); | ||
| 944 | + | ||
| 945 | + // Tạo box manual từ DB (không có nút xóa) | ||
| 946 | + const manualBox = { | ||
| 947 | + text: text || '', | ||
| 948 | + bbox: coords, | ||
| 949 | + field: fieldName, | ||
| 950 | + isManual: true, | ||
| 951 | + showDelete: false, | ||
| 952 | + isDeleted: false, | ||
| 953 | + hideBorder: true | ||
| 954 | + }; | ||
| 955 | + | ||
| 956 | + this.ocrData.push(manualBox); | ||
| 957 | + // console.log('Manual box created successfully:', manualBox); | ||
| 958 | + | ||
| 959 | + // Force re-render | ||
| 960 | + this.$forceUpdate(); | ||
| 961 | + } else { | ||
| 962 | + console.warn('Invalid coordinates for manual box:', coords); | ||
| 963 | + } | ||
| 964 | + }, | ||
| 965 | + | ||
| 966 | + // Hiển thị lại box manual đã quét chọn (có nút xóa) | ||
| 967 | + showManualBox(fieldName, coordinates, text) { | ||
| 968 | + if (!this.imageWidth || !this.imageHeight) { | ||
| 969 | + console.log('Cannot show manual box: Image not loaded'); | ||
| 970 | + return; | ||
| 971 | + } | ||
| 972 | + | ||
| 973 | + // Parse coordinates | ||
| 974 | + let coords; | ||
| 975 | + if (typeof coordinates === 'string') { | ||
| 976 | + coords = coordinates.split(',').map(Number); | ||
| 977 | + } else if (Array.isArray(coordinates)) { | ||
| 978 | + coords = coordinates; | ||
| 979 | + } else { | ||
| 980 | + console.error('Invalid coordinates format:', coordinates); | ||
| 981 | + return; | ||
| 982 | + } | ||
| 983 | + | ||
| 984 | + const [x1, y1, x2, y2] = coords; | ||
| 985 | + | ||
| 986 | + // Kiểm tra tọa độ có hợp lệ không | ||
| 987 | + if (x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1 && | ||
| 988 | + x2 <= this.imageWidth && y2 <= this.imageHeight) { | ||
| 989 | + | ||
| 990 | + // Xóa box cũ có cùng fieldName trước khi hiển thị lại | ||
| 991 | + this.ocrData = this.ocrData.filter(box => !(box.field === fieldName && box.isManual)); | ||
| 992 | + | ||
| 993 | + // Kiểm tra xem đây có phải box quét chọn hay box OCR | ||
| 994 | + const isFromOCR = this.manualBoxData[fieldName] && this.manualBoxData[fieldName].isFromOCR; | ||
| 995 | + | ||
| 996 | + // Hiển thị lại box manual (có nút xóa nếu là quét chọn, không có nút xóa nếu là OCR) | ||
| 997 | + const manualBox = { | ||
| 998 | + text: text || '', | ||
| 999 | + bbox: coords, | ||
| 1000 | + field: fieldName, | ||
| 1001 | + isManual: true, | ||
| 1002 | + showDelete: !isFromOCR, // Chỉ hiển thị nút xóa nếu KHÔNG phải từ OCR | ||
| 1003 | + isDeleted: false, | ||
| 1004 | + hideBorder: false | ||
| 1005 | + }; | ||
| 1006 | + | ||
| 1007 | + this.ocrData.push(manualBox); | ||
| 1008 | + console.log(`Manual box shown successfully: ${isFromOCR ? 'from OCR (no delete btn)' : 'from manual selection (with delete btn)'}`); | ||
| 1009 | + | ||
| 1010 | + // Force re-render | ||
| 1011 | + this.$forceUpdate(); | ||
| 1012 | + } else { | ||
| 1013 | + console.warn('Invalid coordinates for manual box:', coords); | ||
| 1014 | + } | ||
| 398 | } | 1015 | } |
| 399 | 1016 | ||
| 400 | } | 1017 | } | ... | ... |
-
Please register or sign in to post a comment