视频播放修改
This commit is contained in:
parent
6bb7da638f
commit
b081921d0c
|
|
@ -1,6 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="video-monitor-container">
|
<div class="video-monitor-container">
|
||||||
<!-- 顶部状态栏 -->
|
|
||||||
<el-card class="top-bar-card" shadow="never">
|
<el-card class="top-bar-card" shadow="never">
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
|
|
@ -14,8 +13,12 @@
|
||||||
<i class="el-icon-time"></i>
|
<i class="el-icon-time"></i>
|
||||||
{{ currentTime }}
|
{{ currentTime }}
|
||||||
</div>
|
</div>
|
||||||
<!-- 画线按钮组 -->
|
|
||||||
<div class="draw-line-buttons">
|
<div class="draw-line-buttons">
|
||||||
|
<el-button type="default" plain class="fullscreen-btn" @click="toggleFullscreen"
|
||||||
|
icon="el-icon-full-screen">
|
||||||
|
{{ isFullscreen ? '退出全屏' : '全屏监控' }}
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-button v-if="!isDrawingMode" type="primary" class="draw-line-btn" @click="handleDrawLine"
|
<el-button v-if="!isDrawingMode" type="primary" class="draw-line-btn" @click="handleDrawLine"
|
||||||
icon="el-icon-edit">
|
icon="el-icon-edit">
|
||||||
画线
|
画线
|
||||||
|
|
@ -35,25 +38,19 @@
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 视频播放区域 -->
|
|
||||||
<el-card class="video-card" shadow="never" :body-style="{ padding: '0', height: '100%' }">
|
<el-card class="video-card" shadow="never" :body-style="{ padding: '0', height: '100%' }">
|
||||||
<div class="video-wrapper">
|
<div class="video-wrapper">
|
||||||
<div class="video-container" ref="videoContainer">
|
<div class="video-container" ref="videoContainer">
|
||||||
<video ref="videoPlayer" class="video-player" :src="videoUrl" autoplay muted controls
|
<video ref="videoPlayer" class="video-player" :src="videoUrl" autoplay muted controls
|
||||||
@loadedmetadata="handleVideoLoaded"
|
@loadedmetadata="handleVideoLoaded" @error="handleVideoError" controlsList="nofullscreen">
|
||||||
@error="handleVideoError"
|
|
||||||
>
|
|
||||||
您的浏览器不支持视频播放
|
您的浏览器不支持视频播放
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
<!-- 画线画布覆盖层 -->
|
|
||||||
<canvas ref="drawCanvas" class="draw-canvas" :class="{ active: isDrawingMode }"
|
<canvas ref="drawCanvas" class="draw-canvas" :class="{ active: isDrawingMode }"
|
||||||
@mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing"
|
@mousedown="startDrawing" @mousemove="draw" @mouseup="stopDrawing"
|
||||||
@mouseleave="stopDrawing"></canvas>
|
@mouseleave="stopDrawing"></canvas>
|
||||||
<!-- 已保存的画线显示层(只读) -->
|
|
||||||
<canvas ref="savedLineCanvas" class="saved-line-canvas"></canvas>
|
<canvas ref="savedLineCanvas" class="saved-line-canvas"></canvas>
|
||||||
|
|
||||||
<!-- 视频加载提示 -->
|
|
||||||
<div v-if="!videoUrl" class="video-placeholder">
|
<div v-if="!videoUrl" class="video-placeholder">
|
||||||
<i class="el-icon-video-camera"></i>
|
<i class="el-icon-video-camera"></i>
|
||||||
<p>暂无视频流</p>
|
<p>暂无视频流</p>
|
||||||
|
|
@ -72,21 +69,27 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentTime: '',
|
currentTime: '',
|
||||||
videoUrl: '', // 视频流地址,可以从接口获取
|
videoUrl: '',
|
||||||
isDrawingMode: false, // 是否开启画线模式
|
isDrawingMode: false,
|
||||||
isDrawing: false,
|
isDrawing: false,
|
||||||
startX: 0,
|
startX: 0,
|
||||||
startY: 0,
|
startY: 0,
|
||||||
endX: 0,
|
endX: 0,
|
||||||
endY: 0,
|
endY: 0,
|
||||||
line: null, // 保存画线的数据(只允许一条线)
|
line: null,
|
||||||
savedLine: null, // 已保存的画线数据(保存成功后保留)
|
savedLine: null,
|
||||||
initialLine: null, // 初始的线(默认线)
|
initialLine: null,
|
||||||
currentLine: null, // 当前正在画的线
|
currentLine: null,
|
||||||
detectedVehicles: [], // 检测到的车辆数据
|
detectedVehicles: [],
|
||||||
timer: null,
|
timer: null,
|
||||||
saving: false, // 保存中状态
|
saving: false,
|
||||||
videoErrorShown: false, // 是否已显示视频错误提示
|
videoErrorShown: false,
|
||||||
|
// 新增:全屏状态标记
|
||||||
|
isFullscreen: false,
|
||||||
|
// 新增:记录上一次容器尺寸,用于计算缩放比例
|
||||||
|
lastContainerWidth: 0,
|
||||||
|
lastContainerHeight: 0,
|
||||||
|
resizeObserver: null,
|
||||||
lineInfo: {
|
lineInfo: {
|
||||||
equipmentName: '',
|
equipmentName: '',
|
||||||
equipmentStatus: '',
|
equipmentStatus: '',
|
||||||
|
|
@ -114,23 +117,130 @@ export default {
|
||||||
this.updateTime()
|
this.updateTime()
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
// 初始化画布
|
// 初始化 ResizeObserver 监听容器大小变化
|
||||||
|
this.initResizeObserver()
|
||||||
|
|
||||||
|
// 监听全屏变化事件(处理 ESC 退出全屏的情况)
|
||||||
|
document.addEventListener('fullscreenchange', this.onFullscreenChange)
|
||||||
|
document.addEventListener('webkitfullscreenchange', this.onFullscreenChange)
|
||||||
|
document.addEventListener('mozfullscreenchange', this.onFullscreenChange)
|
||||||
|
document.addEventListener('MSFullscreenChange', this.onFullscreenChange)
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.initCanvas()
|
this.initCanvas()
|
||||||
// 初始化默认线(可以从接口获取,这里使用示例数据)
|
|
||||||
this.initInitialLine()
|
this.initInitialLine()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 模拟加载视频(实际应该从接口获取)
|
|
||||||
this.loadVideoStream()
|
this.loadVideoStream()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
clearInterval(this.timer)
|
clearInterval(this.timer)
|
||||||
}
|
}
|
||||||
|
// 销毁监听器
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
document.removeEventListener('fullscreenchange', this.onFullscreenChange)
|
||||||
|
document.removeEventListener('webkitfullscreenchange', this.onFullscreenChange)
|
||||||
|
document.removeEventListener('mozfullscreenchange', this.onFullscreenChange)
|
||||||
|
document.removeEventListener('MSFullscreenChange', this.onFullscreenChange)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
/** 更新时间 */
|
/** 初始化 ResizeObserver */
|
||||||
|
initResizeObserver() {
|
||||||
|
const container = this.$refs.videoContainer
|
||||||
|
if (!container) return
|
||||||
|
|
||||||
|
// 记录初始尺寸
|
||||||
|
this.lastContainerWidth = container.offsetWidth
|
||||||
|
this.lastContainerHeight = container.offsetHeight
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(entries => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
// 使用 requestAnimationFrame 避免频繁触发
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.handleResize(entry.contentRect)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.resizeObserver.observe(container)
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 处理容器大小变化(全屏或窗口缩放)核心逻辑 */
|
||||||
|
handleResize(rect) {
|
||||||
|
const newWidth = rect.width
|
||||||
|
const newHeight = rect.height
|
||||||
|
|
||||||
|
// 防止为0的情况
|
||||||
|
if (newWidth === 0 || newHeight === 0) return
|
||||||
|
|
||||||
|
// 1. 计算缩放比例
|
||||||
|
const scaleX = newWidth / this.lastContainerWidth
|
||||||
|
const scaleY = newHeight / this.lastContainerHeight
|
||||||
|
|
||||||
|
// 2. 更新 Canvas 物理分辨率以匹配新的 CSS 尺寸
|
||||||
|
const drawCanvas = this.$refs.drawCanvas
|
||||||
|
const savedCanvas = this.$refs.savedLineCanvas
|
||||||
|
|
||||||
|
if (drawCanvas) {
|
||||||
|
drawCanvas.width = newWidth
|
||||||
|
drawCanvas.height = newHeight
|
||||||
|
}
|
||||||
|
if (savedCanvas) {
|
||||||
|
savedCanvas.width = newWidth
|
||||||
|
savedCanvas.height = newHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 按比例更新所有线的坐标
|
||||||
|
const scaleLine = (l) => {
|
||||||
|
if (!l) return null
|
||||||
|
return {
|
||||||
|
startX: l.startX * scaleX,
|
||||||
|
startY: l.startY * scaleY,
|
||||||
|
endX: l.endX * scaleX,
|
||||||
|
endY: l.endY * scaleY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.initialLine) this.initialLine = scaleLine(this.initialLine)
|
||||||
|
if (this.savedLine) this.savedLine = scaleLine(this.savedLine)
|
||||||
|
if (this.line) this.line = scaleLine(this.line)
|
||||||
|
if (this.currentLine) this.currentLine = scaleLine(this.currentLine)
|
||||||
|
|
||||||
|
// 4. 更新记录的尺寸
|
||||||
|
this.lastContainerWidth = newWidth
|
||||||
|
this.lastContainerHeight = newHeight
|
||||||
|
|
||||||
|
// 5. 重绘
|
||||||
|
this.redrawCanvas()
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 全屏切换 */
|
||||||
|
toggleFullscreen() {
|
||||||
|
const container = this.$refs.videoContainer
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
// 进入全屏
|
||||||
|
if (container.requestFullscreen) {
|
||||||
|
container.requestFullscreen()
|
||||||
|
} else if (container.webkitRequestFullscreen) {
|
||||||
|
container.webkitRequestFullscreen()
|
||||||
|
} else if (container.msRequestFullscreen) {
|
||||||
|
container.msRequestFullscreen()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 退出全屏
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 监听全屏状态变化 */
|
||||||
|
onFullscreenChange() {
|
||||||
|
this.isFullscreen = !!document.fullscreenElement
|
||||||
|
},
|
||||||
|
|
||||||
updateTime() {
|
updateTime() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const year = now.getFullYear()
|
const year = now.getFullYear()
|
||||||
|
|
@ -142,12 +252,15 @@ export default {
|
||||||
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
this.currentTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 初始化画布 */
|
|
||||||
initCanvas() {
|
initCanvas() {
|
||||||
const canvas = this.$refs.drawCanvas
|
const canvas = this.$refs.drawCanvas
|
||||||
const savedCanvas = this.$refs.savedLineCanvas
|
const savedCanvas = this.$refs.savedLineCanvas
|
||||||
const container = this.$refs.videoContainer
|
const container = this.$refs.videoContainer
|
||||||
if (container) {
|
if (container) {
|
||||||
|
// 确保更新 lastContainerWidth
|
||||||
|
this.lastContainerWidth = container.offsetWidth
|
||||||
|
this.lastContainerHeight = container.offsetHeight
|
||||||
|
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
canvas.width = container.offsetWidth
|
canvas.width = container.offsetWidth
|
||||||
canvas.height = container.offsetHeight
|
canvas.height = container.offsetHeight
|
||||||
|
|
@ -155,7 +268,6 @@ export default {
|
||||||
if (savedCanvas) {
|
if (savedCanvas) {
|
||||||
savedCanvas.width = container.offsetWidth
|
savedCanvas.width = container.offsetWidth
|
||||||
savedCanvas.height = container.offsetHeight
|
savedCanvas.height = container.offsetHeight
|
||||||
// 如果有已保存的线或初始线,重新绘制
|
|
||||||
if (this.savedLine || this.line || this.initialLine) {
|
if (this.savedLine || this.line || this.initialLine) {
|
||||||
this.redrawCanvas()
|
this.redrawCanvas()
|
||||||
}
|
}
|
||||||
|
|
@ -163,7 +275,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 初始化默认线 */
|
|
||||||
async initInitialLine() {
|
async initInitialLine() {
|
||||||
try {
|
try {
|
||||||
const container = this.$refs.videoContainer
|
const container = this.$refs.videoContainer
|
||||||
|
|
@ -172,14 +283,11 @@ export default {
|
||||||
const containerWidth = container.offsetWidth || 1920
|
const containerWidth = container.offsetWidth || 1920
|
||||||
const containerHeight = container.offsetHeight || 1080
|
const containerHeight = container.offsetHeight || 1080
|
||||||
|
|
||||||
// 调用接口获取画线详情
|
|
||||||
const res = await getLineDetailAPI({})
|
const res = await getLineDetailAPI({})
|
||||||
|
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
const data = res.data
|
const data = res.data
|
||||||
this.lineInfo = data
|
this.lineInfo = data
|
||||||
|
|
||||||
// 如果有画线数据,将百分比坐标转换为像素坐标
|
|
||||||
if (data.startX && data.startY && data.endX && data.endY) {
|
if (data.startX && data.startY && data.endX && data.endY) {
|
||||||
this.initialLine = {
|
this.initialLine = {
|
||||||
startX: (parseFloat(data.startX) / 100) * containerWidth,
|
startX: (parseFloat(data.startX) / 100) * containerWidth,
|
||||||
|
|
@ -187,8 +295,6 @@ export default {
|
||||||
endX: (parseFloat(data.endX) / 100) * containerWidth,
|
endX: (parseFloat(data.endX) / 100) * containerWidth,
|
||||||
endY: (parseFloat(data.endY) / 100) * containerHeight
|
endY: (parseFloat(data.endY) / 100) * containerHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置为已保存的线并显示
|
|
||||||
this.savedLine = { ...this.initialLine }
|
this.savedLine = { ...this.initialLine }
|
||||||
this.redrawCanvas()
|
this.redrawCanvas()
|
||||||
}
|
}
|
||||||
|
|
@ -198,95 +304,71 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 处理画线按钮点击 */
|
|
||||||
handleDrawLine() {
|
handleDrawLine() {
|
||||||
this.isDrawingMode = true
|
this.isDrawingMode = true
|
||||||
// 不清除已保存的线,保留旧线显示
|
this.line = null
|
||||||
this.line = null // 重置当前画线数据
|
this.clearCanvas()
|
||||||
this.clearCanvas() // 清除当前画布(只清除正在画的线)
|
|
||||||
// 如果有已保存的线,重新绘制
|
|
||||||
if (this.savedLine) {
|
if (this.savedLine) {
|
||||||
this.redrawCanvas()
|
this.redrawCanvas()
|
||||||
}
|
}
|
||||||
this.$message.success('画线模式已开启,在视频上按住鼠标左键拖动即可画线(只能画一条线)')
|
this.$message.success('画线模式已开启,在视频上按住鼠标左键拖动即可画线')
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 保存画线 */
|
|
||||||
async handleSaveLines() {
|
async handleSaveLines() {
|
||||||
// 如果有已保存的线,可以继续保存(更新保存)
|
|
||||||
// 如果没有新画的线,但有已保存的线,也可以保存(更新保存)
|
|
||||||
const lineToSave = this.line || this.savedLine
|
const lineToSave = this.line || this.savedLine
|
||||||
|
|
||||||
if (!lineToSave) {
|
if (!lineToSave) {
|
||||||
this.$message.warning('请先画线后再保存')
|
this.$message.warning('请先画线后再保存')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
try {
|
try {
|
||||||
// 获取视频容器的尺寸,用于计算相对坐标
|
|
||||||
const container = this.$refs.videoContainer
|
const container = this.$refs.videoContainer
|
||||||
const containerWidth = container ? container.offsetWidth : 1920
|
const containerWidth = container ? container.offsetWidth : 1920
|
||||||
const containerHeight = container ? container.offsetHeight : 1080
|
const containerHeight = container ? container.offsetHeight : 1080
|
||||||
|
|
||||||
// 准备保存的数据,将像素坐标转换为相对坐标(百分比),并转换为字符串
|
|
||||||
const params = {
|
const params = {
|
||||||
startX: String(((lineToSave.startX / containerWidth) * 100).toFixed(2)),
|
startX: String(((lineToSave.startX / containerWidth) * 100).toFixed(2)),
|
||||||
startY: String(((lineToSave.startY / containerHeight) * 100).toFixed(2)),
|
startY: String(((lineToSave.startY / containerHeight) * 100).toFixed(2)),
|
||||||
endX: String(((lineToSave.endX / containerWidth) * 100).toFixed(2)),
|
endX: String(((lineToSave.endX / containerWidth) * 100).toFixed(2)),
|
||||||
endY: String(((lineToSave.endY / containerHeight) * 100).toFixed(2))
|
endY: String(((lineToSave.endY / containerHeight) * 100).toFixed(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await updateLineAPI(params)
|
const res = await updateLineAPI(params)
|
||||||
|
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
this.$message.success('画线数据保存成功')
|
this.$message.success('画线数据保存成功')
|
||||||
// 保存成功后,将当前线保存到已保存的线,同时更新初始线
|
|
||||||
if (this.line) {
|
if (this.line) {
|
||||||
const newLine = { ...this.line }
|
const newLine = { ...this.line }
|
||||||
this.savedLine = newLine
|
this.savedLine = newLine
|
||||||
// 保存成功后,同时更新初始线为新保存的线
|
|
||||||
this.initialLine = { ...newLine }
|
this.initialLine = { ...newLine }
|
||||||
this.line = null
|
this.line = null
|
||||||
}
|
}
|
||||||
// 关闭画线模式,但保留画线在画布上
|
|
||||||
this.isDrawingMode = false
|
this.isDrawingMode = false
|
||||||
// 清除当前画布
|
|
||||||
this.clearCanvas()
|
this.clearCanvas()
|
||||||
// 重绘已保存的画线(只显示最新的线)
|
|
||||||
this.redrawCanvas()
|
this.redrawCanvas()
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(res.msg || '保存失败')
|
this.$message.error(res.msg || '保存失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存画线数据失败:', error)
|
console.error('保存画线数据失败:', error)
|
||||||
// this.$message.error('保存失败,请稍后重试')
|
|
||||||
} finally {
|
} finally {
|
||||||
this.saving = false
|
this.saving = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 取消画线 */
|
|
||||||
handleCancelLines() {
|
handleCancelLines() {
|
||||||
// 如果初始线还在,使用初始线
|
|
||||||
if (this.initialLine) {
|
if (this.initialLine) {
|
||||||
this.isDrawingMode = false
|
this.isDrawingMode = false
|
||||||
this.line = null // 清除当前正在画的线
|
this.line = null
|
||||||
this.clearCanvas() // 清除当前画布
|
this.clearCanvas()
|
||||||
// 恢复使用初始线
|
|
||||||
this.savedLine = { ...this.initialLine }
|
this.savedLine = { ...this.initialLine }
|
||||||
this.redrawCanvas()
|
this.redrawCanvas()
|
||||||
this.$message.info('已取消画线模式,恢复初始线')
|
this.$message.info('已取消画线模式,恢复初始线')
|
||||||
} else if (this.savedLine) {
|
} else if (this.savedLine) {
|
||||||
// 如果有已保存的线(但不是初始线),只关闭画线模式,不清空线
|
|
||||||
this.isDrawingMode = false
|
this.isDrawingMode = false
|
||||||
this.line = null // 清除当前正在画的线
|
this.line = null
|
||||||
this.clearCanvas() // 清除当前画布
|
this.clearCanvas()
|
||||||
// 保留已保存的画线,重新绘制
|
|
||||||
this.redrawCanvas()
|
this.redrawCanvas()
|
||||||
this.$message.info('已取消画线模式')
|
this.$message.info('已取消画线模式')
|
||||||
} else {
|
} else {
|
||||||
// 如果没有已保存的线,清除所有画线
|
|
||||||
this.$confirm('确定要取消画线吗?画线将被清除', '提示', {
|
this.$confirm('确定要取消画线吗?画线将被清除', '提示', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
|
|
@ -295,20 +377,16 @@ export default {
|
||||||
this.isDrawingMode = false
|
this.isDrawingMode = false
|
||||||
this.line = null
|
this.line = null
|
||||||
this.clearCanvas()
|
this.clearCanvas()
|
||||||
// 清除已保存的画线显示层
|
|
||||||
const savedCanvas = this.$refs.savedLineCanvas
|
const savedCanvas = this.$refs.savedLineCanvas
|
||||||
if (savedCanvas) {
|
if (savedCanvas) {
|
||||||
const ctx = savedCanvas.getContext('2d')
|
const ctx = savedCanvas.getContext('2d')
|
||||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||||
}
|
}
|
||||||
this.$message.info('已取消画线')
|
this.$message.info('已取消画线')
|
||||||
}).catch(() => {
|
}).catch(() => { })
|
||||||
// 用户取消操作
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 清除画布 */
|
|
||||||
clearCanvas() {
|
clearCanvas() {
|
||||||
const canvas = this.$refs.drawCanvas
|
const canvas = this.$refs.drawCanvas
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
|
|
@ -317,20 +395,16 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 开始画线 */
|
|
||||||
startDrawing(e) {
|
startDrawing(e) {
|
||||||
if (!this.isDrawingMode) return
|
if (!this.isDrawingMode) return
|
||||||
// 如果已经存在已保存的线,清除它(使用最新的线)
|
|
||||||
if (this.savedLine) {
|
if (this.savedLine) {
|
||||||
this.savedLine = null
|
this.savedLine = null
|
||||||
// 清除已保存的画线显示层
|
|
||||||
const savedCanvas = this.$refs.savedLineCanvas
|
const savedCanvas = this.$refs.savedLineCanvas
|
||||||
if (savedCanvas) {
|
if (savedCanvas) {
|
||||||
const ctx = savedCanvas.getContext('2d')
|
const ctx = savedCanvas.getContext('2d')
|
||||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果正在画新线,清除当前线(只允许一条线)
|
|
||||||
if (this.line) {
|
if (this.line) {
|
||||||
this.line = null
|
this.line = null
|
||||||
}
|
}
|
||||||
|
|
@ -347,7 +421,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 画线 */
|
|
||||||
draw(e) {
|
draw(e) {
|
||||||
if (!this.isDrawing) return
|
if (!this.isDrawing) return
|
||||||
const canvas = this.$refs.drawCanvas
|
const canvas = this.$refs.drawCanvas
|
||||||
|
|
@ -356,18 +429,15 @@ export default {
|
||||||
const currentX = e.clientX - rect.left
|
const currentX = e.clientX - rect.left
|
||||||
const currentY = e.clientY - rect.top
|
const currentY = e.clientY - rect.top
|
||||||
|
|
||||||
// 更新当前线的终点
|
|
||||||
if (this.currentLine) {
|
if (this.currentLine) {
|
||||||
this.currentLine.endX = currentX
|
this.currentLine.endX = currentX
|
||||||
this.currentLine.endY = currentY
|
this.currentLine.endY = currentY
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重绘画布:只绘制当前正在画的线(已保存的线在另一个画布上显示,不会被清除)
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
ctx.strokeStyle = '#ff0000'
|
ctx.strokeStyle = '#ff0000'
|
||||||
ctx.lineWidth = 3
|
ctx.lineWidth = 3
|
||||||
|
|
||||||
// 绘制当前正在画的线
|
|
||||||
if (this.currentLine) {
|
if (this.currentLine) {
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(this.currentLine.startX, this.currentLine.startY)
|
ctx.moveTo(this.currentLine.startX, this.currentLine.startY)
|
||||||
|
|
@ -376,32 +446,24 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 停止画线 */
|
|
||||||
stopDrawing() {
|
stopDrawing() {
|
||||||
if (this.isDrawing && this.currentLine) {
|
if (this.isDrawing && this.currentLine) {
|
||||||
// 只有当起点和终点不同时才保存
|
|
||||||
if (this.currentLine.startX !== this.currentLine.endX ||
|
if (this.currentLine.startX !== this.currentLine.endX ||
|
||||||
this.currentLine.startY !== this.currentLine.endY) {
|
this.currentLine.startY !== this.currentLine.endY) {
|
||||||
// 如果已经存在已保存的线,清除它(使用最新的线)
|
|
||||||
if (this.savedLine) {
|
if (this.savedLine) {
|
||||||
this.savedLine = null
|
this.savedLine = null
|
||||||
// 清除已保存的画线显示层
|
|
||||||
const savedCanvas = this.$refs.savedLineCanvas
|
const savedCanvas = this.$refs.savedLineCanvas
|
||||||
if (savedCanvas) {
|
if (savedCanvas) {
|
||||||
const ctx = savedCanvas.getContext('2d')
|
const ctx = savedCanvas.getContext('2d')
|
||||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存当前画的线(只保存一条线)
|
|
||||||
this.line = {
|
this.line = {
|
||||||
startX: this.currentLine.startX,
|
startX: this.currentLine.startX,
|
||||||
startY: this.currentLine.startY,
|
startY: this.currentLine.startY,
|
||||||
endX: this.currentLine.endX,
|
endX: this.currentLine.endX,
|
||||||
endY: this.currentLine.endY
|
endY: this.currentLine.endY
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重绘画布,显示当前画的线
|
|
||||||
this.redrawCanvas()
|
this.redrawCanvas()
|
||||||
}
|
}
|
||||||
this.currentLine = null
|
this.currentLine = null
|
||||||
|
|
@ -409,21 +471,13 @@ export default {
|
||||||
this.isDrawing = false
|
this.isDrawing = false
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 重绘画布 */
|
|
||||||
redrawCanvas() {
|
redrawCanvas() {
|
||||||
// 绘制到已保存的画线显示层
|
|
||||||
const savedCanvas = this.$refs.savedLineCanvas
|
const savedCanvas = this.$refs.savedLineCanvas
|
||||||
if (savedCanvas) {
|
if (savedCanvas) {
|
||||||
const container = this.$refs.videoContainer
|
|
||||||
if (container) {
|
|
||||||
savedCanvas.width = container.offsetWidth
|
|
||||||
savedCanvas.height = container.offsetHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = savedCanvas.getContext('2d')
|
const ctx = savedCanvas.getContext('2d')
|
||||||
|
// 确保清空整个画布
|
||||||
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
ctx.clearRect(0, 0, savedCanvas.width, savedCanvas.height)
|
||||||
|
|
||||||
// 绘制已保存的线(优先显示已保存的线,如果没有则显示当前线,最后显示初始线)
|
|
||||||
const lineToDraw = this.savedLine || this.line || this.initialLine
|
const lineToDraw = this.savedLine || this.line || this.initialLine
|
||||||
if (lineToDraw) {
|
if (lineToDraw) {
|
||||||
ctx.strokeStyle = '#ff0000'
|
ctx.strokeStyle = '#ff0000'
|
||||||
|
|
@ -436,49 +490,30 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 视频加载完成 */
|
|
||||||
handleVideoLoaded() {
|
handleVideoLoaded() {
|
||||||
console.log('视频加载完成')
|
console.log('视频加载完成')
|
||||||
// 可以在这里获取视频尺寸并调整画布大小
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.initCanvas()
|
this.initCanvas()
|
||||||
// 如果有已保存的线或初始线,重新绘制
|
|
||||||
if (this.savedLine || this.line || this.initialLine) {
|
|
||||||
this.redrawCanvas()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 视频加载错误 */
|
|
||||||
handleVideoError(e) {
|
handleVideoError(e) {
|
||||||
// 如果视频URL为空,不显示错误(因为还没有加载视频)
|
if (!this.videoUrl) return
|
||||||
if (!this.videoUrl) {
|
if (this.videoErrorShown) return
|
||||||
return
|
|
||||||
}
|
|
||||||
// 避免重复显示错误提示
|
|
||||||
if (this.videoErrorShown) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error('视频加载失败:', e)
|
console.error('视频加载失败:', e)
|
||||||
this.videoErrorShown = true
|
this.videoErrorShown = true
|
||||||
this.$message.error('视频加载失败,请检查视频源地址')
|
this.$message.error('视频加载失败,请检查视频源地址')
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 加载视频流 */
|
|
||||||
async loadVideoStream() {
|
async loadVideoStream() {
|
||||||
try {
|
try {
|
||||||
const res = await getVideoStreamAPI()
|
const res = await getVideoStreamAPI()
|
||||||
if (res.code === 200 && res.data && res.data.streamUrl) {
|
if (res.code === 200 && res.data && res.data.streamUrl) {
|
||||||
// 重置错误状态
|
|
||||||
this.videoErrorShown = false
|
this.videoErrorShown = false
|
||||||
this.videoUrl = res.data.streamUrl
|
this.videoUrl = res.data.streamUrl
|
||||||
} else {
|
|
||||||
// 如果没有视频流地址,不显示错误(可能是正常的)
|
|
||||||
console.log('未获取到视频流地址')
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取视频流失败:', error)
|
console.error('获取视频流失败:', error)
|
||||||
// 接口调用失败时不显示错误,因为可能是正常的(没有配置视频流)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -486,6 +521,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
/* 保持原有样式不变,仅为 video-container 增加全屏时的样式处理 */
|
||||||
.video-monitor-container {
|
.video-monitor-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 84px);
|
height: calc(100vh - 84px);
|
||||||
|
|
@ -581,57 +617,6 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -666,11 +651,25 @@ export default {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
/* 关键:确保全屏时背景为黑色 */
|
||||||
|
background-color: #000;
|
||||||
|
|
||||||
|
/* 兼容浏览器的全屏伪类 */
|
||||||
|
&:fullscreen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:-webkit-full-screen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
/* 保持宽高比,或者使用 fill 铺满 */
|
||||||
object-fit: fill;
|
object-fit: fill;
|
||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
@ -723,74 +722,6 @@ export default {
|
||||||
z-index: 1.5;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.video-monitor-container {
|
.video-monitor-container {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
@ -815,4 +746,19 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 隐藏 Chrome, Edge, Safari 的全屏按钮 */
|
||||||
|
video::-webkit-media-controls-fullscreen-button {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 隐藏部分旧版浏览器的全屏功能(如果存在) */
|
||||||
|
video::-webkit-media-controls-enclosure {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 针对特定移动端(如某些安卓浏览器) */
|
||||||
|
video::-internal-media-controls-download-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Loading…
Reference in New Issue