tien_nemo

temp

Showing 1 changed file with 663 additions and 46 deletions
......@@ -21,8 +21,14 @@
cursor: pointer;
}
.bbox.active {
/*border-color: #2196F3;*/
background-color: rgb(33 243 132 / 30%);
border-color: #199601 !important;
background-color: rgba(25, 150, 1, 0.4) !important;
}
@keyframes focusPulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
select {
position: absolute;
......@@ -54,8 +60,8 @@
</head>
<body>
<meta name="csrf-token" content="{{ csrf_token() }}">
<div id="app">
<!-- Right: PDF viewer + select tool -->
<div class="right-panel" >
<div class="pdf-container" ref="pdfContainer"
......@@ -83,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"
......@@ -92,13 +98,13 @@
<!-- Dropdown OCR -->
<select v-if="selectingIndex !== null"
<select v-if="selectingIndex !== null && ocrData[selectingIndex]"
:style="getSelectStyle(ocrData[selectingIndex])"
v-model="ocrData[selectingIndex].field"
@change="applyMapping"
>
<option disabled value="">-- Chọn trường dữ liệu --</option>
<option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
<option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option>
</select>
<!-- Dropdown thủ công -->
......@@ -109,7 +115,7 @@
@click.stop
>
<option disabled value="">-- Chọn trường dữ liệu --</option>
<option v-for="field in fieldOptions" :value="field.value">{{ field.label }}</option>
<option v-for="field in fieldData" :value="field.value">@{{ field.label }}</option>
</select>
</div>
</div>
......@@ -117,9 +123,15 @@
<!-- Left: Form inputs -->
<div class="left-panel">
<div v-for="field in fieldOptions" :key="field.value" class="form-group">
<label>{{ field.label }}</label>
<input v-model="formData[field.value]" @focus="highlightField(field.value)">
<label>@{{ field.label }}</label>
<input v-model="formData[field.value]"
@focus="highlightField(field.value)"
@click="onInputClick(field.value)"
@blur="onInputBlur(field.value)"
:readonly="field.value === 'customer_name' && !hasCustomerNameXY"
>
</div>
<button @click="saveTemplate">💾Save</button>
</div>
</div>
......@@ -136,24 +148,232 @@
isSelecting: false,
activeIndex: null,
manualField: "",
formData: { export_date: "", order_code: "", customer: "", address: "", staff: "" },
fieldOptions: [
{ value: "export_date", label: "Ngày xuất" },
{ value: "order_code", label: "Mã đơn hàng" },
{ value: "customer", label: "Khách hàng" },
{ value: "address", label: "Địa chỉ" },
{ value: "staff", label: "Nhân viên" }
],
formData: {},
manualBoxData: {},
fieldOptions: [],
customer_name_xy: '',
hasCustomerNameXY: false,
ocrData: [],
selectBox: { show: false, showDropdown: false, x: 0, y: 0, width: 0, height: 0, startX: 0, startY: 0 },
manualIndex: null
}
},
created() {
// Chỉ tạo formData cho các field cần mapping
this.fieldOptions
.filter(f => f.value !== "template_name")
.forEach(f => {
this.$set(this.formData, f.value, "");
});
},
mounted() {
this.pdfImageUrl = "/public/image/data_picking_detail_1754967679.jpg"; // ảnh xuất từ Python
this.initData();
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() {
// Lọc bỏ template_name nếu không cần cho phần form mapping
return this.fieldOptions.filter(f => f.value !== "template_name");
}
},
methods: {
removeManualBoxes() {
this.ocrData = this.ocrData.filter(b => !b.isManual);
this.activeIndex = 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; // 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[newIndex].field;
if (prev && prev !== fieldName) {
if (prev === 'customer_name') {
this.hasCustomerNameXY = false;
this.customer_name_xy = '';
}
this.ocrData[newIndex].field = null;
this.ocrData[newIndex].field_xy = null;
}
// Gán field mới
const bbox = this.ocrData[newIndex].bbox; // tọa độ OCR gốc [x1, y1, x2, y2]
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;
// Set text
this.formData[fieldName] = (text !== null ? text : (this.ocrData[newIndex].text || '')).trim();
// KHÔNG set active index (không focus)
// Nếu là customer_name
if (fieldName === 'customer_name') {
this.hasCustomerNameXY = true;
this.customer_name_xy = xyStr;
}
},
assignFieldToBox(index, fieldName, text = null) {
console.log(`Assigning field "${fieldName}" to box at index ${index} with text: "${text}"`);
if (index == null) return;
// 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) => {
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. 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[newIndex].field = null;
this.ocrData[newIndex].field_xy = null;
}
// 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[newIndex].field = fieldName;
this.ocrData[newIndex].field_xy = xyStr;
// 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[newIndex].bbox, finalText);
// Cập nhật manualBoxData
this.manualBoxData[fieldName] = {
coords: this.ocrData[newIndex].bbox,
text: finalText
};
}
// 6. Active index
this.activeIndex = newIndex;
// 7. Nếu là customer_name
if (fieldName === 'customer_name') {
this.hasCustomerNameXY = true;
this.customer_name_xy = xyStr;
}
},
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;
} else {
// Không có → tìm trong ocrData
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) {
// chỉ giữ 1 bản ghi cuối cùng cho mỗi field (box gần nhất)
fieldsByName[box.field] = {
text: box.field,
coords: box.field_xy || ''
};
}
});
// // convert to array
fields = (fieldsByName);
}
console.log('fields:', fields);
if (!customer_coords) {
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
};
// console.log(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) {
......@@ -181,20 +401,78 @@
}
},
async initData() {
await this.loadOCRData();
},
async loadOCRData() {
const res = await fetch("/public/image/data_picking_detail_1754967679.json");
this.ocrData = await res.json();
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.formData = data.formData;
this.fieldOptions = data.fieldOptions;
this.dataMapping = data.dataMapping;
// Đợi image load xong trước khi xử lý
if (this.$refs.pdfImage && this.$refs.pdfImage.complete) {
this.processLoadedData();
} else {
console.log('Image not loaded yet, waiting for onImageLoad');
// Image sẽ được xử lý trong onImageLoad
}
} catch (error) {
console.error('Error in loadOCRData:', error);
}
},
// Xử lý data sau khi image đã load
processLoadedData() {
// Tự động map field cho các box OCR dựa trên formData đã load
this.autoMapFieldsFromFormData();
// Kiểm tra và sửa lại tọa độ của các box manual
// Force re-render để đảm bảo các box được vẽ
this.$nextTick(() => {
this.$forceUpdate();
});
},
// Tự động map field cho các box OCR dựa trên formData đã load từ DB
autoMapFieldsFromFormData() {
this.manualBoxData = {}; // reset
Object.keys(this.dataMapping).forEach(fieldName => {
const { text, coords } = this.dataMapping[fieldName];
// Ví dụ: tạo box từ dữ liệu
//this.createManualBoxFromDB(fieldName, coords, text);
this.manualBoxData[fieldName] = { text, coords };
});
},
onImageLoad() {
const img = this.$refs.pdfImage;
this.imageWidth = img.naturalWidth;
this.imageHeight = img.naturalHeight;
// Nếu đã có data, xử lý ngay
if (this.ocrData && this.ocrData.length > 0) {
console.log('Image loaded and data exists, processing now');
this.processLoadedData();
} else {
console.log('Image loaded but no data yet');
}
},
getBoxStyle(item, index) {
if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return {};
if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) {
return {};
}
const [x1, y1, x2, y2] = item.bbox;
const displayedWidth = this.$refs.pdfImage.clientWidth;
......@@ -203,22 +481,60 @@
const scaleX = displayedWidth / this.imageWidth;
const scaleY = displayedHeight / this.imageHeight;
const left = Math.round(x1 * scaleX);
const top = Math.round(y1 * scaleY);
const width = Math.round((x2 - x1) * scaleX);
const height = Math.round((y2 - y1) * scaleY);
return {
position: 'absolute',
left: `${Math.round(x1 * scaleX)}px`,
top: `${Math.round(y1 * scaleY)}px`,
width: `${Math.round((x2 - x1) * scaleX)}px`,
height: `${Math.round((y2 - y1) * scaleY)}px`,
border: item.hideBorder ? 'none' : '2px solid ' + (index === this.activeIndex ? '#199601' : '#ff5252'),
//backgroundColor: item.hideBorder ? 'transparent' : (this.activeIndex === item.field ? 'rgba(33,150,243,0.3)' : 'rgba(255,82,82,0.2)'),
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
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) {
// tìm box gần nhất match field này
let 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 hoặc hiển thị lại box
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
let idx = -1;
for (let i = this.ocrData.length - 1; i >= 0; i--) {
const it = this.ocrData[i];
......@@ -227,9 +543,131 @@
break;
}
}
this.activeIndex = idx; // nếu không tìm thấy thì = -1
if (idx !== -1) {
this.activeIndex = idx;
this.scrollToBox(idx);
// Reset selectingIndex để không hiển thị dropdown khi highlight từ input
this.selectingIndex = null;
} else {
this.activeIndex = null;
}
},
// Scroll đến box tương ứng
scrollToBox(index) {
if (!this.$refs.pdfContainer || index < 0 || index >= this.ocrData.length) return;
const item = this.ocrData[index];
if (!item || item.isDeleted) return;
// Tính vị trí hiển thị của box
const [x1, y1, x2, y2] = item.bbox;
//const [x1, y1, x2, y2] = item.field_xy.split(',').map(Number);
if (!this.imageWidth || !this.imageHeight || !this.$refs.pdfImage) return;
const displayedWidth = this.$refs.pdfImage.clientWidth;
const displayedHeight = this.$refs.pdfImage.clientHeight;
const scaleX = displayedWidth / this.imageWidth;
const scaleY = displayedHeight / this.imageHeight;
const displayX = Math.round(x1 * scaleX);
const displayY = Math.round(y1 * scaleY);
// Scroll đến vị trí box
const container = this.$refs.pdfContainer;
const containerRect = container.getBoundingClientRect();
const scrollTop = container.scrollTop;
const scrollLeft = container.scrollLeft;
// Tính vị trí scroll để box nằm ở giữa viewport
const targetScrollTop = scrollTop + displayY - (containerRect.height / 2);
const targetScrollLeft = scrollLeft + displayX - (containerRect.width / 2);
container.scrollTo({
top: Math.max(0, targetScrollTop),
left: Math.max(0, targetScrollLeft),
behavior: 'smooth'
});
},
// 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 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;
......@@ -271,7 +709,7 @@
const dispX2 = this.selectBox.x + this.selectBox.width;
const dispY2 = this.selectBox.y + this.selectBox.height;
// scale: displayed -> original
// scale: displayed -> original (sửa lại để chính xác hơn)
const displayedWidth = this.$refs.pdfImage.clientWidth;
const displayedHeight = this.$refs.pdfImage.clientHeight;
const scaleX = this.imageWidth / displayedWidth;
......@@ -309,67 +747,147 @@
e.stopPropagation();
e.preventDefault();
}
,
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;
// 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 (box OCR không có nút xóa)
this.manualBoxData[item.field] = {
coords: item.bbox,
text: item.text || '',
isFromOCR: true // Đánh dấu đây là box OCR, không phải quét chọn
};
// 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 OCR at index ${this.selectingIndex} with new coordinates`);
}
this.selectingIndex = null;
},
applyManualMapping() {
if (!this.manualField) return;
const manualIndex = this.manualIndex;
const newBbox = this.ocrData[manualIndex].bbox;
let combinedText = [];
let foundItems = [];
// Tìm tất cả các box OCR nằm trong vùng manual
this.ocrData.forEach(item => {
if (!item.isManual && this.isBoxInside(item.bbox, newBbox) && item.text.trim()) {
const partial = this.getPartialText(item.text, item.bbox, newBbox);
if (partial) combinedText.push(partial);
// combinedText.push(item.text.trim());
foundItems.push({
text: item.text,
bbox: item.bbox,
index: this.ocrData.indexOf(item)
});
}
});
// Sắp xếp các item theo vị trí (từ trái sang phải, từ trên xuống dưới)
foundItems.sort((a, b) => {
// Ưu tiên theo Y trước (hàng), sau đó theo X (cột)
if (Math.abs(a.bbox[1] - b.bbox[1]) < 20) { // Cùng hàng (tolerance 20px)
return a.bbox[0] - b.bbox[0]; // Sắp xếp theo X
}
return a.bbox[1] - b.bbox[1]; // Sắp xếp theo Y
});
// Gộp text theo thứ tự đã sắp xếp
foundItems.forEach(item => {
combinedText.push(item.text.trim());
});
const finalText = combinedText.join(" ");
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}"`);
// Gán field trực tiếp cho box manual
this.ocrData[manualIndex].field = this.manualField;
this.formData[this.manualField] = finalText;
this.activeIndex = manualIndex;
console.log('manualField', this.manualField, this.manualIndex)
// Cập nhật formData để hiển thị trong input
this.formData[this.manualField] = finalText.trim();
// Cập nhật manualBoxData (box quét chọn có nút xóa)
this.manualBoxData[this.manualField] = {
coords: newBbox,
text: finalText.trim(),
isFromOCR: false // Đánh dấu đây là box quét chọn, không phải OCR
};
// 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;
this.manualField = "";
this.manualIndex = null;
},
isBoxInside(inner, outer) {
return !(
// inner: bbox của OCR item [x1, y1, x2, y2]
// outer: bbox của vùng manual [x1, y1, x2, y2]
// Kiểm tra xem box OCR có nằm hoàn toàn trong vùng manual không
const isFullyInside = (
inner[0] >= outer[0] && // left edge
inner[1] >= outer[1] && // top edge
inner[2] <= outer[2] && // right edge
inner[3] <= outer[3] // bottom edge
);
// Kiểm tra xem box OCR có giao nhau với vùng manual không
const isOverlapping = !(
inner[2] < outer[0] || // box bên trái vùng chọn
inner[0] > outer[2] || // box bên phải vùng chọn
inner[3] < outer[1] || // box phía trên vùng chọn
inner[1] > outer[3] // box phía dưới vùng chọn
);
// 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;
// Chiều rộng box OCR
const boxWidth = x2 - x1;
const boxHeight = y2 - y1;
// Vị trí start và end tương đối trong text
let startRatio = Math.max(0, (sx1 - x1) / boxWidth);
......@@ -395,6 +913,105 @@
top: `${Math.round(y2 * scaleY)}px`,
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');
return;
}
// Parse coordinates từ string "x1,y1,x2,y2"
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 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,
field: fieldName,
isManual: true,
showDelete: false,
isDeleted: false,
hideBorder: true
};
this.ocrData.push(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));
// 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;
// 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
};
this.ocrData.push(manualBox);
console.log(`Manual box shown successfully: ${isFromOCR ? 'from OCR (no delete btn)' : 'from manual selection (with delete btn)'}`);
// Force re-render
this.$forceUpdate();
} else {
console.warn('Invalid coordinates for manual box:', coords);
}
}
}
......