Commit 54d92562 authored by Administrator's avatar Administrator

样本处理v2

parent 5decc3ef
......@@ -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: SampleHandleViewV2
},
{
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
},
]
})
......
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)
})
}
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,
}
}
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,
}
}
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
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
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
.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; /* 垂直居中对齐 */
}
}
<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>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment