Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
A
aitools
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
周成波
aitools
Commits
54d92562
Commit
54d92562
authored
Oct 11, 2025
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
样本处理v2
parent
5decc3ef
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
1285 additions
and
2 deletions
+1285
-2
index.ts
src/router/index.ts
+8
-2
common.ts
src/views/sample_handle_v2/compositions/common.ts
+45
-0
customerInfo.ts
src/views/sample_handle_v2/compositions/customerInfo.ts
+101
-0
fileUpload.ts
src/views/sample_handle_v2/compositions/fileUpload.ts
+105
-0
process.ts
src/views/sample_handle_v2/compositions/process.ts
+224
-0
useCropBox.ts
src/views/sample_handle_v2/compositions/useCropBox.ts
+134
-0
useCropBoxV2.ts
src/views/sample_handle_v2/compositions/useCropBoxV2.ts
+262
-0
index.css
src/views/sample_handle_v2/index.css
+131
-0
index.vue
src/views/sample_handle_v2/index.vue
+275
-0
No files found.
src/router/index.ts
View file @
54d92562
...
...
@@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import
HomeView
from
'../views/home/index.vue'
import
SampleHandleView
from
'../views/sample_handle/index.vue'
import
WmNanoBananaView
from
'../views/wm_nano_banana/index.vue'
import
SampleHandleViewV2
from
'../views/sample_handle_v2/index.vue'
const
router
=
createRouter
({
history
:
createWebHashHistory
(
import
.
meta
.
env
.
BASE_URL
),
...
...
@@ -9,7 +10,7 @@ const router = createRouter({
{
path
:
'/'
,
name
:
'home'
,
component
:
SampleHandleView
component
:
SampleHandleView
V2
},
{
path
:
'/report'
,
...
...
@@ -25,7 +26,12 @@ const router = createRouter({
path
:
'/wm_nano_banana'
,
name
:
'wm_nano_banana'
,
component
:
WmNanoBananaView
}
},
{
path
:
'/sample_handle_v2'
,
name
:
'sample_handle_v2'
,
component
:
SampleHandleViewV2
},
]
})
...
...
src/views/sample_handle_v2/compositions/common.ts
0 → 100644
View file @
54d92562
import
aitoolsService
from
'@/api/service/aitoolsService'
// 检测设备类型
export
const
detectDeviceType
=
()
=>
{
const
userAgent
=
navigator
.
userAgent
.
toLowerCase
()
let
source
=
''
if
(
userAgent
.
match
(
/mobile/i
)
||
userAgent
.
match
(
/android/i
)
||
userAgent
.
match
(
/iphone/i
)
||
userAgent
.
match
(
/ipad/i
))
{
source
=
'mobile'
// if (userAgent.match(/iphone/i) || userAgent.match(/ipad/i)) {
// source = 'ios'
// } else if (userAgent.match(/android/i)) {
// source = 'android'
// }
if
(
userAgent
.
match
(
/micromessenger/i
))
{
source
=
'wechat'
}
}
else
{
source
=
'pc'
}
console
.
log
(
'设备类型:'
,
source
)
return
source
}
// 记录用户操作
export
const
trackUserAction
=
async
(
source
:
string
,
user_id
:
string
,
content
:
string
,
action
:
string
)
=>
{
let
param
:
any
=
{
source
:
source
,
user_id
:
user_id
,
content
:
content
,
action
:
action
,
}
aitoolsService
.
commonApi
(
'提交用户轨迹数据'
,
'track'
,
param
)
.
then
((
response
)
=>
{
if
(
response
==
'ok'
)
{
console
.
log
(
action
,
'上报成功'
)
}
else
{
console
.
log
(
action
,
'上报失败'
)
}
})
.
catch
((
error
)
=>
{
console
.
log
(
action
,
'上报失败:'
,
error
)
})
}
src/views/sample_handle_v2/compositions/customerInfo.ts
0 → 100644
View file @
54d92562
import
aitoolsService
from
'@/api/service/aitoolsService'
;
import
{
trackUserAction
}
from
'./common'
;
import
{
ElMessage
,
ElMessageBox
}
from
'element-plus'
;
import
utils
from
'@/utils/utils'
;
import
{
reactive
,
ref
}
from
'vue'
;
// 客户留资
export
const
myCustomerInfo
=
(
form
:
any
,
title
:
any
)
=>
{
const
customer
=
reactive
({
name
:
''
,
mobile
:
''
,
company
:
''
,
note
:
''
,
})
const
customerInfoVisible
=
ref
(
false
)
const
onCustomerInfoSubmit
=
async
()
=>
{
if
(
!
customer
.
name
||
!
customer
.
mobile
)
{
ElMessage
({
message
:
'请填写姓名和手机号'
,
type
:
'warning'
})
return
}
if
(
!
utils
.
isPhone
(
customer
.
mobile
))
{
ElMessage
({
message
:
'手机号格式不正确'
,
type
:
'warning'
})
return
}
try
{
console
.
log
(
customer
)
let
param
:
any
=
{
name
:
customer
.
name
,
mobile
:
customer
.
mobile
,
company
:
customer
.
company
,
source
:
form
.
source
,
note
:
customer
.
note
,
}
await
aitoolsService
.
commonApi
(
'记录客户信息'
,
'take_customer_info'
,
param
);
// 统计
trackUserAction
(
form
.
source
,
form
.
user_id
,
title
.
value
,
'客户留资'
)
customerInfoVisible
.
value
=
false
// 防止重复提交
localStorage
.
setItem
(
"isSubmitCustomerInfo"
,
'yes'
);
// 新开一个浏览器窗口,打开一个链接
openNewWindow
();
}
catch
(
error
)
{
ElMessage
({
message
:
String
(
error
),
type
:
'error'
})
}
}
const
openNewWindow
=
()
=>
{
if
(
form
.
source
===
'pc'
)
{
window
.
open
(
form
.
report_url
,
'_blank'
);
}
else
{
if
(
form
.
source
===
'wechat'
)
{
ElMessageBox
.
confirm
(
'请先<span style="color: red;">「点击确认」</span>查看完整报告,如需保存报告到手机,再至右上角选择<span style="color: red;">「默认浏览器」</span>打开'
,
'微信提示'
,
{
confirmButtonText
:
'确认'
,
type
:
'info'
,
center
:
true
,
showClose
:
false
,
showCancelButton
:
false
,
dangerouslyUseHTMLString
:
true
,
closeOnClickModal
:
false
,
closeOnPressEscape
:
false
,
}
).
then
(()
=>
{
window
.
location
.
href
=
form
.
report_url
;
})
}
else
{
window
.
location
.
href
=
form
.
report_url
;
}
}
}
const
onClickDownloadPDF
=
()
=>
{
const
isSubmitCustomerInfo
=
localStorage
.
getItem
(
"isSubmitCustomerInfo"
);
console
.
log
(
'isSubmitCustomerInfo ='
,
isSubmitCustomerInfo
);
if
(
isSubmitCustomerInfo
===
'yes'
)
{
customerInfoVisible
.
value
=
false
// 直接打开链接
openNewWindow
();
}
else
{
customerInfoVisible
.
value
=
true
}
}
return
{
customer
,
customerInfoVisible
,
onCustomerInfoSubmit
,
onClickDownloadPDF
,
}
}
src/views/sample_handle_v2/compositions/fileUpload.ts
0 → 100644
View file @
54d92562
import
{
ref
}
from
'vue'
import
{
ElMessage
,
genFileId
,
type
UploadInstance
,
type
UploadProps
,
type
UploadRawFile
,
type
UploadFile
,
}
from
'element-plus'
export
const
myFileUpload
=
(
FileSizeLimitM
:
number
,
form
:
any
,
loading
:
any
)
=>
{
// ############ 处理上传文件 begin #############
const
upload
=
ref
<
UploadInstance
>
()
const
actionUrl
=
ref
(
import
.
meta
.
env
.
MODE
===
'production'
?
'/file'
:
import
.
meta
.
env
.
VITE_APP_BASE_API
+
'/file'
)
const
handleBeforeUpload
=
async
(
file
:
any
)
=>
{
const
isLimit
=
file
.
size
/
1024
/
1024
<=
FileSizeLimitM
if
(
!
isLimit
)
{
ElMessage
.
error
(
'上传文件大小不能超过 '
+
FileSizeLimitM
+
'MB!'
)
return
false
}
}
const
handleUploadSuccess
=
(
val
:
Wm
.
UploadResult
,
file
:
UploadFile
)
=>
{
// console.log(val)
if
(
val
.
code
==
0
)
{
form
.
file_path
=
val
.
data
[
0
].
path
console
.
log
(
'form.file_path ='
,
form
.
file_path
)
form
.
file_path_domain
=
val
.
data
[
0
].
url
console
.
log
(
'form.file_path_domain ='
,
form
.
file_path_domain
)
// console.log('file =', file.name, file.size, file.url)
if
(
file
.
name
&&
file
.
size
!==
undefined
)
{
form
.
file_name
=
file
.
name
form
.
file_size
=
file
.
size
}
ElMessage
({
message
:
'上传成功'
,
type
:
'success'
})
}
else
{
ElMessage
({
message
:
'上传失败:'
+
val
.
message
,
type
:
'error'
})
}
loading
.
value
=
false
}
const
handleUploadExceed
:
UploadProps
[
'onExceed'
]
=
(
files
)
=>
{
// 清除已上传的文件
upload
.
value
!
.
clearFiles
()
// 获取超出限制的第一个文件
const
file
=
files
[
0
]
as
UploadRawFile
// 给文件分配一个新的唯一标识
file
.
uid
=
genFileId
()
// 手动触发文件上传
upload
.
value
!
.
handleStart
(
file
)
// 提交上传
upload
.
value
!
.
submit
()
}
const
handleUploadError
=
(
error
:
Error
)
=>
{
ElMessage
({
message
:
String
(
error
.
message
),
type
:
'error'
})
loading
.
value
=
false
}
const
handleUploadProgress
=
(
event
:
ProgressEvent
,
file
:
UploadFile
)
=>
{
file
.
percentage
=
Math
.
round
(
event
.
loaded
/
event
.
total
*
100
)
console
.
log
(
'上传进度:'
,
file
.
percentage
)
file
.
url
=
'/video-icon.png'
// 根据进度控制 loading 状态
if
(
file
.
percentage
<=
100
)
{
loading
.
value
=
true
}
// if (file.percentage == 100) {
// loading.value = false
// }
}
const
handleRemoveFile
=
()
=>
{
// 清除已上传的文件
// upload.value!.clearFiles()
form
.
file_path
=
""
form
.
file_path_domain
=
""
form
.
file_name
=
""
form
.
file_size
=
0
console
.
log
(
'文件列表移除File, form.file_path ='
,
form
.
file_path
,
', form.file_path_domain ='
,
form
.
file_path_domain
)
}
// ############ 处理上传文件 end #############
return
{
loading
,
upload
,
actionUrl
,
handleBeforeUpload
,
handleUploadSuccess
,
handleUploadExceed
,
handleUploadError
,
handleUploadProgress
,
handleRemoveFile
,
}
}
src/views/sample_handle_v2/compositions/process.ts
0 → 100644
View file @
54d92562
import
aitoolsService
from
'@/api/service/aitoolsService'
import
utils
from
'@/utils/utils'
import
{
ElMessage
,
ElLoading
,
ElMessageBox
}
from
'element-plus'
import
type
{
Action
}
from
'element-plus'
export
const
onProcessing
=
(
form
:
any
,
steps_active
:
any
,
process_loading
:
any
,
result_loading
:
any
)
=>
{
const
fileToClassify
=
async
()
=>
{
if
(
!
form
.
file_path
||
form
.
file_path
.
length
==
0
)
{
ElMessage
({
message
:
'请先上传文件'
,
type
:
'warning'
})
return
}
// 进入第二页
from_first_to_second
();
// 清除结果
form
.
sample_result
=
{
root_path
:
''
,
data
:
[{
dir_name
:
''
,
images
:
[],
}]
};
// 提交处理请求
try
{
let
param
:
any
=
{
task_id
:
form
.
task_id
,
file_path
:
form
.
file_path
,
crop_range
:
JSON
.
stringify
(
form
.
crop_range
),
box_range
:
JSON
.
stringify
(
form
.
box_range
),
output_type
:
"dir"
,
classes_select
:
""
,
target_fps
:
form
.
target_fps
,
// 目标帧率
}
process_loading
.
value
=
true
;
// 发起请求
aitoolsService
.
commonApi
(
'提交处理'
,
'gen_sample_from_video'
,
param
)
.
then
((
response
)
=>
{
// console.log(form)
console
.
log
(
`接口返回:
${
response
}
`
);
form
.
sample_result
=
response
;
// 设置默认值
form
.
classes_select
=
{}
form
.
classes_select2
=
{}
response
.
data
.
forEach
((
item
:
{
dir_name
:
string
;
images
:
string
[]
})
=>
{
form
.
classes_select
[
item
.
dir_name
]
=
'0'
;
form
.
classes_select2
[
item
.
dir_name
]
=
''
;
});
process_loading
.
value
=
false
;
})
.
catch
((
error
)
=>
{
ElMessage
({
message
:
error
,
type
:
'error'
});
// // 重置task_id
// form.task_id = utils.genDateTimeStr();
// console.log('重置 task_id =', form.task_id);
process_loading
.
value
=
false
;
})
}
catch
(
error
:
any
)
{
ElMessage
({
message
:
error
,
type
:
'error'
});
process_loading
.
value
=
false
;
}
}
function
isAllDataClassified
():
boolean
{
let
is_ok
=
false
;
if
(
!
form
.
sample_result
?.
data
||
!
form
.
classes_select
)
{
ElMessage
({
message
:
'数据未正确初始化'
,
type
:
'error'
});
}
form
.
sample_result
.
data
.
every
((
item
:
{
dir_name
:
string
;
images
:
string
[]
})
=>
{
if
(
!
form
.
classes_select
.
hasOwnProperty
(
item
.
dir_name
))
{
ElMessage
({
message
:
`请为
${
item
.
dir_name
}
分类`
,
type
:
'error'
});
}
else
{
is_ok
=
true
}
});
return
is_ok
;
}
const
classifyToDownload
=
async
()
=>
{
if
(
!
isAllDataClassified
())
{
return
}
// 进入第3页
from_second_to_third
();
// 清除结果
form
.
sample_path
=
''
;
// 提交处理请求
try
{
let
param
:
any
=
{
task_id
:
form
.
task_id
,
file_path
:
form
.
file_path
,
crop_range
:
JSON
.
stringify
(
form
.
crop_range
),
box_range
:
JSON
.
stringify
(
form
.
box_range
),
output_type
:
"zip"
,
classes_select
:
JSON
.
stringify
(
form
.
classes_select
),
target_fps
:
form
.
target_fps
,
// 目标帧率
}
result_loading
.
value
=
true
;
// 发起请求
aitoolsService
.
commonApi
(
'提交处理'
,
'gen_sample_from_video'
,
param
)
.
then
((
response
)
=>
{
// console.log(form)
console
.
log
(
`接口返回:
${
response
}
`
);
form
.
sample_path
=
response
.
sample_path
;
result_loading
.
value
=
false
;
})
.
catch
((
error
)
=>
{
ElMessage
({
message
:
error
,
type
:
'error'
});
// // 重置task_id
// form.task_id = utils.genDateTimeStr();
// console.log('重置 task_id =', form.task_id);
result_loading
.
value
=
false
;
})
}
catch
(
error
:
any
)
{
ElMessage
({
message
:
error
,
type
:
'error'
});
result_loading
.
value
=
false
;
}
}
const
from_first_to_second
=
()
=>
{
steps_active
.
value
=
1
}
const
back_to_first
=
()
=>
{
steps_active
.
value
=
0
;
form
.
task_id
=
utils
.
genDateTimeStr
();
console
.
log
(
'返回首页,task_id ='
,
form
.
task_id
);
}
const
from_second_to_third
=
()
=>
{
steps_active
.
value
=
2
;
}
const
back_to_second
=
()
=>
{
steps_active
.
value
=
1
;
form
.
classes_select2
=
JSON
.
parse
(
JSON
.
stringify
(
form
.
classes_select
));
}
function
downloadFile
(
url
:
string
,
filename
?:
string
)
{
const
a
=
document
.
createElement
(
'a'
)
a
.
href
=
url
if
(
filename
)
{
a
.
download
=
filename
// 指定下载文件名
}
a
.
target
=
'_blank'
document
.
body
.
appendChild
(
a
)
a
.
click
()
document
.
body
.
removeChild
(
a
)
}
function
deleteOneSample
(
sample_dir
:
string
)
{
ElMessageBox
.
alert
(
`确认删除
${
sample_dir
}
这行样本吗?`
,
'删除样本'
,
{
// autofocus: false,
confirmButtonText
:
'删除'
,
callback
:
(
action
:
Action
)
=>
{
if
(
action
===
'confirm'
)
{
// 删除 form.sample_result.data
const
index
=
form
.
sample_result
.
data
.
findIndex
((
item
:
any
)
=>
item
.
dir_name
===
sample_dir
);
form
.
sample_result
.
data
.
splice
(
index
,
1
);
// 删除 form.classes_select[sample_dir]
delete
form
.
classes_select
[
sample_dir
];
// 删除 form.classes_select2[sample_dir]
delete
form
.
classes_select2
[
sample_dir
];
ElMessage
({
type
:
'success'
,
message
:
'删除成功'
})
}
},
})
}
return
{
from_first_to_second
,
back_to_first
,
from_second_to_third
,
back_to_second
,
fileToClassify
,
classifyToDownload
,
downloadFile
,
process_loading
,
result_loading
,
deleteOneSample
,
}
}
\ No newline at end of file
src/views/sample_handle_v2/compositions/useCropBox.ts
0 → 100644
View file @
54d92562
import
{
ref
}
from
'vue'
import
{
ElMessage
,
}
from
'element-plus'
export
function
useCropBox
(
form
:
any
,
canvas
:
any
,
ctx
:
any
)
{
const
isDrawing
=
ref
(
false
)
const
startX
=
ref
(
0
)
const
startY
=
ref
(
0
)
const
endX
=
ref
(
0
)
const
endY
=
ref
(
0
)
const
boxes
=
ref
<
Array
<
{
start
:
number
[],
end
:
number
[]
}
>>
([])
function
generate_canvas
()
{
const
el
=
document
.
getElementById
(
'crop-canvas'
)
as
HTMLCanvasElement
if
(
el
)
{
canvas
.
value
=
el
ctx
.
value
=
canvas
.
value
.
getContext
(
'2d'
)
// 设置 canvas 尺寸与视频一致
const
video
=
document
.
getElementById
(
'video-player'
)
as
HTMLVideoElement
if
(
video
)
{
if
(
video
.
videoWidth
>
0
&&
video
.
videoHeight
>
0
)
{
console
.
log
(
'video size ='
,
video
.
videoWidth
,
video
.
videoHeight
)
canvas
.
value
.
width
=
video
.
videoWidth
canvas
.
value
.
height
=
video
.
videoHeight
canvas
.
value
.
style
.
zIndex
=
'2'
drawBoxes
()
}
}
}
}
function
startDrawing
(
e
:
MouseEvent
)
{
if
(
boxes
.
value
.
length
>=
1
)
{
while
(
boxes
.
value
.
length
>
1
)
{
boxes
.
value
.
pop
()
}
return
}
const
rect
=
canvas
.
value
!
.
getBoundingClientRect
()
isDrawing
.
value
=
true
startX
.
value
=
e
.
clientX
-
rect
.
left
startY
.
value
=
e
.
clientY
-
rect
.
top
endX
.
value
=
startX
.
value
endY
.
value
=
startY
.
value
console
.
log
(
'开始绘制,起点:'
,
startX
.
value
,
startY
.
value
)
}
function
draw
(
e
:
MouseEvent
)
{
if
(
!
isDrawing
.
value
)
return
const
rect
=
canvas
.
value
!
.
getBoundingClientRect
()
endX
.
value
=
e
.
clientX
-
rect
.
left
endY
.
value
=
e
.
clientY
-
rect
.
top
clearCanvas
()
drawBoxes
()
drawRect
(
startX
.
value
,
startY
.
value
,
endX
.
value
-
startX
.
value
,
endY
.
value
-
startY
.
value
)
}
function
endDrawing
()
{
if
(
!
isDrawing
.
value
)
return
isDrawing
.
value
=
false
boxes
.
value
.
push
({
start
:
[
startX
.
value
,
startY
.
value
],
end
:
[
endX
.
value
,
endY
.
value
]
})
clearCanvas
()
drawBoxes
()
console
.
log
(
'结束绘制,终点:'
,
endX
.
value
,
endY
.
value
)
console
.
log
(
'框'
,
boxes
.
value
.
length
,
'个: '
,
boxes
.
value
)
// if (boxes.value.length === 1) {
// form.crop_range = boxes.value[0]
// }
// if (boxes.value.length === 2) {
// if (boxes.value[1].start[0] < boxes.value[0].start[0]
// || boxes.value[1].start[1] < boxes.value[0].start[1]
// || boxes.value[1].end[0] > boxes.value[0].end[0]
// || boxes.value[1].end[1] > boxes.value[0].end[1]) {
// ElMessage({
// message: '扫描框必须在裁剪框内',
// type: 'warning'
// })
// reset()
// } else {
// form.box_range = boxes.value[1]
// form.is_set = true
// }
// }
if
(
boxes
.
value
.
length
===
1
)
{
form
.
box_range
=
boxes
.
value
[
0
]
form
.
is_set
=
true
}
}
function
drawRect
(
x
:
number
,
y
:
number
,
width
:
number
,
height
:
number
)
{
const
c
=
ctx
.
value
!
c
.
strokeStyle
=
'green'
c
.
lineWidth
=
2
c
.
strokeRect
(
x
,
y
,
width
,
height
)
}
function
drawBoxes
()
{
boxes
.
value
.
forEach
(
box
=>
{
drawRect
(
box
.
start
[
0
],
box
.
start
[
1
],
box
.
end
[
0
]
-
box
.
start
[
0
],
box
.
end
[
1
]
-
box
.
start
[
1
])
})
}
function
clearCanvas
()
{
const
c
=
canvas
.
value
!
ctx
.
value
!
.
clearRect
(
0
,
0
,
c
.
width
,
c
.
height
)
}
function
reset
()
{
if
(
boxes
.
value
.
length
===
0
)
{
return
}
while
(
boxes
.
value
.
length
>
0
)
{
boxes
.
value
.
pop
()
}
clearCanvas
()
form
.
crop_range
=
{}
form
.
box_range
=
{}
form
.
is_set
=
false
console
.
log
(
'重置绘制数据'
)
}
return
{
startDrawing
,
draw
,
endDrawing
,
clearCanvas
,
reset
,
generate_canvas
}
}
\ No newline at end of file
src/views/sample_handle_v2/compositions/useCropBoxV2.ts
0 → 100644
View file @
54d92562
import
{
ref
}
from
'vue'
import
{
ElMessage
,
}
from
'element-plus'
export
function
useCropBox
(
form
:
any
,
canvas
:
any
,
ctx
:
any
)
{
const
isDrawing
=
ref
(
false
)
const
startX
=
ref
(
0
)
const
startY
=
ref
(
0
)
const
endX
=
ref
(
0
)
const
endY
=
ref
(
0
)
const
boxes
=
ref
<
Array
<
{
start
:
number
[],
end
:
number
[]
}
>>
([])
const
draggingBoxIndex
=
ref
<
number
|
null
>
(
null
)
// 正在拖动的框索引
const
resizingBoxIndex
=
ref
<
number
|
null
>
(
null
)
// 正在缩放的框索引
const
resizeHandle
=
ref
<
'nw'
|
'ne'
|
'sw'
|
'se'
|
null
>
(
null
)
// 缩放角方向
const
dragStart
=
ref
({
x
:
0
,
y
:
0
})
// 拖动起始点
const
boxStart
=
ref
({
x
:
0
,
y
:
0
,
width
:
0
,
height
:
0
})
// 框原始位置
function
isInsideBox
(
x
:
number
,
y
:
number
,
box
:
any
):
boolean
{
return
(
x
>=
box
.
start
[
0
]
&&
x
<=
box
.
end
[
0
]
&&
y
>=
box
.
start
[
1
]
&&
y
<=
box
.
end
[
1
]
)
}
function
getHandleAt
(
x
:
number
,
y
:
number
,
box
:
any
):
'nw'
|
'ne'
|
'sw'
|
'se'
|
null
{
const
size
=
10
const
[
x1
,
y1
]
=
box
.
start
const
[
x2
,
y2
]
=
box
.
end
if
(
Math
.
abs
(
x
-
x1
)
<
size
&&
Math
.
abs
(
y
-
y1
)
<
size
)
return
'nw'
if
(
Math
.
abs
(
x
-
x2
)
<
size
&&
Math
.
abs
(
y
-
y1
)
<
size
)
return
'ne'
if
(
Math
.
abs
(
x
-
x1
)
<
size
&&
Math
.
abs
(
y
-
y2
)
<
size
)
return
'sw'
if
(
Math
.
abs
(
x
-
x2
)
<
size
&&
Math
.
abs
(
y
-
y2
)
<
size
)
return
'se'
return
null
}
function
onMouseDown
(
e
:
MouseEvent
)
{
const
rect
=
canvas
.
value
!
.
getBoundingClientRect
()
const
x
=
e
.
clientX
-
rect
.
left
const
y
=
e
.
clientY
-
rect
.
top
// 检查是否点击到了某个框的角
for
(
let
i
=
0
;
i
<
boxes
.
value
.
length
;
i
++
)
{
const
box
=
boxes
.
value
[
i
]
const
handle
=
getHandleAt
(
x
,
y
,
box
)
if
(
handle
)
{
resizingBoxIndex
.
value
=
i
resizeHandle
.
value
=
handle
return
}
if
(
isInsideBox
(
x
,
y
,
box
))
{
draggingBoxIndex
.
value
=
i
dragStart
.
value
=
{
x
,
y
}
const
[
bx1
,
by1
]
=
box
.
start
const
[
bx2
,
by2
]
=
box
.
end
boxStart
.
value
=
{
x
:
bx1
,
y
:
by1
,
width
:
bx2
-
bx1
,
height
:
by2
-
by1
}
return
}
}
}
function
onMouseMove
(
e
:
MouseEvent
)
{
const
rect
=
canvas
.
value
!
.
getBoundingClientRect
()
const
x
=
e
.
clientX
-
rect
.
left
const
y
=
e
.
clientY
-
rect
.
top
if
(
resizingBoxIndex
.
value
!==
null
&&
resizeHandle
.
value
)
{
const
i
=
resizingBoxIndex
.
value
const
box
=
boxes
.
value
[
i
]
const
[
bx1
,
by1
]
=
box
.
start
const
[
bx2
,
by2
]
=
box
.
end
switch
(
resizeHandle
.
value
)
{
case
'nw'
:
boxes
.
value
[
i
]
=
{
start
:
[
x
,
y
],
end
:
[
bx2
,
by2
]
}
break
case
'ne'
:
boxes
.
value
[
i
]
=
{
start
:
[
bx1
,
y
],
end
:
[
x
,
by2
]
}
break
case
'sw'
:
boxes
.
value
[
i
]
=
{
start
:
[
x
,
by1
],
end
:
[
bx2
,
y
]
}
break
case
'se'
:
boxes
.
value
[
i
]
=
{
start
:
[
bx1
,
by1
],
end
:
[
x
,
y
]
}
break
}
clearCanvas
()
drawBoxes
()
if
(
i
===
0
)
{
form
.
crop_range
=
boxes
.
value
[
0
]
}
else
{
form
.
box_range
=
boxes
.
value
[
1
]
}
}
else
if
(
draggingBoxIndex
.
value
!==
null
)
{
const
i
=
draggingBoxIndex
.
value
const
dx
=
x
-
dragStart
.
value
.
x
const
dy
=
y
-
dragStart
.
value
.
y
const
box
=
boxes
.
value
[
i
]
boxes
.
value
[
i
]
=
{
start
:
[
box
.
start
[
0
]
+
dx
,
box
.
start
[
1
]
+
dy
],
end
:
[
box
.
end
[
0
]
+
dx
,
box
.
end
[
1
]
+
dy
]
}
clearCanvas
()
drawBoxes
()
if
(
i
===
0
)
{
form
.
crop_range
=
boxes
.
value
[
0
]
}
else
{
form
.
box_range
=
boxes
.
value
[
1
]
}
}
}
function
onMouseUp
()
{
draggingBoxIndex
.
value
=
null
resizingBoxIndex
.
value
=
null
resizeHandle
.
value
=
null
}
function
generate_canvas
()
{
const
el
=
document
.
getElementById
(
'crop-canvas'
)
as
HTMLCanvasElement
if
(
el
)
{
canvas
.
value
=
el
ctx
.
value
=
canvas
.
value
.
getContext
(
'2d'
)
// 设置 canvas 尺寸与视频一致
const
video
=
document
.
getElementById
(
'video-player'
)
as
HTMLVideoElement
if
(
video
)
{
if
(
video
.
videoWidth
>
0
&&
video
.
videoHeight
>
0
)
{
console
.
log
(
'video size ='
,
video
.
videoWidth
,
video
.
videoHeight
)
canvas
.
value
.
width
=
video
.
videoWidth
canvas
.
value
.
height
=
video
.
videoHeight
canvas
.
value
.
style
.
zIndex
=
'2'
drawBoxes
()
}
}
// 绑定事件监听器
canvas
.
value
.
addEventListener
(
'mousedown'
,
onMouseDown
)
canvas
.
value
.
addEventListener
(
'mousemove'
,
onMouseMove
)
canvas
.
value
.
addEventListener
(
'mouseup'
,
onMouseUp
)
canvas
.
value
.
addEventListener
(
'mouseleave'
,
onMouseUp
)
}
}
function
startDrawing
(
e
:
MouseEvent
)
{
if
(
boxes
.
value
.
length
>=
2
)
{
while
(
boxes
.
value
.
length
>
2
)
{
boxes
.
value
.
pop
()
}
return
}
const
rect
=
canvas
.
value
!
.
getBoundingClientRect
()
isDrawing
.
value
=
true
startX
.
value
=
e
.
clientX
-
rect
.
left
startY
.
value
=
e
.
clientY
-
rect
.
top
endX
.
value
=
startX
.
value
endY
.
value
=
startY
.
value
console
.
log
(
'开始绘制,起点:'
,
startX
.
value
,
startY
.
value
)
}
function
draw
(
e
:
MouseEvent
)
{
if
(
!
isDrawing
.
value
)
return
const
rect
=
canvas
.
value
!
.
getBoundingClientRect
()
endX
.
value
=
e
.
clientX
-
rect
.
left
endY
.
value
=
e
.
clientY
-
rect
.
top
clearCanvas
()
drawBoxes
()
drawRect
(
startX
.
value
,
startY
.
value
,
endX
.
value
-
startX
.
value
,
endY
.
value
-
startY
.
value
)
}
function
endDrawing
()
{
if
(
!
isDrawing
.
value
)
return
isDrawing
.
value
=
false
boxes
.
value
.
push
({
start
:
[
startX
.
value
,
startY
.
value
],
end
:
[
endX
.
value
,
endY
.
value
]
})
clearCanvas
()
drawBoxes
()
console
.
log
(
'结束绘制,终点:'
,
endX
.
value
,
endY
.
value
)
console
.
log
(
'框'
,
boxes
.
value
.
length
,
'个: '
,
boxes
.
value
)
if
(
boxes
.
value
.
length
===
1
)
{
form
.
crop_range
=
boxes
.
value
[
0
]
}
if
(
boxes
.
value
.
length
===
2
)
{
if
(
boxes
.
value
[
1
].
start
[
0
]
<
boxes
.
value
[
0
].
start
[
0
]
||
boxes
.
value
[
1
].
start
[
1
]
<
boxes
.
value
[
0
].
start
[
1
]
||
boxes
.
value
[
1
].
end
[
0
]
>
boxes
.
value
[
0
].
end
[
0
]
||
boxes
.
value
[
1
].
end
[
1
]
>
boxes
.
value
[
0
].
end
[
1
])
{
ElMessage
({
message
:
'扫描框必须在裁剪框内'
,
type
:
'warning'
})
reset
()
}
else
{
form
.
box_range
=
boxes
.
value
[
1
]
form
.
is_set
=
true
}
}
}
function
drawRect
(
x
:
number
,
y
:
number
,
width
:
number
,
height
:
number
)
{
const
c
=
ctx
.
value
!
c
.
strokeStyle
=
'green'
c
.
lineWidth
=
2
c
.
strokeRect
(
x
,
y
,
width
,
height
)
}
function
drawBoxes
()
{
boxes
.
value
.
forEach
(
box
=>
{
drawRect
(
box
.
start
[
0
],
box
.
start
[
1
],
box
.
end
[
0
]
-
box
.
start
[
0
],
box
.
end
[
1
]
-
box
.
start
[
1
])
})
}
function
clearCanvas
()
{
const
c
=
canvas
.
value
!
ctx
.
value
!
.
clearRect
(
0
,
0
,
c
.
width
,
c
.
height
)
}
function
reset
()
{
if
(
boxes
.
value
.
length
===
0
)
{
return
}
while
(
boxes
.
value
.
length
>
0
)
{
boxes
.
value
.
pop
()
}
clearCanvas
()
form
.
crop_range
=
{}
form
.
box_range
=
{}
form
.
is_set
=
false
console
.
log
(
'重置绘制数据'
)
// 移除事件监听器
const
el
=
document
.
getElementById
(
'crop-canvas'
)
as
HTMLCanvasElement
if
(
el
)
{
el
.
removeEventListener
(
'mousedown'
,
onMouseDown
)
el
.
removeEventListener
(
'mousemove'
,
onMouseMove
)
el
.
removeEventListener
(
'mouseup'
,
onMouseUp
)
el
.
removeEventListener
(
'mouseleave'
,
onMouseUp
)
}
}
return
{
startDrawing
,
draw
,
endDrawing
,
clearCanvas
,
reset
,
generate_canvas
}
}
\ No newline at end of file
src/views/sample_handle_v2/index.css
0 → 100644
View file @
54d92562
.home-container
{
width
:
100%
;
}
.title
{
:is(span)
{
font-size
:
25px
;
font-weight
:
bold
;
color
:
#181818
;
}
text-align
:
center
;
margin
:
20px
0
0
0
;
}
.subtitle
{
:is(span)
{
font-size
:
13px
;
color
:
#181818
;
}
text-align
:
center
;
margin
:
0
0
20px
0
;
}
.progress
{
margin
:
10px
50px
;
text-align
:
center
;
}
.content-container
{
margin
:
20px
;
display
:
flex
;
flex-direction
:
row
;
/* 默认横向排列 */
justify-content
:
center
;
/* 水平居中 */
/* box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
border-radius
:
8px
;
/* 可选:添加圆角 */
/* background-color: #fff; /* 可选:添加背景色 */
@media
(
max-width
:
768px
)
{
/* 当屏幕宽度小于768px时变为竖向排列 */
flex-direction
:
column
;
}
}
/* 几个页面的公共样式 */
.upload-section
,
.progress-section
,
.report-section
{
padding
:
20px
;
/* 标题 */
.section-title
{
font-size
:
18px
;
font-weight
:
bold
;
color
:
#181818
;
}
/* 描述 */
.section-desc
{
font-size
:
12px
;
}
}
/* 上传页 */
.upload-section
{
/* 上传区域 */
.upload-div
{
margin
:
20px
0
;
/* border: 2px dashed #dddfe5; */
border-radius
:
4px
;
}
/* 已上传文件 */
.uploaded-div
{
margin
:
20px
0
;
padding
:
20px
10px
;
border-radius
:
4px
;
border
:
1px
dashed
#dddfe5
;
/* 添加边框样式 */
.uploaded-file-info
{
position
:
relative
;
width
:
auto
;
}
}
/* 按钮 */
.button
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
flex-start
;
.next
{
align-self
:
flex-end
;
}
}
}
/* 处理页 */
.progress-section
{
.row
{
display
:
flex
;
margin
:
10px
10px
0
0
;
flex-wrap
:
wrap
;
/* 如果图片过多,允许换行 */
.images
{
/* width: auto; */
/* display: inline-block; */
margin-right
:
10px
;
/* 可选:为每个图片之间添加间距 */
}
}
/* 按钮 */
.button
{
background-color
:
#f56c6c
;
;
margin
:
20px
0
;
padding
:
10px
;
display
:
flex
;
flex-direction
:
row
;
/* 修改为行排列 */
justify-content
:
space-between
;
/* 使按钮两端对齐 */
align-items
:
center
;
/* 垂直居中对齐 */
position
:
fixed
;
bottom
:
20px
;
gap
:
30px
;
}
}
/* 结果页*/
.report-section
{
/* 按钮 */
.button
{
margin
:
20px
0
;
display
:
flex
;
flex-direction
:
row
;
/* 修改为行排列 */
justify-content
:
space-between
;
/* 使按钮两端对齐 */
align-items
:
center
;
/* 垂直居中对齐 */
}
}
src/views/sample_handle_v2/index.vue
0 → 100644
View file @
54d92562
<
script
setup
lang=
"ts"
>
import
utils
from
'@/utils/utils'
import
{
onUpdated
,
onMounted
,
reactive
,
ref
}
from
'vue'
import
{
detectDeviceType
}
from
'./compositions/common'
import
{
myFileUpload
}
from
'./compositions/fileUpload'
import
{
useCropBox
}
from
'./compositions/useCropBox'
import
{
onProcessing
}
from
'./compositions/process'
import
{
Check
,
Delete
,
Edit
,
Message
,
Search
,
Star
,
}
from
'@element-plus/icons-vue'
const
title
=
ref
(
'元芒数字'
)
const
form
=
reactive
({
source
:
''
,
task_id
:
''
,
file_path
:
''
,
file_path_domain
:
''
,
file_name
:
''
,
file_size
:
0
,
crop_range
:
{},
box_range
:
{},
is_set
:
false
,
classes_select
:
<
Record
<
string
,
string
>>
({}),
classes_select2
:
<
Record
<
string
,
string
>>
({}),
sample_result
:
{
root_path
:
''
,
data
:
[{
dir_name
:
''
,
images
:
[],
}]
},
// 样本处理结果
sample_path
:
''
,
// 目标帧率
target_fps
:
'10'
,
// 图片显示的大小
image_width
:
'100'
,
})
const
steps_active
=
ref
(
0
)
const
FileSizeLimitM
=
500
const
file_loading
=
ref
(
false
)
const
my_file_upload
=
myFileUpload
(
FileSizeLimitM
,
form
,
file_loading
)
const
upload
=
my_file_upload
.
upload
const
actionUrl
=
my_file_upload
.
actionUrl
// 裁剪
const
videoPlayer
=
ref
<
HTMLVideoElement
|
null
>
(
null
)
const
canvas
=
ref
<
HTMLCanvasElement
|
null
>
(
null
)
const
ctx
=
ref
<
CanvasRenderingContext2D
|
null
>
(
null
)
const
cropBox
=
useCropBox
(
form
,
canvas
,
ctx
)
// 处理
const
classes
=
[{
value
:
'0'
,
label
:
'0'
},{
value
:
'1'
,
label
:
'1'
},{
value
:
'2'
,
label
:
'2'
},{
value
:
'3'
,
label
:
'3'
}]
const
process_loading
=
ref
(
false
)
const
result_loading
=
ref
(
false
)
const
processing
=
onProcessing
(
form
,
steps_active
,
process_loading
,
result_loading
)
// 目标帧率选项
const
fps_options
=
[
{
value
:
'10'
,
label
:
'10'
},
{
value
:
'25'
,
label
:
'25'
},
]
// 图片显示的大小选项
const
image_width_options
=
[
{
value
:
'100'
,
label
:
'100'
},
{
value
:
'150'
,
label
:
'150'
},
{
value
:
'200'
,
label
:
'200'
},
{
value
:
'250'
,
label
:
'250'
},
{
value
:
'300'
,
label
:
'300'
},
]
onMounted
(
async
()
=>
{
// 检测设备类型
form
.
source
=
detectDeviceType
();
// 设置页面标题
document
.
title
=
title
.
value
;
// 生成task_id
form
.
task_id
=
utils
.
genDateTimeStr
();
console
.
log
(
'页面加载,task_id ='
,
form
.
task_id
);
})
const
onVideoLoaded
=
()
=>
{
if
(
steps_active
.
value
===
0
&&
form
.
is_set
&&
form
.
sample_result
.
root_path
!==
''
)
{
console
.
log
(
'切换到步骤 0 视频加载完成'
);
cropBox
.
generate_canvas
();
}
};
</
script
>
<
template
>
<main
class=
"home-container"
>
<!-- 标题 -->
<div
class=
"title"
><el-text>
生成训练样本
</el-text></div>
<div
class=
"subtitle"
><el-text>
上传视频生成训练样本
</el-text></div>
<!-- 步骤条 -->
<div>
<el-steps
:active=
"steps_active"
align-center
finish-status=
"success"
>
<el-step
title=
"上传视频并设置范围"
/>
<el-step
title=
"样本分类"
/>
<el-step
title=
"下载样本"
/>
</el-steps>
</div>
<div
class=
"content-container"
>
<!-- 文件页 -->
<div
class=
"upload-section"
v-if=
"steps_active === 0"
>
<div><el-text
class=
"section-title"
>
上传文件、设置范围
</el-text></div>
<div><el-text
class=
"section-desc"
>
上传视频后,可以播放、暂停,然后点击设置,先画裁剪框,再画扫描框
</el-text></div>
<div
class=
"upload-div"
v-loading=
"file_loading"
v-if=
"!form.file_path"
>
<el-upload
ref=
"upload"
:show-file-list=
"false"
:limit=
"1"
drag
accept=
".mp4"
:action=
"actionUrl"
:on-success=
"my_file_upload.handleUploadSuccess"
:on-exceed=
"my_file_upload.handleUploadExceed"
:on-error=
"my_file_upload.handleUploadError"
:on-remove=
"my_file_upload.handleRemoveFile"
:before-upload=
"my_file_upload.handleBeforeUpload"
:on-progress=
"my_file_upload.handleUploadProgress"
list-type=
"picture"
>
<el-icon
class=
"el-icon--upload"
><upload-filled
/></el-icon>
<div
class=
"el-upload__text"
>
拖拽文件到这里 或
<em>
浏览文件
</em></div>
<div
class=
"el-upload__tip"
slot=
"tip"
>
文件格式mp4, 大小限制
{{
FileSizeLimitM
}}
M
</div>
</el-upload>
</div>
<div
class=
"uploaded-div"
v-if=
"form.file_path"
>
<div
class=
"uploaded-file-info"
>
<video
id=
"video-player"
:src=
"form.file_path_domain"
controls
@
loadeddata=
"onVideoLoaded"
></video>
<canvas
id=
"crop-canvas"
style=
"background-color: rgba(255, 0, 0, 0.1); position: absolute; left: 0; z-index: -1;"
@
mousedown=
"cropBox.startDrawing"
@
mousemove=
"cropBox.draw"
@
mouseup=
"cropBox.endDrawing"
>
</canvas>
</div>
<el-button
color=
"#181818"
@
click=
"cropBox.generate_canvas"
>
设置
</el-button>
<el-button
color=
"#181818"
@
click=
"cropBox.reset"
>
重置
</el-button>
<el-text
style=
"color: #181818; margin-left: 10px; font-weight: bold;"
>
目标帧率:
</el-text>
<el-select
v-model=
"form.target_fps"
placeholder=
"请选择"
style=
"width: 80px"
>
<el-option
v-for=
"item in fps_options"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
<!--
<p>
裁剪框坐标:
{{
form
.
crop_range
}}
</p>
-->
<p>
扫描框坐标:
{{
form
.
box_range
}}
</p>
<!--
<p>
{{
form
.
task_id
}}
</p>
-->
</div>
<div
class=
"button"
>
<el-button
class=
"next"
color=
"#181818"
size=
"large"
@
click=
"processing.fileToClassify"
:disabled=
"!form.is_set"
>
Next
</el-button>
</div>
</div>
<!-- 处理页 -->
<div
class=
"progress-section"
v-if=
"steps_active === 1"
>
<div><el-text
class=
"section-title"
>
样本分类
</el-text></div>
<div>
<el-text
class=
"section-desc"
>
当前fps=
{{
form
.
target_fps
}}
,图片显示大小:
</el-text>
<el-select
v-model=
"form.image_width"
placeholder=
"请选择"
style=
"width: 80px"
size=
"small"
>
<el-option
v-for=
"item in image_width_options"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
</div>
<!-- 内容 -->
<div
v-loading=
"process_loading"
>
<div
class=
"row"
v-for=
"dir in form.sample_result.data"
>
<div
class=
"images"
v-for=
"image in dir.images"
>
<img
:src=
"form.sample_result.root_path + dir.dir_name + '/' + form.classes_select2[dir.dir_name] + '/' + image"
:width=
"form.image_width"
/>
<p>
{{
image
}}
</p>
</div>
<div
class=
"classifications"
>
<p
style=
"margin-bottom: 5px;"
>
{{
dir
.
dir_name
}}
</p>
<el-select
v-model=
"form.classes_select[dir.dir_name]"
placeholder=
"分类"
size=
"large"
style=
"width: 100px;"
:class=
"[`class-select-$
{form.classes_select[dir.dir_name] || 'default'}`]"
>
<el-option
v-for=
"item in classes"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
<el-button
type=
"danger"
:icon=
"Delete"
circle
@
click=
"processing.deleteOneSample(dir.dir_name)"
style=
"margin-left: 10px;"
/>
</div>
</div>
</div>
<!--
<p>
{{
form
.
classes_select
}}
</p>
-->
<!-- 按钮 -->
<div
class=
"button"
>
<el-button
class=
"back"
size=
"large"
@
click=
"processing.back_to_first"
>
Back
</el-button>
<el-button
class=
"next"
color=
"#181818"
size=
"large"
@
click=
"processing.classifyToDownload"
>
Next
</el-button>
</div>
</div>
<!-- 结果页 -->
<div
class=
"report-section"
v-if=
"steps_active === 2"
>
<div><el-text
class=
"section-title"
>
结果
</el-text></div>
<div><el-text
class=
"section-desc"
>
下载样本
</el-text></div>
<!-- 内容 -->
<div
v-loading=
"result_loading"
>
<el-text
class=
"progress-info-sub"
>
{{
form
.
sample_path
}}
</el-text>
</div>
<!-- 按钮 -->
<div
class=
"button"
>
<el-button
class=
"back"
size=
"large"
@
click=
"processing.back_to_second"
>
Back
</el-button>
<el-button
class=
"next"
color=
"#181818"
size=
"large"
@
click=
"processing.downloadFile(form.sample_path)"
:disabled=
"!(form.sample_path && form.sample_path.length > 0)"
>
Download
</el-button>
</div>
</div>
</div>
</main>
</
template
>
<!-- 样式 只在当前页面生效,优先级比组件样式低 -->
<
style
lang=
"scss"
scoped
src=
"./index.css"
></
style
>
<!-- 全局样式,如果要改组件样式,得在这里改,但为了避免所有组件都改,这里可以设置class层级 -->
<
style
lang=
"scss"
>
.progress-section
{
.classifications
{
.class-select-1
{
.el-select__wrapper
{
background-color
:
aquamarine
;
}
}
.class-select-2
{
.el-select__wrapper
{
background-color
:
rgb
(
121
,
160
,
245
);
}
}
.class-select-3
{
.el-select__wrapper
{
background-color
:
rgb
(
164
,
139
,
246
);
}
}
}
}
</
style
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment