Showing
2 changed files
with
216 additions
and
6 deletions
| ... | @@ -20,7 +20,7 @@ class OcrController extends Controller | ... | @@ -20,7 +20,7 @@ class OcrController extends Controller |
| 20 | $request->validate([ | 20 | $request->validate([ |
| 21 | 'customer_name_text' => 'required|string', | 21 | 'customer_name_text' => 'required|string', |
| 22 | 'customer_name_xy' => 'required|string', | 22 | 'customer_name_xy' => 'required|string', |
| 23 | - 'tpl_name' => 'unique:mst_template', | 23 | + 'template_name' => 'unique:mst_template,tpl_name', |
| 24 | ]); | 24 | ]); |
| 25 | 25 | ||
| 26 | // Lưu vào bảng mst_template | 26 | // Lưu vào bảng mst_template |
| ... | @@ -50,7 +50,7 @@ class OcrController extends Controller | ... | @@ -50,7 +50,7 @@ class OcrController extends Controller |
| 50 | $imgPath = ("image/data_picking_detail_1754967679.jpg"); | 50 | $imgPath = ("image/data_picking_detail_1754967679.jpg"); |
| 51 | 51 | ||
| 52 | 52 | ||
| 53 | - $templateName = 'nemo_4'; | 53 | + $templateName = 'nemo'; |
| 54 | /// Lấy từ request hoặc mặc định | 54 | /// Lấy từ request hoặc mặc định |
| 55 | 55 | ||
| 56 | 56 | ... | ... |
| ... | @@ -21,8 +21,20 @@ | ... | @@ -21,8 +21,20 @@ |
| 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 | + .bbox.focus-highlight { | ||
| 29 | + animation: focusPulse 2s ease-in-out; | ||
| 30 | + border-color: #ff6b35 !important; | ||
| 31 | + background-color: rgba(255, 107, 53, 0.4) !important; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + @keyframes focusPulse { | ||
| 35 | + 0% { transform: scale(1); } | ||
| 36 | + 50% { transform: scale(1.05); } | ||
| 37 | + 100% { transform: scale(1); } | ||
| 26 | } | 38 | } |
| 27 | select { | 39 | select { |
| 28 | position: absolute; | 40 | position: absolute; |
| ... | @@ -120,6 +132,7 @@ | ... | @@ -120,6 +132,7 @@ |
| 120 | <label>@{{ field.label }}</label> | 132 | <label>@{{ field.label }}</label> |
| 121 | <input v-model="formData[field.value]" | 133 | <input v-model="formData[field.value]" |
| 122 | @focus="highlightField(field.value)" | 134 | @focus="highlightField(field.value)" |
| 135 | + @click="onInputClick(field.value)" | ||
| 123 | :readonly="field.value === 'customer_name' && !hasCustomerNameXY" | 136 | :readonly="field.value === 'customer_name' && !hasCustomerNameXY" |
| 124 | > | 137 | > |
| 125 | </div> | 138 | </div> |
| ... | @@ -167,6 +180,56 @@ | ... | @@ -167,6 +180,56 @@ |
| 167 | } | 180 | } |
| 168 | }, | 181 | }, |
| 169 | methods: { | 182 | methods: { |
| 183 | + // Map field cho box (không set active, chỉ dùng để load data từ DB) | ||
| 184 | + mapFieldToBox(index, fieldName, text = null) { | ||
| 185 | + if (index == null) return; | ||
| 186 | + | ||
| 187 | + // Xóa fieldName ở box khác | ||
| 188 | + this.ocrData.forEach((box, i) => { | ||
| 189 | + if (i !== index && box.field === fieldName) { | ||
| 190 | + box.field = null; | ||
| 191 | + box.field_xy = null; | ||
| 192 | + } | ||
| 193 | + }); | ||
| 194 | + | ||
| 195 | + // Nếu box này từng gán field khác thì bỏ | ||
| 196 | + const prev = this.ocrData[index].field; | ||
| 197 | + if (prev && prev !== fieldName) { | ||
| 198 | + if (prev === 'customer_name') { | ||
| 199 | + this.hasCustomerNameXY = false; | ||
| 200 | + this.customer_name_xy = ''; | ||
| 201 | + } | ||
| 202 | + this.ocrData[index].field = null; | ||
| 203 | + this.ocrData[index].field_xy = null; | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + // Gán field mới | ||
| 207 | + const bbox = this.ocrData[index].bbox; // tọa độ OCR gốc [x1, y1, x2, y2] | ||
| 208 | + | ||
| 209 | + console.log('1111111111111111',bbox); | ||
| 210 | + const x1 = bbox[0]; | ||
| 211 | + const y1 = bbox[1]; | ||
| 212 | + const w = bbox[2]; | ||
| 213 | + const h = bbox[3]; | ||
| 214 | + | ||
| 215 | + const xyStr = `${x1},${y1},${w},${h}`; | ||
| 216 | + | ||
| 217 | + this.ocrData[index].field = fieldName; | ||
| 218 | + this.ocrData[index].field_xy = xyStr; | ||
| 219 | + | ||
| 220 | + // Set text | ||
| 221 | + this.formData[fieldName] = (text !== null ? text : (this.ocrData[index].text || '')).trim(); | ||
| 222 | + | ||
| 223 | + // KHÔNG set active index (không focus) | ||
| 224 | + | ||
| 225 | + // Nếu là customer_name | ||
| 226 | + if (fieldName === 'customer_name') { | ||
| 227 | + this.hasCustomerNameXY = true; | ||
| 228 | + this.customer_name_xy = xyStr; | ||
| 229 | + } | ||
| 230 | + }, | ||
| 231 | + | ||
| 232 | + // Assign field và set active (dùng khi user tương tác) | ||
| 170 | assignFieldToBox(index, fieldName, text = null) { | 233 | assignFieldToBox(index, fieldName, text = null) { |
| 171 | if (index == null) return; | 234 | if (index == null) return; |
| 172 | 235 | ||
| ... | @@ -206,7 +269,7 @@ | ... | @@ -206,7 +269,7 @@ |
| 206 | // Set text | 269 | // Set text |
| 207 | this.formData[fieldName] = (text !== null ? text : (this.ocrData[index].text || '')).trim(); | 270 | this.formData[fieldName] = (text !== null ? text : (this.ocrData[index].text || '')).trim(); |
| 208 | 271 | ||
| 209 | - // Active index | 272 | + // Active index (focus vào box này) |
| 210 | this.activeIndex = index; | 273 | this.activeIndex = index; |
| 211 | 274 | ||
| 212 | // Nếu là customer_name | 275 | // Nếu là customer_name |
| ... | @@ -309,6 +372,77 @@ | ... | @@ -309,6 +372,77 @@ |
| 309 | this.pdfImageUrl = data.pdfImageUrl; | 372 | this.pdfImageUrl = data.pdfImageUrl; |
| 310 | this.formData = data.formData; | 373 | this.formData = data.formData; |
| 311 | this.fieldOptions = data.fieldOptions; | 374 | this.fieldOptions = data.fieldOptions; |
| 375 | + | ||
| 376 | + // Tự động map field cho các box OCR dựa trên formData đã load | ||
| 377 | + this.autoMapFieldsFromFormData(); | ||
| 378 | + }, | ||
| 379 | + | ||
| 380 | + // Tự động map field cho các box OCR dựa trên formData đã load từ DB | ||
| 381 | + autoMapFieldsFromFormData() { | ||
| 382 | + // Duyệt qua tất cả các field trong formData | ||
| 383 | + Object.keys(this.formData).forEach(fieldName => { | ||
| 384 | + const fieldValue = this.formData[fieldName]; | ||
| 385 | + | ||
| 386 | + // Chỉ xử lý các field có giá trị (không phải template_name) | ||
| 387 | + if (fieldValue && fieldValue.trim() && fieldName !== 'template_name') { | ||
| 388 | + // Tìm box OCR phù hợp nhất để map | ||
| 389 | + const bestMatchIndex = this.findBestMatchingBox(fieldName, fieldValue); | ||
| 390 | + | ||
| 391 | + if (bestMatchIndex !== -1) { | ||
| 392 | + // Chỉ map field, không set active (không focus) | ||
| 393 | + this.mapFieldToBox(bestMatchIndex, fieldName, fieldValue); | ||
| 394 | + } | ||
| 395 | + } | ||
| 396 | + }); | ||
| 397 | + }, | ||
| 398 | + | ||
| 399 | + // Tìm box OCR phù hợp nhất để map với field | ||
| 400 | + findBestMatchingBox(fieldName, fieldValue) { | ||
| 401 | + let bestMatchIndex = -1; | ||
| 402 | + let bestScore = 0; | ||
| 403 | + | ||
| 404 | + this.ocrData.forEach((item, index) => { | ||
| 405 | + if (item.isDeleted) return; | ||
| 406 | + | ||
| 407 | + // Nếu box này đã được map field khác, bỏ qua | ||
| 408 | + if (item.field && item.field !== fieldName) return; | ||
| 409 | + | ||
| 410 | + // Tính điểm phù hợp dựa trên text | ||
| 411 | + const text = item.text || ''; | ||
| 412 | + const score = this.calculateTextSimilarity(text, fieldValue); | ||
| 413 | + | ||
| 414 | + if (score > bestScore) { | ||
| 415 | + bestScore = score; | ||
| 416 | + bestMatchIndex = index; | ||
| 417 | + } | ||
| 418 | + }); | ||
| 419 | + | ||
| 420 | + // Chỉ map nếu điểm phù hợp đủ cao (ví dụ > 0.5) | ||
| 421 | + return bestScore > 0.5 ? bestMatchIndex : -1; | ||
| 422 | + }, | ||
| 423 | + | ||
| 424 | + // Tính điểm tương đồng giữa 2 text | ||
| 425 | + calculateTextSimilarity(text1, text2) { | ||
| 426 | + if (!text1 || !text2) return 0; | ||
| 427 | + | ||
| 428 | + const t1 = text1.toLowerCase().trim(); | ||
| 429 | + const t2 = text2.toLowerCase().trim(); | ||
| 430 | + | ||
| 431 | + // Nếu text giống hệt nhau | ||
| 432 | + if (t1 === t2) return 1.0; | ||
| 433 | + | ||
| 434 | + // Nếu một text là subset của text kia | ||
| 435 | + if (t1.includes(t2) || t2.includes(t1)) return 0.8; | ||
| 436 | + | ||
| 437 | + // Tính điểm dựa trên số ký tự giống nhau | ||
| 438 | + let commonChars = 0; | ||
| 439 | + const minLength = Math.min(t1.length, t2.length); | ||
| 440 | + | ||
| 441 | + for (let i = 0; i < minLength; i++) { | ||
| 442 | + if (t1[i] === t2[i]) commonChars++; | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + return commonChars / Math.max(t1.length, t2.length); | ||
| 312 | }, | 446 | }, |
| 313 | onImageLoad() { | 447 | onImageLoad() { |
| 314 | const img = this.$refs.pdfImage; | 448 | const img = this.$refs.pdfImage; |
| ... | @@ -348,7 +482,83 @@ | ... | @@ -348,7 +482,83 @@ |
| 348 | break; | 482 | break; |
| 349 | } | 483 | } |
| 350 | } | 484 | } |
| 351 | - this.activeIndex = idx === -1 ? null : idx; | 485 | + |
| 486 | + if (idx !== -1) { | ||
| 487 | + // Set active index (chuyển trạng thái active và màu xanh) | ||
| 488 | + this.activeIndex = idx; | ||
| 489 | + // Scroll đến box tương ứng | ||
| 490 | + this.scrollToBox(idx); | ||
| 491 | + // Focus vào box để người dùng thấy rõ | ||
| 492 | + this.focusOnBox(idx); | ||
| 493 | + } else { | ||
| 494 | + this.activeIndex = null; | ||
| 495 | + } | ||
| 496 | + }, | ||
| 497 | + | ||
| 498 | + // Scroll đến box tương ứng | ||
| 499 | + scrollToBox(index) { | ||
| 500 | + if (!this.$refs.pdfContainer || index < 0 || index >= this.ocrData.length) return; | ||
| 501 | + | ||
| 502 | + const item = this.ocrData[index]; | ||
| 503 | + if (!item || item.isDeleted) return; | ||
| 504 | + | ||
| 505 | + // Tính vị trí hiển thị của box | ||
| 506 | + const [x1, y1, x2, y2] = item.bbox; | ||
| 507 | + if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return; | ||
| 508 | + | ||
| 509 | + const displayedWidth = this.$refs.pdfImage.clientWidth; | ||
| 510 | + const displayedHeight = this.$refs.pdfImage.clientHeight; | ||
| 511 | + const scaleX = displayedWidth / this.imageWidth; | ||
| 512 | + const scaleY = displayedHeight / this.imageHeight; | ||
| 513 | + | ||
| 514 | + const displayX = Math.round(x1 * scaleX); | ||
| 515 | + const displayY = Math.round(y1 * scaleY); | ||
| 516 | + | ||
| 517 | + // Scroll đến vị trí box | ||
| 518 | + const container = this.$refs.pdfContainer; | ||
| 519 | + const containerRect = container.getBoundingClientRect(); | ||
| 520 | + const scrollTop = container.scrollTop; | ||
| 521 | + const scrollLeft = container.scrollLeft; | ||
| 522 | + | ||
| 523 | + // Tính vị trí scroll để box nằm ở giữa viewport | ||
| 524 | + const targetScrollTop = scrollTop + displayY - (containerRect.height / 2); | ||
| 525 | + const targetScrollLeft = scrollLeft + displayX - (containerRect.width / 2); | ||
| 526 | + | ||
| 527 | + container.scrollTo({ | ||
| 528 | + top: Math.max(0, targetScrollTop), | ||
| 529 | + left: Math.max(0, targetScrollLeft), | ||
| 530 | + behavior: 'smooth' | ||
| 531 | + }); | ||
| 532 | + }, | ||
| 533 | + | ||
| 534 | + // Focus vào box (thêm hiệu ứng nhấp nháy) | ||
| 535 | + focusOnBox(index) { | ||
| 536 | + if (index < 0 || index >= this.ocrData.length) return; | ||
| 537 | + | ||
| 538 | + const item = this.ocrData[index]; | ||
| 539 | + if (!item || item.isDeleted) return; | ||
| 540 | + | ||
| 541 | + // Thêm class để tạo hiệu ứng focus | ||
| 542 | + this.$nextTick(() => { | ||
| 543 | + const boxElement = document.querySelector(`[data-field="${item.field}"]`); | ||
| 544 | + if (boxElement) { | ||
| 545 | + boxElement.classList.add('focus-highlight'); | ||
| 546 | + setTimeout(() => { | ||
| 547 | + boxElement.classList.remove('focus-highlight'); | ||
| 548 | + }, 2000); | ||
| 549 | + } | ||
| 550 | + }); | ||
| 551 | + }, | ||
| 552 | + | ||
| 553 | + // Xử lý khi click vào input | ||
| 554 | + onInputClick(fieldName) { | ||
| 555 | + // Kiểm tra xem field này có data không | ||
| 556 | + const fieldValue = this.formData[fieldName]; | ||
| 557 | + if (fieldValue && fieldValue.trim()) { | ||
| 558 | + // Nếu có data, highlight và focus vào box tương ứng | ||
| 559 | + // Chỉ khi click vào input mới focus và chuyển trạng thái active | ||
| 560 | + this.highlightField(fieldName); | ||
| 561 | + } | ||
| 352 | }, | 562 | }, |
| 353 | startSelect(e) { | 563 | startSelect(e) { |
| 354 | if (this.isMappingManually || e.button !== 0) return; | 564 | if (this.isMappingManually || e.button !== 0) return; | ... | ... |
-
Please register or sign in to post a comment