tien_nemo

test load data

...@@ -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;
......