Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Satini_pvduc
/
ocrpdf
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Commits
Issue Boards
Files
Commits
Network
Compare
Branches
Tags
Authored by
tien_nemo
2025-08-14 08:29:13 +0700
Browse Files
Options
Browse Files
Tag
Download
Email Patches
Plain Diff
Commit
cc375b04113f7cd7636da36fad301dfbda1d5be7
cc375b04
1 parent
7ba730da
temp
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
663 additions
and
46 deletions
select.html
select.html
View file @
cc375b0
...
...
@@ -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 field
Options"
:value=
"field.value"
>
{{ field.label }}
</option>
<option
v-for=
"field in field
Data"
: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 field
Options"
:value=
"field.value"
>
{{ field.label }}
</option>
<option
v-for=
"field in field
Data"
: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
);
}
}
}
...
...
Please
register
or
sign in
to post a comment