视频管理
This commit is contained in:
parent
c77a06511d
commit
9fb36f623d
|
|
@ -0,0 +1,20 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
// 设备管理->视频监控->保存画线数据
|
||||
export function saveVideoLineAPI(data) {
|
||||
return request({
|
||||
url: '/smartCar/device/video/saveVideoLine',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 设备管理->视频监控->获取视频流地址
|
||||
export function getVideoStreamAPI(params) {
|
||||
return request({
|
||||
url: '/smartCar/device/video/getVideoStream',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,847 @@
|
|||
<template>
|
||||
<div class="video-monitor-container">
|
||||
<!-- 顶部状态栏 -->
|
||||
<el-card class="top-bar-card" shadow="never">
|
||||
<div class="top-bar">
|
||||
<div class="status-bar">
|
||||
<span class="status-text">博诺思车辆道路计数仪</span>
|
||||
<span class="status-indicator" :class="deviceStatus">
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<div class="timestamp">
|
||||
<i class="el-icon-time"></i>
|
||||
{{ currentTime }}
|
||||
</div>
|
||||
<!-- 画线按钮组 -->
|
||||
<div class="draw-line-buttons">
|
||||
<el-button
|
||||
v-if="!isDrawingMode"
|
||||
type="primary"
|
||||
class="draw-line-btn"
|
||||
@click="handleDrawLine"
|
||||
icon="el-icon-edit"
|
||||
>
|
||||
画线
|
||||
</el-button>
|
||||
<template v-else>
|
||||
<el-button
|
||||
type="success"
|
||||
class="save-line-btn"
|
||||
@click="handleSaveLines"
|
||||
icon="el-icon-check"
|
||||
:loading="saving"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
class="cancel-line-btn"
|
||||
@click="handleCancelLines"
|
||||
icon="el-icon-close"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 视频播放区域 -->
|
||||
<el-card class="video-card" shadow="never" :body-style="{ padding: '0', height: '100%' }">
|
||||
<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"
|
||||
>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
|
||||
<!-- 画线画布覆盖层 -->
|
||||
<canvas
|
||||
ref="drawCanvas"
|
||||
class="draw-canvas"
|
||||
:class="{ active: isDrawingMode }"
|
||||
@mousedown="startDrawing"
|
||||
@mousemove="draw"
|
||||
@mouseup="stopDrawing"
|
||||
@mouseleave="stopDrawing"
|
||||
></canvas>
|
||||
<!-- 已保存的画线显示层(只读) -->
|
||||
<canvas
|
||||
ref="savedLineCanvas"
|
||||
class="saved-line-canvas"
|
||||
></canvas>
|
||||
|
||||
<!-- 车辆检测框覆盖层 -->
|
||||
<div class="detection-overlay">
|
||||
<div
|
||||
v-for="(vehicle, index) in detectedVehicles"
|
||||
:key="index"
|
||||
class="vehicle-box"
|
||||
:style="{
|
||||
left: vehicle.x + '%',
|
||||
top: vehicle.y + '%',
|
||||
width: vehicle.width + '%',
|
||||
height: vehicle.height + '%'
|
||||
}"
|
||||
>
|
||||
<div class="box-corner corner-tl"></div>
|
||||
<div class="box-corner corner-tr"></div>
|
||||
<div class="box-corner corner-bl"></div>
|
||||
<div class="box-corner corner-br"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视频加载提示 -->
|
||||
<div v-if="!videoUrl" class="video-placeholder">
|
||||
<i class="el-icon-video-camera"></i>
|
||||
<p>暂无视频流</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { saveVideoLineAPI } from '@/api/device/video'
|
||||
|
||||
export default {
|
||||
name: 'VideoMonitor',
|
||||
data() {
|
||||
return {
|
||||
deviceStatus: 'online', // online, offline, standby, upgrading
|
||||
currentTime: '',
|
||||
videoUrl: '', // 视频流地址,可以从接口获取
|
||||
isDrawingMode: false, // 是否开启画线模式
|
||||
isDrawing: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
endX: 0,
|
||||
endY: 0,
|
||||
line: null, // 保存画线的数据(只允许一条线)
|
||||
savedLine: null, // 已保存的画线数据(保存成功后保留)
|
||||
initialLine: null, // 初始的线(默认线)
|
||||
currentLine: null, // 当前正在画的线
|
||||
detectedVehicles: [], // 检测到的车辆数据
|
||||
timer: null,
|
||||
saving: false, // 保存中状态
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
statusText() {
|
||||
const statusMap = {
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
standby: '待机',
|
||||
upgrading: '升级中'
|
||||
}
|
||||
return statusMap[this.deviceStatus] || '未知'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateTime()
|
||||
this.timer = setInterval(() => {
|
||||
this.updateTime()
|
||||
}, 1000)
|
||||
|
||||
// 初始化画布
|
||||
this.$nextTick(() => {
|
||||
this.initCanvas()
|
||||
// 初始化默认线(可以从接口获取,这里使用示例数据)
|
||||
this.initInitialLine()
|
||||
})
|
||||
|
||||
// 模拟加载视频(实际应该从接口获取)
|
||||
// this.loadVideoStream()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 更新时间 */
|
||||
updateTime() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const hours = String(now.getHours()).padStart(2, '0')
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
},
|
||||
|
||||
/** 初始化画布 */
|
||||
initCanvas() {
|
||||
const canvas = this.$refs.drawCanvas
|
||||
const savedCanvas = this.$refs.savedLineCanvas
|
||||
const container = this.$refs.videoContainer
|
||||
if (container) {
|
||||
if (canvas) {
|
||||
canvas.width = container.offsetWidth
|
||||
canvas.height = container.offsetHeight
|
||||
}
|
||||
if (savedCanvas) {
|
||||
savedCanvas.width = container.offsetWidth
|
||||
savedCanvas.height = container.offsetHeight
|
||||
// 如果有已保存的线或初始线,重新绘制
|
||||
if (this.savedLine || this.line || this.initialLine) {
|
||||
this.redrawCanvas()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** 初始化默认线 */
|
||||
initInitialLine() {
|
||||
// 可以从接口获取初始线数据
|
||||
// 这里使用示例数据:从左上角到右下角的一条线
|
||||
const container = this.$refs.videoContainer
|
||||
if (container) {
|
||||
const containerWidth = container.offsetWidth || 1920
|
||||
const containerHeight = container.offsetHeight || 1080
|
||||
|
||||
// 示例:从左上角20%位置到右下角80%位置
|
||||
this.initialLine = {
|
||||
startX: containerWidth * 0.2,
|
||||
startY: containerHeight * 0.2,
|
||||
endX: containerWidth * 0.8,
|
||||
endY: containerHeight * 0.8
|
||||
}
|
||||
|
||||
// 如果有初始线,设置为已保存的线并显示
|
||||
if (this.initialLine) {
|
||||
this.savedLine = { ...this.initialLine }
|
||||
this.redrawCanvas()
|
||||
}
|
||||
}
|
||||
|
||||
// 真实场景:从接口获取初始线
|
||||
// this.loadInitialLine()
|
||||
},
|
||||
|
||||
/** 处理画线按钮点击 */
|
||||
handleDrawLine() {
|
||||
this.isDrawingMode = true
|
||||
// 不清除已保存的线,保留旧线显示
|
||||
this.line = null // 重置当前画线数据
|
||||
this.clearCanvas() // 清除当前画布(只清除正在画的线)
|
||||
// 如果有已保存的线,重新绘制
|
||||
if (this.savedLine) {
|
||||
this.redrawCanvas()
|
||||
}
|
||||
this.$message.success('画线模式已开启,在视频上按住鼠标左键拖动即可画线(只能画一条线)')
|
||||
},
|
||||
|
||||
/** 保存画线 */
|
||||
async handleSaveLines() {
|
||||
// 如果有已保存的线,可以继续保存(更新保存)
|
||||
// 如果没有新画的线,但有已保存的线,也可以保存(更新保存)
|
||||
const lineToSave = this.line || this.savedLine
|
||||
|
||||
if (!lineToSave) {
|
||||
this.$message.warning('请先画线后再保存')
|
||||
return
|
||||
}
|
||||
|
||||
this.saving = true
|
||||
try {
|
||||
// 获取视频容器的尺寸,用于计算相对坐标
|
||||
const container = this.$refs.videoContainer
|
||||
const containerWidth = container ? container.offsetWidth : 1920
|
||||
const containerHeight = container ? container.offsetHeight : 1080
|
||||
|
||||
// 准备保存的数据,将像素坐标转换为相对坐标(百分比)
|
||||
const lineData = {
|
||||
startX: (lineToSave.startX / containerWidth) * 100,
|
||||
startY: (lineToSave.startY / containerHeight) * 100,
|
||||
endX: (lineToSave.endX / containerWidth) * 100,
|
||||
endY: (lineToSave.endY / containerHeight) * 100,
|
||||
}
|
||||
|
||||
const params = {
|
||||
line: lineData,
|
||||
videoUrl: this.videoUrl,
|
||||
deviceId: this.$route.query.deviceId || '', // 如果有设备ID
|
||||
}
|
||||
|
||||
// 模拟保存成功(延迟500ms模拟API调用)
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// 模拟API返回成功
|
||||
const res = { code: 200, msg: '保存成功' }
|
||||
|
||||
// 真实API调用(注释掉,使用模拟数据)
|
||||
// const res = await saveVideoLineAPI(params)
|
||||
|
||||
if (res.code === 200) {
|
||||
this.$message.success('画线数据保存成功')
|
||||
// 保存成功后,将当前线保存到已保存的线,同时更新初始线
|
||||
if (this.line) {
|
||||
const newLine = { ...this.line }
|
||||
this.savedLine = newLine
|
||||
// 保存成功后,同时更新初始线为新保存的线
|
||||
this.initialLine = { ...newLine }
|
||||
this.line = null
|
||||
}
|
||||
// 关闭画线模式,但保留画线在画布上
|
||||
this.isDrawingMode = false
|
||||
// 清除当前画布
|
||||
this.clearCanvas()
|
||||
// 重绘已保存的画线(只显示最新的线)
|
||||
this.redrawCanvas()
|
||||
} else {
|
||||
this.$message.error(res.msg || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存画线数据失败:', error)
|
||||
this.$message.error('保存失败,请稍后重试')
|
||||
} finally {
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
|
||||
/** 取消画线 */
|
||||
handleCancelLines() {
|
||||
// 如果初始线还在,使用初始线
|
||||
if (this.initialLine) {
|
||||
this.isDrawingMode = false
|
||||
this.line = null // 清除当前正在画的线
|
||||
this.clearCanvas() // 清除当前画布
|
||||
// 恢复使用初始线
|
||||
this.savedLine = { ...this.initialLine }
|
||||
this.redrawCanvas()
|
||||
this.$message.info('已取消画线模式,恢复初始线')
|
||||
} else if (this.savedLine) {
|
||||
// 如果有已保存的线(但不是初始线),只关闭画线模式,不清空线
|
||||
this.isDrawingMode = false
|
||||
this.line = null // 清除当前正在画的线
|
||||
this.clearCanvas() // 清除当前画布
|
||||
// 保留已保存的画线,重新绘制
|
||||
this.redrawCanvas()
|
||||
this.$message.info('已取消画线模式')
|
||||
} else {
|
||||
// 如果没有已保存的线,清除所有画线
|
||||
this.$confirm('确定要取消画线吗?画线将被清除', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.isDrawingMode = false
|
||||
this.line = null
|
||||
this.clearCanvas()
|
||||
// 清除已保存的画线显示层
|
||||
const savedCanvas = this.$refs.savedLineCanvas
|
||||
if (savedCanvas) {
|
||||
const ctx = savedCanvas.getContext('2d')
|
||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||
}
|
||||
this.$message.info('已取消画线')
|
||||
}).catch(() => {
|
||||
// 用户取消操作
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/** 清除画布 */
|
||||
clearCanvas() {
|
||||
const canvas = this.$refs.drawCanvas
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
},
|
||||
|
||||
/** 开始画线 */
|
||||
startDrawing(e) {
|
||||
if (!this.isDrawingMode) return
|
||||
// 如果已经存在已保存的线,清除它(使用最新的线)
|
||||
if (this.savedLine) {
|
||||
this.savedLine = null
|
||||
// 清除已保存的画线显示层
|
||||
const savedCanvas = this.$refs.savedLineCanvas
|
||||
if (savedCanvas) {
|
||||
const ctx = savedCanvas.getContext('2d')
|
||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||
}
|
||||
}
|
||||
// 如果正在画新线,清除当前线(只允许一条线)
|
||||
if (this.line) {
|
||||
this.line = null
|
||||
}
|
||||
this.isDrawing = true
|
||||
const canvas = this.$refs.drawCanvas
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
this.startX = e.clientX - rect.left
|
||||
this.startY = e.clientY - rect.top
|
||||
this.currentLine = {
|
||||
startX: this.startX,
|
||||
startY: this.startY,
|
||||
endX: this.startX,
|
||||
endY: this.startY
|
||||
}
|
||||
},
|
||||
|
||||
/** 画线 */
|
||||
draw(e) {
|
||||
if (!this.isDrawing) return
|
||||
const canvas = this.$refs.drawCanvas
|
||||
const ctx = canvas.getContext('2d')
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const currentX = e.clientX - rect.left
|
||||
const currentY = e.clientY - rect.top
|
||||
|
||||
// 更新当前线的终点
|
||||
if (this.currentLine) {
|
||||
this.currentLine.endX = currentX
|
||||
this.currentLine.endY = currentY
|
||||
}
|
||||
|
||||
// 重绘画布:只绘制当前正在画的线(已保存的线在另一个画布上显示,不会被清除)
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.strokeStyle = '#00ff00'
|
||||
ctx.lineWidth = 2
|
||||
|
||||
// 绘制当前正在画的线
|
||||
if (this.currentLine) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(this.currentLine.startX, this.currentLine.startY)
|
||||
ctx.lineTo(this.currentLine.endX, this.currentLine.endY)
|
||||
ctx.stroke()
|
||||
}
|
||||
},
|
||||
|
||||
/** 停止画线 */
|
||||
stopDrawing() {
|
||||
if (this.isDrawing && this.currentLine) {
|
||||
// 只有当起点和终点不同时才保存
|
||||
if (this.currentLine.startX !== this.currentLine.endX ||
|
||||
this.currentLine.startY !== this.currentLine.endY) {
|
||||
// 如果已经存在已保存的线,清除它(使用最新的线)
|
||||
if (this.savedLine) {
|
||||
this.savedLine = null
|
||||
// 清除已保存的画线显示层
|
||||
const savedCanvas = this.$refs.savedLineCanvas
|
||||
if (savedCanvas) {
|
||||
const ctx = savedCanvas.getContext('2d')
|
||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存当前画的线(只保存一条线)
|
||||
this.line = {
|
||||
startX: this.currentLine.startX,
|
||||
startY: this.currentLine.startY,
|
||||
endX: this.currentLine.endX,
|
||||
endY: this.currentLine.endY
|
||||
}
|
||||
|
||||
// 重绘画布,显示当前画的线
|
||||
this.redrawCanvas()
|
||||
}
|
||||
this.currentLine = null
|
||||
}
|
||||
this.isDrawing = false
|
||||
},
|
||||
|
||||
/** 重绘画布 */
|
||||
redrawCanvas() {
|
||||
// 绘制到已保存的画线显示层
|
||||
const savedCanvas = this.$refs.savedLineCanvas
|
||||
if (savedCanvas) {
|
||||
const container = this.$refs.videoContainer
|
||||
if (container) {
|
||||
savedCanvas.width = container.offsetWidth
|
||||
savedCanvas.height = container.offsetHeight
|
||||
}
|
||||
|
||||
const ctx = savedCanvas.getContext('2d')
|
||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||
|
||||
// 绘制已保存的线(优先显示已保存的线,如果没有则显示当前线,最后显示初始线)
|
||||
const lineToDraw = this.savedLine || this.line || this.initialLine
|
||||
if (lineToDraw) {
|
||||
ctx.strokeStyle = '#00ff00'
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(lineToDraw.startX, lineToDraw.startY)
|
||||
ctx.lineTo(lineToDraw.endX, lineToDraw.endY)
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** 视频加载完成 */
|
||||
handleVideoLoaded() {
|
||||
console.log('视频加载完成')
|
||||
// 可以在这里获取视频尺寸并调整画布大小
|
||||
this.$nextTick(() => {
|
||||
this.initCanvas()
|
||||
// 如果有已保存的线或初始线,重新绘制
|
||||
if (this.savedLine || this.line || this.initialLine) {
|
||||
this.redrawCanvas()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 视频加载错误 */
|
||||
handleVideoError(e) {
|
||||
console.error('视频加载失败:', e)
|
||||
this.$message.error('视频加载失败,请检查视频源地址')
|
||||
},
|
||||
|
||||
/** 加载视频流 */
|
||||
async loadVideoStream() {
|
||||
// 这里应该调用API获取视频流地址
|
||||
// const res = await getVideoStreamAPI()
|
||||
// if (res.code === 200) {
|
||||
// this.videoUrl = res.data.streamUrl
|
||||
// }
|
||||
|
||||
// 示例:使用测试视频URL
|
||||
// this.videoUrl = 'https://example.com/video-stream.mp4'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.video-monitor-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 84px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #f1f6ff 20%, #e5efff 100%);
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top-bar-card {
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.status-text {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
|
||||
&.online {
|
||||
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.standby {
|
||||
background: linear-gradient(135deg, #e6a23c 0%, #ebb563 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.upgrading {
|
||||
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.top-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
|
||||
.timestamp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-family: 'Courier New', monospace;
|
||||
padding: 6px 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.draw-line-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.draw-line-btn {
|
||||
background: #1f72ea;
|
||||
border: none;
|
||||
box-shadow: 0px 4px 8px 0px rgba(51, 135, 255, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 10px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #4a8bff;
|
||||
box-shadow: 0px 6px 12px 0px rgba(51, 135, 255, 0.6);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.save-line-btn {
|
||||
background: #67c23a;
|
||||
border: none;
|
||||
box-shadow: 0px 4px 8px 0px rgba(103, 194, 58, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 10px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #85ce61;
|
||||
box-shadow: 0px 6px 12px 0px rgba(103, 194, 58, 0.6);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-line-btn {
|
||||
background: #909399;
|
||||
border: none;
|
||||
box-shadow: 0px 4px 8px 0px rgba(144, 147, 153, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 10px 24px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #a6a9ad;
|
||||
box-shadow: 0px 6px 12px 0px rgba(144, 147, 153, 0.6);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
border-radius: 4px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #909399;
|
||||
z-index: 0;
|
||||
|
||||
i {
|
||||
font-size: 64px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.draw-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.draw-canvas.active {
|
||||
pointer-events: auto;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.saved-line-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1.5;
|
||||
}
|
||||
|
||||
.detection-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.vehicle-box {
|
||||
position: absolute;
|
||||
border: 2px solid #00ff00;
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
animation: pulse 2s infinite;
|
||||
|
||||
.box-corner {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #00ff00;
|
||||
border: 2px solid #000;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.corner-tl {
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.corner-tr {
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
border-left: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.corner-bl {
|
||||
bottom: -6px;
|
||||
left: -6px;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&.corner-br {
|
||||
bottom: -6px;
|
||||
right: -6px;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式调整
|
||||
@media (max-width: 768px) {
|
||||
.video-monitor-container {
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
|
||||
.top-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.top-bar-card {
|
||||
::v-deep .el-card__body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue