视频对接

This commit is contained in:
cwchen 2026-01-08 15:53:56 +08:00
parent 2d5a6e8254
commit f3ee222607
9 changed files with 533 additions and 42 deletions

View File

@ -9,6 +9,8 @@ VUE_APP_ENV = 'development'
VUE_APP_BASE_API = '/dev-api'
VUE_APP_ONLYOFFICE_URL = 'http://36.33.26.201:19840'
# 图片预览路径
VUE_APP_FILE_URL = 'http://192.168.0.108:58089/smartCar/profile'
# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

View File

@ -5,6 +5,8 @@ VUE_APP_TITLE = 车辆道路监测系统
ENV = 'production'
VUE_APP_ENV = 'production'
VUE_APP_ONLYOFFICE_URL = 'http://36.33.26.201:19840'
# 图片预览路径
VUE_APP_FILE_URL = 'http://192.168.0.108:58089/smartCar/profile'
# 车辆道路监测系统/生产环境
VUE_APP_BASE_API = '/smart-car-api'

View File

@ -18,10 +18,28 @@ export function dataRecognitionDetailAPI(params) {
})
}
// 设备管理->数据识别->导出数据识别数据
// 设备管理->数据识别->导出数据识别数据(启动导出任务)
export function exportDataRecognitionAPI(params) {
return request({
url: '/smartCar/device/dataRecognition/exportDataRecognition',
url: '/smartCar/device/data-recognition/export',
method: 'GET',
params
})
}
// 设备管理->数据识别->查询导出进度
export function getDataRecognitionExportProgressAPI(params) {
return request({
url: '/smartCar/device/data-recognition/progress',
method: 'GET',
params
})
}
// 设备管理->数据识别->下载导出文件
export function downloadDataRecognitionExportFileAPI(params) {
return request({
url: '/smartCar/device/data-recognition/download',
method: 'GET',
params,
responseType: 'blob'

View File

@ -92,6 +92,11 @@ export const constantRoutes = [
path: '/device/image-recognition/export-progress',
component: () => import('@/views/device/image-recognition/ExportProgress'),
hidden: true
},
{
path: '/device/data-recognition/export-progress',
component: () => import('@/views/device/data-recognition/ExportProgress'),
hidden: true
}
]

View File

@ -0,0 +1,453 @@
<template>
<div class="export-progress-container">
<div class="progress-card">
<div class="card-header">
<h3 class="title">导出进度</h3>
</div>
<div class="card-body">
<!-- 进度显示 -->
<div v-if="!isCompleted && !isError" class="progress-section">
<div class="progress-info">
<i class="el-icon-loading loading-icon"></i>
<span class="progress-text">{{ progressText }}</span>
</div>
<el-progress
:percentage="progress"
:status="progressStatus"
:stroke-width="8"
class="progress-bar">
</el-progress>
<div class="progress-detail">
<span>{{ progressDetailText }}</span>
</div>
</div>
<!-- 完成状态 -->
<div v-if="isCompleted" class="completed-section">
<div class="success-icon">
<i class="el-icon-success"></i>
</div>
<div class="success-text">{{ successText }}</div>
<el-button
type="primary"
class="download-btn"
@click="handleDownload">
<i class="el-icon-download"></i>
下载文件
</el-button>
</div>
<!-- 错误状态 -->
<div v-if="isError" class="error-section">
<div class="error-icon">
<i class="el-icon-error"></i>
</div>
<div class="error-text">{{ errorMessage || '导出失败,请稍后重试' }}</div>
<el-button
type="default"
class="close-btn"
@click="handleClose">
关闭窗口
</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { exportDataRecognitionAPI, getDataRecognitionExportProgressAPI, downloadDataRecognitionExportFileAPI } from '@/api/device/data-recognition'
export default {
name: 'DataRecognitionExportProgress',
data() {
return {
progress: 0,
progressText: '0%',
progressStatus: '',
isCompleted: false,
isError: false,
errorMessage: '',
taskId: null,
downloadUrl: null,
fileName: null,
pollTimer: null,
params: {},
isMultiDay: false //
}
},
computed: {
progressDetailText() {
return this.isMultiDay
? '正在生成压缩包,请稍候...'
: '正在生成Excel文件请稍候...'
},
successText() {
return this.isMultiDay
? '压缩包生成完成!'
: 'Excel文件生成完成'
}
},
created() {
// URL
const urlParams = new URLSearchParams(window.location.search)
const paramsStr = urlParams.get('params')
if (paramsStr) {
try {
this.params = JSON.parse(decodeURIComponent(paramsStr))
//
this.checkDateRange()
} catch (e) {
console.error('解析参数失败:', e)
this.isError = true
this.errorMessage = '参数解析失败'
return
}
}
//
this.startExport()
},
beforeDestroy() {
//
if (this.pollTimer) {
clearInterval(this.pollTimer)
}
},
methods: {
/** 检查日期范围 */
checkDateRange() {
if (this.params.startTime && this.params.endTime) {
const startDate = new Date(this.params.startTime.split(' ')[0])
const endDate = new Date(this.params.endTime.split(' ')[0])
//
const diffTime = Math.abs(endDate - startDate)
//
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
// 0
this.isMultiDay = diffDays > 0
}
},
/** 开始导出任务 */
async startExport() {
try {
//
const res = await exportDataRecognitionAPI(this.params)
if (res.code === 200) {
// ID
if (res.data && res.data.taskId) {
this.taskId = res.data.taskId
//
this.startPolling()
} else if (res.data && res.data.downloadUrl) {
//
this.downloadUrl = res.data.downloadUrl
this.fileName = res.data.fileName || this.getDefaultFileName()
this.isCompleted = true
this.progress = 100
} else if (res.data && typeof res.data === 'string') {
// ID
this.taskId = res.data
this.startPolling()
} else if (res.taskId) {
// ID
this.taskId = res.taskId
this.startPolling()
} else {
// ID
this.taskId = res.data || res.taskId
if (this.taskId) {
this.startPolling()
} else {
this.isError = true
this.errorMessage = '未获取到任务ID'
}
}
} else {
this.isError = true
this.errorMessage = res.msg || '启动导出任务失败'
}
} catch (error) {
console.error('启动导出任务失败:', error)
this.isError = true
this.errorMessage = error.message || '启动导出任务失败,请稍后重试'
}
},
/** 获取默认文件名 */
getDefaultFileName() {
const timestamp = new Date().getTime()
return this.isMultiDay
? `数据识别数据_${timestamp}.zip`
: `数据识别数据_${timestamp}.xlsx`
},
/** 开始轮询进度 */
startPolling() {
//
this.checkProgress()
// 2
this.pollTimer = setInterval(() => {
this.checkProgress()
}, 2000)
},
/** 查询导出进度 */
async checkProgress() {
if (!this.taskId) {
this.isError = true
this.errorMessage = '任务ID不存在'
return
}
try {
const res = await getDataRecognitionExportProgressAPI({ taskId: this.taskId })
if (res.code === 200) {
const data = res.data
//
this.progress = data.progress || 0
this.progressText = `${this.progress}%`
// 100%
if (this.progress >= 100 || data.status === 'completed' || data.status === 'success') {
this.isCompleted = true
this.downloadUrl = data.downloadUrl
this.fileName = data.fileName || this.getDefaultFileName()
//
if (this.pollTimer) {
clearInterval(this.pollTimer)
this.pollTimer = null
}
} else if (data.status === 'failed' || data.status === 'error') {
//
this.isError = true
this.errorMessage = data.message || '导出任务失败'
if (this.pollTimer) {
clearInterval(this.pollTimer)
this.pollTimer = null
}
}
} else {
this.isError = true
this.errorMessage = res.msg || '查询进度失败'
if (this.pollTimer) {
clearInterval(this.pollTimer)
this.pollTimer = null
}
}
} catch (error) {
console.error('查询进度失败:', error)
//
}
},
/** 下载文件 */
async handleDownload() {
try {
let blob
let fileName = this.fileName || this.getDefaultFileName()
if (this.downloadUrl && this.downloadUrl.startsWith('http')) {
// URL
window.open(this.downloadUrl, '_blank')
this.$message.success('开始下载')
return
} else if (this.taskId) {
// 使ID
const res = await downloadDataRecognitionExportFileAPI({
taskId: this.taskId
})
// blob
if (res instanceof Blob) {
blob = res
} else if (res.code === 200 && res.data) {
blob = res.data instanceof Blob ? res.data : new Blob([res.data], {
type: this.isMultiDay ? 'application/zip' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
if (res.data.fileName) {
fileName = res.data.fileName
}
} else {
this.$message.error(res.msg || '下载失败')
return
}
} else if (this.downloadUrl) {
// 使URL
const res = await downloadDataRecognitionExportFileAPI({
downloadUrl: this.downloadUrl
})
if (res instanceof Blob) {
blob = res
} else if (res.code === 200 && res.data) {
blob = res.data instanceof Blob ? res.data : new Blob([res.data], {
type: this.isMultiDay ? 'application/zip' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
} else {
this.$message.error(res.msg || '下载失败')
return
}
} else {
this.$message.error('下载地址不存在')
return
}
//
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
link.click()
window.URL.revokeObjectURL(url)
this.$message.success('下载成功')
} catch (error) {
console.error('下载失败:', error)
// 使downloadUrl
if (this.downloadUrl && this.downloadUrl.startsWith('http')) {
window.open(this.downloadUrl, '_blank')
} else {
this.$message.error('下载失败,请稍后重试')
}
}
},
/** 关闭窗口 */
handleClose() {
window.close()
}
}
}
</script>
<style scoped lang="scss">
.export-progress-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #f1f6ff 20%, #e5efff 100%);
padding: 20px;
.progress-card {
width: 500px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
.card-header {
padding: 20px 24px;
border-bottom: 1px solid #ebeef5;
background: #fff;
.title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
}
.card-body {
padding: 40px 24px;
.progress-section {
text-align: center;
.progress-info {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20px;
.loading-icon {
font-size: 24px;
color: #1f72ea;
margin-right: 12px;
animation: rotating 2s linear infinite;
}
.progress-text {
font-size: 16px;
font-weight: 600;
color: #333;
}
}
.progress-bar {
margin-bottom: 16px;
}
.progress-detail {
color: #606266;
font-size: 14px;
}
}
.completed-section {
text-align: center;
.success-icon {
margin-bottom: 20px;
i {
font-size: 64px;
color: #67c23a;
}
}
.success-text {
font-size: 16px;
color: #333;
margin-bottom: 24px;
font-weight: 500;
}
.download-btn {
width: 160px;
height: 40px;
background: #1f72ea;
box-shadow: 0px 4px 8px 0px rgba(51, 135, 255, 0.5);
border-radius: 4px;
border: none;
font-size: 14px;
&:hover {
background: #4a8bff;
box-shadow: 0px 6px 12px 0px rgba(51, 135, 255, 0.6);
}
}
}
.error-section {
text-align: center;
.error-icon {
margin-bottom: 20px;
i {
font-size: 64px;
color: #f56c6c;
}
}
.error-text {
font-size: 16px;
color: #333;
margin-bottom: 24px;
}
.close-btn {
width: 160px;
height: 40px;
border-radius: 4px;
font-size: 14px;
}
}
}
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@ -13,7 +13,7 @@ export const formLabel = [
export const columnsList = [
{ t_props: 'identificationLocation', t_label: '识别地点' },
{ t_props: 'identificationTime', t_label: '识别时间' },
{ t_props: 'carColor', t_label: '车牌颜色' },
{ t_slot: 'carColor', t_label: '车牌颜色' },
{ t_slot: 'carType', t_label: '车辆类型' },
{ t_slot: 'recognitionPhoto', t_label: '识别照片', t_width: '120px' },
]

View File

@ -36,6 +36,11 @@
<template slot="tableTitle">
<h3>数据列表</h3>
</template>
<template slot="carColor" slot-scope="{ data }">
<el-tag v-if="data.carColor === '1'" type="success">蓝色</el-tag>
<el-tag v-else-if="data.carColor === '2'" type="warning">绿色</el-tag>
<el-tag v-else type="info">{{ data.carColor }}</el-tag>
</template>
<template slot="carType" slot-scope="{ data }">
<el-tag v-if="data.carType === '1'" type="success">油车</el-tag>
<el-tag v-else-if="data.carType === '2'" type="warning">新能源</el-tag>
@ -57,8 +62,9 @@
<script>
import TableModel from '@/components/TableModel2'
import { columnsList } from './config'
import { dataRecognitionListAPI, dataRecognitionDetailAPI, exportDataRecognitionAPI } from '@/api/device/data-recognition'
import { dataRecognitionListAPI, dataRecognitionDetailAPI } from '@/api/device/data-recognition'
import test_image from '@/assets/images/test_image.png'
import { isExternal } from '@/utils/validate'
export default {
name: 'DataRecognition',
@ -113,7 +119,15 @@ export default {
// URL
if (this.$refs.dataRecognitionTableRef && this.$refs.dataRecognitionTableRef.tableList) {
return this.$refs.dataRecognitionTableRef.tableList
.map(item => item.recognitionPhoto || this.test_image)
.map(item => {
if (item.filePath) {
if (isExternal(item.filePath)) {
return item.filePath
} else {
return process.env.VUE_APP_FILE_URL + item.filePath
}
}
})
.filter(url => url)
}
return []
@ -243,34 +257,29 @@ export default {
},
/** 导出操作 */
async handleExport() {
try {
const params = { ...this.queryParams }
// :00 :59
if (params.startTime && params.startTime.length === 16) {
params.startTime = params.startTime + ':00'
}
if (params.endTime && params.endTime.length === 16) {
params.endTime = params.endTime + ':59'
}
const res = await exportDataRecognitionAPI(params)
if (res.code === 200) {
//
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `数据识别数据_${new Date().getTime()}.xlsx`
link.click()
window.URL.revokeObjectURL(url)
this.$message.success('导出成功')
} else {
this.$message.error(res.msg || '导出失败')
}
} catch (error) {
console.error('导出失败:', error)
this.$message.error('导出失败')
handleExport() {
const params = { ...this.queryParams }
// :00 :59
if (params.startTime && params.startTime.length === 16) {
params.startTime = params.startTime + ':00'
}
if (params.endTime && params.endTime.length === 16) {
params.endTime = params.endTime + ':59'
}
// URL
const paramsStr = encodeURIComponent(JSON.stringify(params))
const url = `${window.location.origin}${process.env.VUE_APP_ENV === 'production' ? '/smart-car' : ''}/device/data-recognition/export-progress?params=${paramsStr}`
//
const width = 750
const height = 500
const left = (window.screen.width - width) / 2
const top = (window.screen.height - height) / 2
//
window.open(url, '_blank', `width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`)
},
/** 时间范围变化 */

View File

@ -65,7 +65,7 @@
</template>
<script>
import { imageRecognitionListAPI, exportImageRecognitionAPI } from '@/api/device/image-recognition'
import { imageRecognitionListAPI } from '@/api/device/image-recognition'
import { isExternal } from '@/utils/validate'
import test_image from '@/assets/images/test_image.png'
export default {
@ -139,7 +139,7 @@ export default {
if (!carType) return ''
const typeMap = {
'1': '油车',
'2': '电车'
'2': '新能源'
}
return typeMap[carType] || carType
},
@ -183,14 +183,14 @@ export default {
// 使
const list = (res.rows || []).map(item => {
// 使 test_image
let imageUrl = this.test_image
/* if (item.filePath) {
// let imageUrl = this.test_image
if (item.filePath) {
if (isExternal(item.filePath)) {
imageUrl = item.filePath
} else {
imageUrl = process.env.VUE_APP_BASE_API + item.filePath
imageUrl = process.env.VUE_APP_FILE_URL + item.filePath
}
} */
}
return {
id: item.identificationDataId,

View File

@ -40,7 +40,9 @@
<div class="video-wrapper">
<div class="video-container" ref="videoContainer">
<video ref="videoPlayer" class="video-player" :src="videoUrl" autoplay muted controls
@loadedmetadata="handleVideoLoaded" @error="handleVideoError">
@loadedmetadata="handleVideoLoaded"
@error="handleVideoError"
>
您的浏览器不支持视频播放
</video>
@ -361,7 +363,7 @@ export default {
// 线线
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.strokeStyle = '#00ff00'
ctx.strokeStyle = '#ff0000'
ctx.lineWidth = 2
// 线
@ -423,7 +425,7 @@ export default {
// 线线线线
const lineToDraw = this.savedLine || this.line || this.initialLine
if (lineToDraw) {
ctx.strokeStyle = '#00ff00'
ctx.strokeStyle = '#ff0000'
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(lineToDraw.startX, lineToDraw.startY)
@ -649,7 +651,7 @@ export default {
.video-player {
width: 100%;
height: 100%;
object-fit: contain;
object-fit: fill;
background: #000;
}