tien_nemo

test ok

......@@ -25,12 +25,6 @@
background-color: rgba(25, 150, 1, 0.4) !important;
}
.bbox.focus-highlight {
/*animation: focusPulse 2s ease-in-out;*/
border-color: #ff6b35 !important;
/*background-color: rgba(255, 107, 53, 0.4) !important;*/
}
@keyframes focusPulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
......@@ -95,7 +89,7 @@
:class="{ active: index === activeIndex }"
:data-field="item.field"
:style="getBoxStyle(item, index)"
@click="selectingIndex = index">
@click="onBoxClick(index)">
<button v-if="item.isManual && item.showDelete"
class="delete-btn"
......@@ -104,7 +98,7 @@
<!-- Dropdown OCR -->
<select v-if="selectingIndex !== null && ocrData[selectingIndex].isManual"
<select v-if="selectingIndex !== null && ocrData[selectingIndex]"
:style="getSelectStyle(ocrData[selectingIndex])"
v-model="ocrData[selectingIndex].field"
@change="applyMapping"
......@@ -133,7 +127,7 @@
<input v-model="formData[field.value]"
@focus="highlightField(field.value)"
@click="onInputClick(field.value)"
{{-- @blur="removeManualBoxes"--}}
@blur="onInputBlur(field.value)"
:readonly="field.value === 'customer_name' && !hasCustomerNameXY"
>
</div>
......@@ -174,6 +168,14 @@
},
mounted() {
this.loadOCRData();
// Thêm event listener để xóa focus khi click ra ngoài
document.addEventListener('click', (e) => {
// Nếu click không phải vào input hoặc box
if (!e.target.closest('.left-panel') && !e.target.closest('.bbox')) {
this.removeAllFocus();
}
});
},
computed: {
fieldData() {
......@@ -190,27 +192,33 @@
mapFieldToBox(index, fieldName, text = null) {
if (index == null) return;
// Xóa fieldName ở box khác đảm bảo mỗi field chỉ gán cho 1 box duy nhất
this.ocrData.forEach((box, i) => {
if (i !== index && box.field === fieldName) {
box.field = null;
box.field_xy = null;
// Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại
this.ocrData = this.ocrData.filter((box, i) => {
if (i === index) return true; // Giữ lại box hiện tại
if (box.field === fieldName) {
// Xóa box có field trùng lặp
return false;
}
return true; // Giữ lại các box khác
});
// Cập nhật lại index sau khi filter
const newIndex = this.ocrData.findIndex(box => box.bbox === this.ocrData[index]?.bbox);
if (newIndex === -1) return;
// Nếu box này từng gán field khác thì bỏ reset flag và tọa độ liên quan.
const prev = this.ocrData[index].field;
const prev = this.ocrData[newIndex].field;
if (prev && prev !== fieldName) {
if (prev === 'customer_name') {
this.hasCustomerNameXY = false;
this.customer_name_xy = '';
}
this.ocrData[index].field = null;
this.ocrData[index].field_xy = null;
this.ocrData[newIndex].field = null;
this.ocrData[newIndex].field_xy = null;
}
// Gán field mới
const bbox = this.ocrData[index].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
const bbox = this.ocrData[newIndex].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
const x1 = bbox[0];
const y1 = bbox[1];
......@@ -219,11 +227,11 @@
const xyStr = `${x1},${y1},${w},${h}`;
this.ocrData[index].field = fieldName;
this.ocrData[index].field_xy = xyStr;
this.ocrData[newIndex].field = fieldName;
this.ocrData[newIndex].field_xy = xyStr;
// Set text
this.formData[fieldName] = (text !== null ? text : (this.ocrData[index].text || '')).trim();
this.formData[fieldName] = (text !== null ? text : (this.ocrData[newIndex].text || '')).trim();
// KHÔNG set active index (không focus)
......@@ -238,50 +246,59 @@
console.log(`Assigning field "${fieldName}" to box at index ${index} with text: "${text}"`);
if (index == null) return;
// 1. Xóa box cũ của field này (nếu có)
// 1. Xóa tất cả box có fieldName trùng lặp, chỉ giữ lại box hiện tại
this.ocrData = this.ocrData.filter((box, i) => {
return !(i !== index && box.field === fieldName && box.isManual);
if (i === index) return true; // Giữ lại box hiện tại
if (box.field === fieldName) {
// Xóa box có field trùng lặp
return false;
}
return true; // Giữ lại các box khác
});
// 2. Nếu box này từng gán field khác thì bỏ
const prev = this.ocrData[index]?.field;
// 2. Cập nhật lại index sau khi filter
const newIndex = this.ocrData.findIndex(box => box.bbox === this.ocrData[index]?.bbox);
if (newIndex === -1) return;
// 3. Nếu box này từng gán field khác thì bỏ
const prev = this.ocrData[newIndex]?.field;
if (prev && prev !== fieldName) {
if (prev === 'customer_name') {
this.hasCustomerNameXY = false;
this.customer_name_xy = '';
}
this.ocrData[index].field = null;
this.ocrData[index].field_xy = null;
this.ocrData[newIndex].field = null;
this.ocrData[newIndex].field_xy = null;
}
// 3. Gán field mới
const bbox = this.ocrData[index].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
// 4. Gán field mới
const bbox = this.ocrData[newIndex].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
console.log('bbox', bbox);
const [x1, y1, w, h] = bbox;
const xyStr = `${x1},${y1},${w},${h}`;
this.ocrData[index].field = fieldName;
this.ocrData[index].field_xy = xyStr;
this.ocrData[newIndex].field = fieldName;
this.ocrData[newIndex].field_xy = xyStr;
// 4. Gán text
const finalText = text !== null ? text : (this.ocrData[index].text || '');
// 5. Gán text
const finalText = text !== null ? text : (this.ocrData[newIndex].text || '');
this.formData[fieldName] = finalText.trim();
console.log(`formData ${fieldName}`, this.formData[fieldName]);
// Nếu trong manualBoxData tồn tại field này hoặc muốn tạo lại manual box từ ocrData
if (this.manualBoxData[fieldName]) {
// Lấy tọa độ + text từ box hiện tại để tạo manual box mới
this.createManualBoxFromDB(fieldName, this.ocrData[index].bbox, finalText);
this.createManualBoxFromDB(fieldName, this.ocrData[newIndex].bbox, finalText);
// Cập nhật manualBoxData
this.manualBoxData[fieldName] = {
coords: this.ocrData[index].bbox,
coords: this.ocrData[newIndex].bbox,
text: finalText
};
}
// 5. Active index
this.activeIndex = index;
// 6. Nếu là customer_name
// 6. Active index
this.activeIndex = newIndex;
// 7. Nếu là customer_name
if (fieldName === 'customer_name') {
this.hasCustomerNameXY = true;
this.customer_name_xy = xyStr;
......@@ -475,38 +492,49 @@
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
border: item.hideBorder ? 'none' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'),
border: item.hideBorder ? '2px solid #ccc' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'),
boxSizing: 'border-box',
cursor: 'pointer',
zIndex: item.isManual ? 30 : 10
};
},
highlightField(field) {
// Xóa tất cả manual box cũ trước khi tạo mới
//this.ocrData = this.ocrData.filter(b => !b.isManual);
let coords, text;
// Kiểm tra xem ocrData đã có box nào với field này chưa
const existingBox = this.ocrData.find(b => b.field === field && !b.isManual);
if (existingBox) {
coords = existingBox.bbox;
text = existingBox.text;
console.log(`Using ocrData for field "${field}":`, coords, text);
} else if (this.manualBoxData[field]) {
// Nếu không có trong ocrData, dùng manualBoxData
coords = this.manualBoxData[field].coords;
text = this.manualBoxData[field].text;
console.log(`Using manualBoxData for field "${field}":`, coords, text);
let isFromDB = false;
// Kiểm tra xem field này có phải từ DB không
if (this.dataMapping && this.dataMapping[field]) {
isFromDB = true;
coords = this.dataMapping[field].coords;
text = this.dataMapping[field].text;
console.log(`Using dataMapping for field "${field}":`, coords, text);
} else {
// Kiểm tra xem ocrData đã có box nào với field này chưa
const existingBox = this.ocrData.find(b => b.field === field && !b.isDeleted);
if (existingBox) {
coords = existingBox.bbox;
text = existingBox.text;
console.log(`Using existing box for field "${field}":`, coords, text);
} else if (this.manualBoxData[field]) {
// Nếu không có trong ocrData, dùng manualBoxData (tọa độ mới)
coords = this.manualBoxData[field].coords;
text = this.manualBoxData[field].text;
console.log(`Using manualBoxData (new coordinates) for field "${field}":`, coords, text);
}
}
// Nếu có coords thì tạo manual box
// Nếu có coords thì tạo hoặc hiển thị lại box
if (coords) {
this.createManualBoxFromDB(field, coords, text);
if (isFromDB) {
// Tạo box manual từ DB (không có nút xóa)
this.createManualBoxFromDB(field, coords, text);
} else {
// Hiển thị lại box manual đã quét chọn (có nút xóa)
this.showManualBox(field, coords, text);
}
}
// Tìm lại index của box vừa tạo để set active
// Tìm lại index của box để set active
let idx = -1;
for (let i = this.ocrData.length - 1; i >= 0; i--) {
const it = this.ocrData[i];
......@@ -519,7 +547,8 @@
if (idx !== -1) {
this.activeIndex = idx;
this.scrollToBox(idx);
this.focusOnBox(idx);
// Reset selectingIndex để không hiển thị dropdown khi highlight từ input
this.selectingIndex = null;
} else {
this.activeIndex = null;
}
......@@ -563,32 +592,82 @@
});
},
// Focus vào box (thêm hiệu ứng nhấp nháy)
focusOnBox(index) {
if (index < 0 || index >= this.ocrData.length) return;
const item = this.ocrData[index];
if (!item || item.isDeleted) return;
// Thêm class để tạo hiệu ứng focus
this.$nextTick(() => {
const boxElement = document.querySelector(`[data-field="${item.field}"]`);
if (boxElement) {
boxElement.classList.add('focus-highlight');
}
});
},
// Xử lý khi click vào input
onInputClick(fieldName) {
// Kiểm tra xem field này có data không
const fieldValue = this.formData[fieldName];
if (fieldValue && fieldValue.trim()) {
// Nếu có data, highlight và focus vào box tương ứng
// Chỉ khi click vào input mới focus và chuyển trạng thái active
// Nếu có data từ DB, highlight và focus vào box tương ứng
this.highlightField(fieldName);
}
},
// Xử lý khi click ra ngoài input (blur)
onInputBlur(fieldName) {
// Khi không focus vào input nào, xóa tất cả focus
this.removeAllFocus();
},
// Xóa tất cả focus
removeAllFocus() {
// Reset active index (chỉ mất màu xanh, không xóa box)
this.activeIndex = null;
// Ẩ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)
this.ocrData.forEach(item => {
if (item.isManual && !item.showDelete) {
// Box manual từ DB (không có nút xóa) - ẩn hoàn toàn
item.isDeleted = true;
}
});
// Đảm bảo tất cả box OCR đều hiển thị (chỉ ẩn border khi cần thiết)
this.ocrData.forEach(item => {
if (!item.isManual && item.hideBorder) {
// Chỉ ẩn border cho box OCR nằm trong vùng manual
item.hideBorder = true;
}
});
},
// Xử lý khi click vào box
onBoxClick(index) {
const item = this.ocrData[index];
// Kiểm tra xem field này có phải từ DB không
const isFromDB = this.dataMapping && this.dataMapping[item.field];
// Kiểm tra xem data có được ghi đè không (so sánh với data gốc từ DB)
const isDataOverridden = item.field && isFromDB &&
this.formData[item.field] !== this.dataMapping[item.field].text;
if (item.isManual) {
// Manual box
if (isFromDB && !isDataOverridden) {
// Manual box từ DB chưa ghi đè, KHÔNG cho chọn option
this.activeIndex = index;
this.selectingIndex = null;
} else {
// Manual box từ DB có data ghi đè HOẶC manual box bình thường, CHO PHÉP chọn option
this.selectingIndex = index;
}
} else if (item.field) {
// Box OCR có field
if (isFromDB && !isDataOverridden) {
// Box có field từ DB chưa ghi đè, KHÔNG cho chọn option
this.activeIndex = index;
this.selectingIndex = null;
} else {
// Box có field từ DB đã ghi đè HOẶC field mới, CHO PHÉP chọn option
this.selectingIndex = index;
}
} else {
// Box OCR thông thường (chưa có field), cho phép hiển thị dropdown
this.selectingIndex = index;
}
},
startSelect(e) {
if (this.isMappingManually || e.button !== 0) return;
this.isSelecting = true;
......@@ -673,19 +752,41 @@
,
applyMapping() {
const item = this.ocrData[this.selectingIndex];
if (!item) return;
if (item && item.isManual) {
if (item.isManual) {
// Nếu là manual box, chuyển sang chế độ manual mapping
this.manualIndex = this.selectingIndex;
this.manualField = item.field || "";
this.applyManualMapping();
return;
}
// Xử lý box OCR (có thể chưa có field hoặc đã có field)
if (item.field) {
// this.formData[item.field] = item.text;
// this.activeIndex = this.selectingIndex;
this.assignFieldToBox(this.selectingIndex, item.field, item.text);
// Nếu box đã có field, cập nhật lại
const oldField = item.field;
// Cập nhật formData để hiển thị trong input
this.formData[item.field] = item.text || '';
// Cập nhật manualBoxData với tọa độ mới
this.manualBoxData[item.field] = {
coords: item.bbox,
text: item.text || ''
};
// Xóa dataMapping cũ để tránh focus về tọa độ cũ
if (this.dataMapping && this.dataMapping[item.field]) {
delete this.dataMapping[item.field];
}
// Set active index
this.activeIndex = this.selectingIndex;
console.log(`Updated field "${item.field}" for box at index ${this.selectingIndex} with new coordinates`);
}
this.selectingIndex = null;
},
applyManualMapping() {
......@@ -723,17 +824,34 @@
});
const finalText = combinedText.join(" ");
// console.log('Combined text:', finalText);
console.log('Combined text:', finalText);
// Gán field và text cho box manual
console.log(`Assigning manual field "${this.manualField}" to box at index ${manualIndex} with text: "${finalText}"`);
// console.log(this.ocrData[manualIndex]);
this.assignFieldToBox(manualIndex, this.manualField, finalText);
// Gán field trực tiếp cho box manual
this.ocrData[manualIndex].field = this.manualField;
// Cập nhật formData để hiển thị trong input
this.formData[this.manualField] = finalText.trim();
// Cập nhật manualBoxData
this.manualBoxData[this.manualField] = {
coords: newBbox,
text: finalText.trim()
};
// Xóa dataMapping cũ để tránh focus về tọa độ cũ
if (this.dataMapping && this.dataMapping[this.manualField]) {
delete this.dataMapping[this.manualField];
}
// Reset trạng thái chọn
this.isMappingManually = false;
this.selectBox.show = false;
this.selectBox.showDropdown = false;
this.manualField = "";
this.manualIndex = null;
},
isBoxInside(inner, outer) {
......@@ -820,7 +938,10 @@
if (x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1 &&
x2 <= this.imageWidth && y2 <= this.imageHeight) {
// Tạo box manual
// Xóa box cũ có cùng fieldName trước khi tạo mới (chỉ xóa manual box)
this.ocrData = this.ocrData.filter(box => !(box.field === fieldName && box.isManual));
// Tạo box manual từ DB (không có nút xóa)
const manualBox = {
text: text || '',
bbox: coords,
......@@ -832,7 +953,55 @@
};
this.ocrData.push(manualBox);
// console.log('Manual box created successfully:', manualBox);
// console.log('Manual box created successfully:', manualBox);
// Force re-render
this.$forceUpdate();
} else {
console.warn('Invalid coordinates for manual box:', coords);
}
},
// Hiển thị lại box manual đã quét chọn (có nút xóa)
showManualBox(fieldName, coordinates, text) {
if (!this.imageWidth || !this.imageHeight) {
console.log('Cannot show manual box: Image not loaded');
return;
}
// Parse coordinates
let coords;
if (typeof coordinates === 'string') {
coords = coordinates.split(',').map(Number);
} else if (Array.isArray(coordinates)) {
coords = coordinates;
} else {
console.error('Invalid coordinates format:', coordinates);
return;
}
const [x1, y1, x2, y2] = coords;
// Kiểm tra tọa độ có hợp lệ không
if (x1 >= 0 && y1 >= 0 && x2 > x1 && y2 > y1 &&
x2 <= this.imageWidth && y2 <= this.imageHeight) {
// Xóa box cũ có cùng fieldName trước khi hiển thị lại
this.ocrData = this.ocrData.filter(box => !(box.field === fieldName && box.isManual));
// Hiển thị lại box manual đã quét chọn (có nút xóa)
const manualBox = {
text: text || '',
bbox: coords,
field: fieldName,
isManual: true,
showDelete: true,
isDeleted: false,
hideBorder: false
};
this.ocrData.push(manualBox);
console.log('Manual box shown successfully:', manualBox);
// Force re-render
this.$forceUpdate();
......