tien_nemo

resize box

......@@ -7,7 +7,7 @@ body {
display: flex; gap: 20px; padding: 20px;
}
.left-panel {
.right-panel {
width: 500px; background: #fff; padding: 15px;
border-radius: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.1);
}
......@@ -27,7 +27,7 @@ body {
border-radius: 4px;
}
.right-panel {
.left-panel {
flex: 1;
position: relative;
background: #eee;
......
......@@ -9,7 +9,7 @@
<body>
<meta name="csrf-token" content="{{ csrf_token() }}">
<div id="app">
<div class="right-panel" >
<div class="left-panel" >
<div class="pdf-container" ref="pdfContainer"
@mousedown="startSelect"
@mousemove="onSelect"
......@@ -33,6 +33,7 @@
v-if="!item.isDeleted"
class="bbox"
:class="{ active: index === activeIndex }"
:data-index="index"
:data-field="item.field"
:style="getBoxStyle(item, index)"
@click="onBoxClick(index)">
......@@ -94,13 +95,12 @@
</div>
</div>
<div class="left-panel">
<div class="right-panel">
<div v-for="field in fieldOptions" :key="field.value" class="form-group">
<label>@{{ field.label }}</label>
<input v-model="formData[field.value]"
@click="onInputClick(field.value)"
@blur="removeAllFocus()"
>
</div>
<button @click="saveTemplate">💾Save</button>
......@@ -141,7 +141,7 @@
//Thêm event listener để xóa focus khi click ra ngoài
document.addEventListener('click', (e) => {
if (
!e.target.closest('.left-panel') &&
!e.target.closest('.right-panel') &&
!e.target.closest('.bbox') &&
!e.target.closest('select')
) {
......@@ -169,7 +169,6 @@
document.addEventListener("mousemove", this.onResizing);
document.addEventListener("mouseup", this.stopResize);
},
onResizing(e) {
if (!this.resizing) return;
......@@ -197,129 +196,70 @@
x2 += dx;
}
// giữ không cho x1 > x2, y1 > y2
if (x1 < x2 && y1 < y2) {
this.$set(this.ocrData[index], "bbox", [x1, y1, x2, y2]);
const scaleX = this.$refs.pdfImage.clientWidth / this.imageWidth;
const scaleY = this.$refs.pdfImage.clientHeight / this.imageHeight;
const newBbox = [
Math.round(x1),
Math.round(y1),
Math.round(x2),
Math.round(y2)
];
if (this.ocrData[index].isManual) {
this.$set(this.ocrData[index], "bbox", newBbox);
} else {
this.selectBox = {
...this.selectBox,
x: Math.round(x1 * scaleX),
y: Math.round(y1 * scaleY),
width: Math.round((x2 - x1) * scaleX),
height: Math.round((y2 - y1) * scaleY),
show: true,
};
this.resizing.newBbox = newBbox;
}
}
},
},
stopResize() {
document.removeEventListener("mousemove", this.onResizing);
document.removeEventListener("mouseup", this.stopResize);
this.resizing = null;
},
// Map field cho box (không set active, chỉ dùng để load data từ DB)
mapFieldToBox(index, fieldName, text = null) {
if (index == null) return;
// 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;
if (box.field === fieldName) {
return false;
}
return true;
});
// 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;
if (!this.resizing) return;
const { index, newBbox } = this.resizing;
const targetBox = this.ocrData[index];
if (targetBox) {
if (!targetBox.isManual && newBbox) {
const newBox = {
...targetBox,
bbox: newBbox,
isManual: true,
isDeleted: false,
showDelete: true,
text: "",
};
this.$set(this.ocrData[index], "isDeleted", true);
this.ocrData.push(newBox);
this.selectingIndex = this.ocrData.length - 1;
this.updateHiddenBorders(newBox.bbox);
// 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[newIndex].field;
if (prev && prev !== fieldName) {
if (prev === 'customer_name') {
this.hasCustomerNameXY = false;
this.customer_name_xy = '';
} else if (targetBox.isManual) {
this.updateHiddenBorders(targetBox.bbox);
}
this.ocrData[newIndex].field = null;
this.ocrData[newIndex].field_xy = null;
}
const bbox = this.ocrData[newIndex].bbox;
const x1 = bbox[0];
const y1 = bbox[1];
const w = bbox[2];
const h = bbox[3];
const xyStr = `${x1},${y1},${w},${h}`;
this.ocrData[newIndex].field = fieldName;
this.ocrData[newIndex].field_xy = xyStr;
this.formData[fieldName] = (text !== null ? text : (this.ocrData[newIndex].text || '')).trim();
if (fieldName === 'customer_name') {
this.hasCustomerNameXY = true;
this.customer_name_xy = xyStr;
}
this.resizing = null;
this.selectBox.show = false;
},
async saveTemplate() {
let customer_name = null;
let customer_coords = null;
let fields = [];
if (this.manualBoxData.customer_name) {
// Lấy từ manualBoxData nếu có
customer_name = this.manualBoxData.customer_name.text;
customer_coords = this.manualBoxData.customer_name.coords.join(',');
fields = this.manualBoxData;
console.log('Using manualBoxData for customer_name:', customer_name, customer_coords);
} else {
const found = this.ocrData.find(item => item.field === 'customer_name');
if (found) {
customer_name = found.text;
customer_coords = found.field_xy;
}
const fieldsByName = {};
this.ocrData.forEach(box => {
if (box.field && !box.isDeleted) {
fieldsByName[box.field] = {
text: box.field,
coords: box.field_xy || ''
};
}
});
fields = (fieldsByName);
console.log('Using ocrData for customer_name:', customer_name, customer_coords);
}
if (!customer_coords || !customer_name) {
alert("Bạn phải map customer_name (quét/select) trước khi lưu.");
return;
updateHiddenBorders(manualBox) {
this.ocrData.forEach(item => {
if (!item.isManual) {
item.hideBorder = this.isBoxInside(item.bbox, manualBox);
}
const payload = {
customer_name_text: customer_name || '',
template_name: this.formData.template_name || this.formData.customer_name,
customer_name_xy: customer_coords || [],
fields: fields
};
try {
const res = await fetch('/ocr/save-template', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify(payload)
});
const json = await res.json();
if (json.success) {
alert(json.message);
} else {
alert('Save failed');
}
} catch (err) {
console.error(err);
alert('Save error');
}
},
deleteBox(index) {
const item = this.ocrData[index];
if (item.isManual) {
......@@ -345,30 +285,6 @@
this.selectingIndex = null;
}
},
async loadOCRData() {
try {
const res = await fetch(`/ocr/data-list`);
const data = await res.json();
if (data.error) {
console.error('Error loading data:', data.error);
return;
}
this.ocrData = data.ocrData;
this.pdfImageUrl = data.pdfImageUrl;
this.fieldOptions = data.fieldOptions;
this.dataMapping = data.dataMapping;
this.is_template = data.is_template;
} catch (error) {
console.error('Error in loadOCRData:', error);
}
},
// Xử lý data sau khi image đã load
processLoadedData() {
this.autoMapFieldsFromFormData();
......@@ -377,7 +293,6 @@
this.$forceUpdate();
});
},
autoMapFieldsFromFormData() {
this.manualBoxData = {};
if(this.is_template) {
......@@ -417,7 +332,6 @@
});
},
onImageLoad() {
const img = this.$refs.pdfImage;
this.imageWidth = img.naturalWidth;
......@@ -484,13 +398,7 @@
}
if (coords) {
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 để set active
......@@ -512,7 +420,6 @@
this.activeIndex = null;
}
},
scrollToBox(index) {
if (!this.$refs.pdfContainer || index < 0 || index >= this.ocrData.length) return;
......@@ -547,7 +454,6 @@
behavior: 'smooth'
});
},
// Xử lý khi click vào input
onInputClick(fieldName) {
// Kiểm tra xem field này có data không
......@@ -557,30 +463,26 @@
this.highlightField(fieldName);
}
},
// Xóa tất cả focus
removeAllFocus() {
// Reset active index (chỉ mất màu xanh, không xóa box)
this.activeIndex = null;
this.selectingIndex = 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)
// Đả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.showDelete) {
// Box manual từ DB (không có nút xóa) - ẩn hoàn toàn
// if (!item.isManual && item.hideBorder) {
// item.hideBorder = true;
// }
if (item.isManual) {
if (!item.showDelete) {
item.isDeleted = true;
} else {
item.hideBorder = false;
}
});
// Đả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;
// Hiển thị lại border cho manual box
}
});
},
// Xử lý khi click vào box
onBoxClick(index) {
const item = this.ocrData[index];
......@@ -631,7 +533,6 @@
this.selectBox.showDropdown = false;
this.manualField = "";
},
onSelect(e) {
if (!this.isSelecting) return;
const rect = this.$refs.pdfContainer.getBoundingClientRect();
......@@ -642,7 +543,6 @@
this.selectBox.width = Math.abs(currentX - this.selectBox.startX);
this.selectBox.height = Math.abs(currentY - this.selectBox.startY);
},
endSelect(e) {
if (!this.isSelecting) return;
this.isSelecting = false;
......@@ -702,12 +602,14 @@
const item = this.ocrData[this.selectingIndex];
if (!item) return;
console.log(`Applying mapping for index:`, item);
this.ocrData.forEach((box, i) => {
if (i !== this.selectingIndex && box.field === item.field) {
box.field = '';
}
});
if (item.isManual) {
console.log('aaaaaaaaaa')
// Nếu là manual box, chuyển sang chế độ manual mapping
this.manualIndex = this.selectingIndex;
this.manualField = item.field || "";
......@@ -742,7 +644,7 @@
const newBbox = this.ocrData[manualIndex].bbox;
console.log(`manual for field "${this.manualField}" at index ${manualIndex} with bbox:`, newBbox);
//console.log(`manual for field "${this.manualField}" at index ${manualIndex} with bbox:`, newBbox);
this.ocrData.forEach((box, i) => {
if (i !== manualIndex && box.field === this.ocrData[manualIndex].field) {
box.field = '';
......@@ -782,6 +684,8 @@
const finalText = combinedText.join(" ");
this.ocrData[manualIndex].field = this.manualField;
console.log('123',this.ocrData[manualIndex])
this.formData[this.manualField] = finalText.trim();
this.manualBoxData[this.manualField] = {
coords: newBbox,
......@@ -800,8 +704,8 @@
this.selectBox.showDropdown = false;
this.manualField = "";
this.manualIndex = null;
},
},
isBoxInside(inner, outer) {
// inner: bbox của OCR item [x1, y1, x2, y2]
// outer: bbox của vùng manual [x1, y1, x2, y2]
......@@ -821,13 +725,10 @@
inner[3] < outer[1] || // inner.bottom < outer.top → hoàn toàn phía trên
inner[1] > outer[3] // inner.top > outer.bottom → hoàn toàn phía dưới
);
// console.log(`isBoxInside: inner=${inner}, outer=${outer}, isFullyInside=${isFullyInside}, isOverlapping=${isOverlapping}`);
// Trả về true nếu box OCR nằm hoàn toàn trong hoặc giao nhau đáng kể
return isFullyInside || isOverlapping;
},
getPartialText(text, bbox, selectBbox) {
const [x1, y1, x2, y2] = bbox;
const [sx1, sy1, sx2, sy2] = selectBbox;
......@@ -856,8 +757,6 @@
zIndex: 9999
};
},
// Tạo box manual từ tọa độ trong DB
createManualBoxFromDB(fieldName, coordinates, text) {
if (!this.imageWidth || !this.imageHeight) {
console.log('Cannot create manual box: Image not loaded');
......@@ -888,7 +787,7 @@
bbox: coords,
field: fieldName,
isManual: true,
showDelete: false,
showDelete: true,
isDeleted: false,
hideBorder: true
};
......@@ -901,55 +800,91 @@
console.warn('Invalid coordinates for manual box:', coords);
}
},
async loadOCRData() {
// 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;
}
try {
const res = await fetch(`/ocr/data-list`);
const data = await res.json();
// 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);
if (data.error) {
console.error('Error loading data:', data.error);
return;
}
const [x1, y1, x2, y2] = coords;
this.ocrData = data.ocrData;
this.pdfImageUrl = data.pdfImageUrl;
this.fieldOptions = data.fieldOptions;
this.dataMapping = data.dataMapping;
this.is_template = data.is_template;
console.log('Loaded OCR data:', this.ocrData);
// 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) {
} catch (error) {
console.error('Error in loadOCRData:', error);
}
},
async saveTemplate() {
// 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));
let customer_name = null;
let customer_coords = null;
let fields = [];
if (this.manualBoxData.customer_name) {
// Lấy từ manualBoxData nếu có
customer_name = this.manualBoxData.customer_name.text;
customer_coords = this.manualBoxData.customer_name.coords.join(',');
fields = this.manualBoxData;
console.log('Using manualBoxData for customer_name:', customer_name, customer_coords);
} else {
const found = this.ocrData.find(item => item.field === 'customer_name');
if (found) {
customer_name = found.text;
customer_coords = found.field_xy;
}
// Kiểm tra xem đây có phải box quét chọn hay box OCR
const isFromOCR = this.manualBoxData[fieldName] && this.manualBoxData[fieldName].isFromOCR;
const fieldsByName = {};
this.ocrData.forEach(box => {
if (box.field && !box.isDeleted) {
fieldsByName[box.field] = {
text: box.field,
coords: box.field_xy || ''
};
}
});
fields = (fieldsByName);
console.log('Using ocrData for customer_name:', customer_name, customer_coords);
}
// 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)
const manualBox = {
text: text || '',
bbox: coords,
field: fieldName,
isManual: true,
showDelete: !isFromOCR, // Chỉ hiển thị nút xóa nếu KHÔNG phải từ OCR
isDeleted: false,
hideBorder: false
if (!customer_coords || !customer_name) {
alert("Bạn phải map customer_name (quét/select) trước khi lưu.");
return;
}
const payload = {
customer_name_text: customer_name || '',
template_name: this.formData.template_name || this.formData.customer_name,
customer_name_xy: customer_coords || [],
fields: fields
};
this.ocrData.push(manualBox);
// Force re-render
this.$forceUpdate();
try {
const res = await fetch('/ocr/save-template', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify(payload)
});
const json = await res.json();
if (json.success) {
alert(json.message);
} else {
console.warn('Invalid coordinates for manual box:', coords);
alert('Save failed');
}
} catch (err) {
console.error(err);
alert('Save error');
}
},
}
});
......