类型:开发

描述:
This commit is contained in:
2025-11-13 21:18:45 +08:00
parent a23660efd6
commit d1411314a5
18 changed files with 1724 additions and 1100 deletions

Binary file not shown.

Binary file not shown.

22
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"hls.js": "^1.5.18", "hls.js": "^1.5.18",
"jssip": "^3.10.1", "jssip": "^3.10.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mpegts.js": "^1.8.0",
"pinia": "^2.2.6", "pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"pubsub-js": "^1.9.5", "pubsub-js": "^1.9.5",
@@ -3201,6 +3202,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
@@ -4272,6 +4279,16 @@
"ufo": "^1.5.4" "ufo": "^1.5.4"
} }
}, },
"node_modules/mpegts.js": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/mpegts.js/-/mpegts.js-1.8.0.tgz",
"integrity": "sha512-ZtujqtmTjWgcDDkoOnLvrOKUTO/MKgLHM432zGDI8oPaJ0S+ebPxg1nEpDpLw6I7KmV/GZgUIrfbWi3qqEircg==",
"license": "Apache-2.0",
"dependencies": {
"es6-promise": "^4.2.5",
"webworkify-webpack": "github:xqq/webworkify-webpack"
}
},
"node_modules/mrmime": { "node_modules/mrmime": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -6143,6 +6160,11 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/webworkify-webpack": {
"version": "2.1.5",
"resolved": "git+ssh://git@github.com/xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef",
"license": "MIT"
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -16,6 +16,7 @@
"hls.js": "^1.5.18", "hls.js": "^1.5.18",
"jssip": "^3.10.1", "jssip": "^3.10.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mpegts.js": "^1.8.0",
"pinia": "^2.2.6", "pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"pubsub-js": "^1.9.5", "pubsub-js": "^1.9.5",

View File

@@ -12,6 +12,7 @@ const instance = axios.create({
timeout: 100000, timeout: 100000,
headers: { headers: {
Authorization: mode == 'dev' ? devToken : proToken, Authorization: mode == 'dev' ? devToken : proToken,
'User-Type': '1',
'Content-Type': 'application/json;charset=UTF-8' 'Content-Type': 'application/json;charset=UTF-8'
} }
}) })

View File

@@ -0,0 +1,352 @@
<template>
<div v-show="isActive" class="myVideo-container">
<video
class="myVideo"
ref="videoElement"
muted
playsinline
:controls="false"
disablePictureInPicture
></video>
<!-- <div v-if="loading" class="loading-overlay pointer-events-none">
<div class="loading-text">{{ loadingText }}</div>
</div> -->
</div>
</template>
<script>
import Hls from 'hls.js'
export default {
name: 'HlsPlayer',
props: {
url: {
type: String,
default: ''
},
isActive: {
type: Boolean,
default: true
},
width: {
type: [String, Number],
default: '100%'
},
height: {
type: [String, Number],
default: '100%'
}
},
data() {
return {
hls: null,
video: null,
loading: false,
loadingText: '加载中...',
retryCount: 0,
maxRetries: 3,
isReady: false,
playAttempts: 0,
maxPlayAttempts: 3,
cleanupTimeout: null
}
},
computed: {
videoStyle() {
const style = {}
if (this.width) {
style.width = typeof this.width === 'number' ? `${this.width}px` : this.width
}
if (this.height) {
style.height = typeof this.height === 'number' ? `${this.height}px` : this.height
}
return style
}
},
watch: {
isActive: {
handler(newValue) {
if (newValue) {
this.$nextTick(this.initializePlayer)
} else {
this.immediateCleanup()
}
},
immediate: true
},
url(newUrl) {
// console.log(newUrl,'77777777777777777777777777777777')
if (newUrl && this.isActive) {
this.initializePlayer()
}
}
},
mounted() {
this.video = this.$refs.videoElement
this.registerVideoEvents()
},
methods: {
registerVideoEvents() {
const events = ['canplay', 'waiting', 'playing', 'error', 'stalled', 'suspend']
events.forEach((event) => {
this.video.addEventListener(event, this.handleVideoEvent)
})
},
handleVideoEvent(event) {
switch (event.type) {
case 'canplay':
this.isReady = true
break
case 'waiting':
this.showLoadingIndicator('缓冲中...')
break
case 'playing':
this.loading = false
break
case 'error':
this.handlePlaybackError()
break
case 'stalled':
this.showLoadingIndicator('播放卡顿...')
break
case 'suspend':
this.cleanupNetworkResources()
break
}
},
immediateCleanup() {
// 立即停止网络请求
if (this.hls) {
this.hls.stopLoad()
this.hls.detachMedia()
}
// 延迟完全清理以避免播放卡顿
this.cleanupTimeout = setTimeout(() => {
// 仅在不重新初始化播放器时才调用 fullCleanup
if (!this.url || !this.isActive) {
this.fullCleanup()
}
}, 1000)
},
fullCleanup() {
if (this.hls) {
this.hls.destroy()
this.hls = null
}
if (this.video) {
this.video.pause()
this.video.removeAttribute('src')
this.video.load()
}
this.loading = false
this.retryCount = 0
this.isReady = false
this.playAttempts = 0
if (this.cleanupTimeout) {
clearTimeout(this.cleanupTimeout)
}
},
initializePlayer() {
if (!this.isActive || !this.url) return
// 如果是重新初始化播放器,先清理已存在的资源
if (this.hls) {
this.immediateCleanup()
}
this.hls = new Hls({
// 内存优化配置
maxBufferSize: 0, // 降低缓冲区大小15MB
maxBufferLength: 0.1, // 更小的缓冲窗口
liveSyncDuration: 1, // 紧跟直播点
liveMaxLatencyDuration: 5, // 最大延迟5秒
liveDurationInfinity: true,
lowLatencyMode: true, // 启用低延迟模式
maxMaxBufferLength: 60,
backBufferLength: 1, // 减少保留的缓冲数据
manifestLoadingTimeOut: 10000,
manifestLoadingMaxRetry: 5,
fragLoadingTimeOut: 5000,
fragLoadingMaxRetry: 2,
enableWorker: true, // 启用Web Worker
recycleVideoFrames: true, // 启用帧回收
startLevel: -1,
autoStartLoad: true,
maxBufferHole: 2, // 允许更大的时间缺口
highBufferWatchdogPeriod: 4, // 延长监控周期
nudgeMaxRetry: 5, // 增加微调重试次数
nudgeOffset: 0.05, // 微调步长(秒)
jumpGaps: false,
stallThreshold: 1000
})
this.hls.attachMedia(this.video)
this.hls.loadSource(this.url)
// 事件处理
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.loading = false
this.retryCount = 0
this.safePlay()
})
this.hls.on(Hls.Events.ERROR, (event, data) => {
console.log('核心视频错误',data.type)
// this.hls.startLoad(); //重连
if (data.type === Hls.ErrorTypes.BUFFER_STALLED_ERROR) {
console.error('缓冲停滞错误,尝试重新加载视频');
this.hls.startLoad(); // 尝试重新加载视频
}
this.handleHlsError(data)
})
this.hls.on(Hls.Events.BUFFER_EOS, () => {
this.cleanupNetworkResources()
})
},
initVideo() {
this.beforeDestroy()
this.hls = new Hls({
maxBufferLength: 30, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
this.hls.attachMedia(this.video)
this.hls.loadSource(this.url)
// 事件处理
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
this.loading = false
this.retryCount = 0
this.safePlay()
})
this.hls.on(Hls.Events.ERROR, (event, data) => {
this.hls.startLoad(); //重连
this.handleHlsError(data)
})
this.hls.on(Hls.Events.BUFFER_EOS, () => {
this.cleanupNetworkResources()
})
},
cleanupNetworkResources() {
// 清理已播放的缓冲数据
if (this.hls && this.video) {
try {
const currentTime = this.video.currentTime
this.hls.mediaBuffer = null
if (this.hls.bufferTimer) {
clearInterval(this.hls.bufferTimer)
}
this.hls.flushBuffer()
this.video.currentTime = currentTime
} catch (e) {
console.warn('Buffer cleanup error:', e)
}
}
},
handleHlsError(data) {
// console.error('HLS Error:', data)
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
this.retryLoad()
break
case Hls.ErrorTypes.MEDIA_ERROR:
this.hls.recoverMediaError()
break
default:
this.fullCleanup()
break
}
}
},
retryLoad() {
if (this.retryCount < this.maxRetries) {
this.retryCount++
this.showLoadingIndicator(`重试第 ${this.retryCount} 次...`)
setTimeout(() => this.initializePlayer(), 2000)
} else {
this.showLoadingIndicator('视频加载失败')
this.fullCleanup()
}
},
showLoadingIndicator(text) {
this.loading = true
this.loadingText = text
},
async safePlay() {
if (!this.video || this.playAttempts >= this.maxPlayAttempts) return
try {
this.playAttempts++
if (this.video.readyState >= 2) {
await this.video.play()
this.playAttempts = 0
} else {
setTimeout(this.safePlay, 500)
}
} catch (error) {
console.warn('播放失败:', error)
setTimeout(this.safePlay, 1000)
}
}
},
beforeDestroy() {
// 移除所有事件监听
if (this.video) {
const events = ['canplay', 'waiting', 'playing', 'error', 'stalled', 'suspend']
events.forEach((event) => {
this.video.removeEventListener(event, this.handleVideoEvent)
})
}
this.fullCleanup()
}
}
</script>
<style scoped lang="scss">
.myVideo-container {
position: relative;
height: 100%;
}
.myVideo {
width: 100%;
height: 100%;
// aspect-ratio: 16/9;
/* border: 1px solid #ccc; */
// border-radius: vw(5);
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.loading-text {
color: white;
padding: vw(10);
border-radius: vw(4);
background: rgba(0, 0, 0, 0.6);
}
</style>

View File

@@ -2,6 +2,7 @@
<div v-show="isActive" class="myVideo-container"> <div v-show="isActive" class="myVideo-container">
<video <video
class="myVideo" class="myVideo"
id="video"
ref="videoElement" ref="videoElement"
muted muted
playsinline playsinline
@@ -16,7 +17,7 @@
<script> <script>
import Hls from 'hls.js' import Hls from 'hls.js'
import mpegtsjs from 'mpegts.js'
export default { export default {
name: 'HlsPlayer', name: 'HlsPlayer',
props: { props: {
@@ -162,6 +163,27 @@
if (this.hls) { if (this.hls) {
this.immediateCleanup() this.immediateCleanup()
} }
if(this.url.startsWith('ws')){
const videoElement = document.getElementById('video')
const player = mpegtsjs.createPlayer({
url: this.url,
type: 'flv',
isLive: true,
hasAudio: false
})
player.attachMediaElement(videoElement)
player.load()
player.play()
// 错误处理和重连机制
player.on('error', (err) => {
// 3 秒后尝试重新加载
setTimeout(() => {
player.load()
player.play()
}, 3000)
})
}else{
this.hls = new Hls({ this.hls = new Hls({
// 内存优化配置 // 内存优化配置
maxBufferSize: 0, // 降低缓冲区大小15MB maxBufferSize: 0, // 降低缓冲区大小15MB
@@ -211,6 +233,8 @@
this.hls.on(Hls.Events.BUFFER_EOS, () => { this.hls.on(Hls.Events.BUFFER_EOS, () => {
this.cleanupNetworkResources() this.cleanupNetworkResources()
}) })
}
}, },
initVideo() { initVideo() {
this.beforeDestroy() this.beforeDestroy()

View File

@@ -59,7 +59,6 @@
watch( watch(
() => props.dataList, () => props.dataList,
(newVal) => { (newVal) => {
aIndex+=1 aIndex+=1
if(aIndex>=3&&!newVal.length){ if(aIndex>=3&&!newVal.length){
@@ -67,22 +66,8 @@
} }
if (newVal.length > 0) { if (newVal.length > 0) {
console.log(colorList.value,'colorList')
condShow.value = 2
nextTick(() => { nextTick(() => {
init() init()
// defaultCofig.legend.formatter = (name) => {
// let percent = props.dataList.find((item) => item.name == name).value
// return name + '\u3000' + `${percent}%`
// }
// defaultCofig.series[0].data = props.dataList
// defaultCofig.series[0].label.formatter = () => {
// return `{value|${props.total}}` + '\n' + `{name|${props.label} }`
// }
// setOption({
// ...defaultCofig,
// ...props.config
// })
}) })
}else{ }else{
@@ -93,6 +78,10 @@
{ immediate: true } { immediate: true }
) )
const init = ()=>{ const init = ()=>{
if(condShow.value===2){
return;
}
condShow.value = 2
props.dataList.forEach((item,index)=>{ props.dataList.forEach((item,index)=>{
if(item.name=='负面'){ if(item.name=='负面'){
@@ -167,10 +156,36 @@
defaultCofig.series[0].label.formatter = () => { defaultCofig.series[0].label.formatter = () => {
return `{value|${props.total}}` + '\n' + `{name|${props.label} }` return `{value|${props.total}}` + '\n' + `{name|${props.label} }`
} }
const chart = setOption({
...defaultCofig,
...props.config
})
chart.on('legendselectchanged', function (e) {
console.log(e,'e')
var echartsArr = [];
for (let key in e.selected) {
if (e.selected[key]) {
echartsArr.push(key)
}
}
var echartsNum = 0;
props.dataList.forEach(item => {
if(echartsArr.includes(item.name)){
echartsNum += parseFloat(item.value)
}
})
defaultCofig.series[0].label.formatter = `{value|${parseInt(echartsNum/100*props.total)}}` + '\n' + `{name|${props.label}}`;
console.log(111111)
console.log({
...defaultCofig,
...props.config
})
console.log(222222)
setOption({ setOption({
...defaultCofig, ...defaultCofig,
...props.config ...props.config
}) })
});
} }
</script> </script>

View File

@@ -49,8 +49,8 @@
</div> </div>
<div class="action-item"> <div class="action-item">
<div class="video-follow" @click="handleCollectAdd(cameraIndexCode, isDiy, index)" v-if="isDiy==0">收藏</div> <div class="video-follow" @click="handleCollectAdd()" v-if="isDayCurr==0">收藏</div>
<div class="video-follow" @click="handleCollectAdd(cameraIndexCode, isDiy, index)" v-else="isDiy==1">取消收藏</div> <div class="video-follow" @click="handleCollectAdd()" v-if="isDayCurr==1">取消收藏</div>
</div> </div>
</div> </div>
@@ -103,8 +103,8 @@
watch( watch(
() =>modelValue.value, () =>modelValue.value,
(val) => { (val) => {
// colletCond.value = props.isCollect colletCond.value = props.isCollect
// isDayCurr.value = props.isDiy isDayCurr.value = props.isDiy
console.log(props.isDiy,'val[0].value') console.log(props.isDiy,'val[0].value')
}, },
@@ -114,7 +114,7 @@
) )
const emit = defineEmits(['isDiyChange']); const emit = defineEmits(['isDiyChange']);
// 收藏 // 收藏
const handleCollectAdd = async (id, status, index) => { const handleCollectAdd = async () => {
await getColletDiyApi({ await getColletDiyApi({
cameraIndexCode:props.cameraIndexCode, cameraIndexCode:props.cameraIndexCode,
isDiy: props.isDiy == 0 ? 1 : 0 isDiy: props.isDiy == 0 ? 1 : 0

View File

@@ -13,9 +13,9 @@ export function useEchart() {
const setOption = (params, update = false) => { const setOption = (params, update = false) => {
initChart() initChart()
chart.setOption(params, update) chart.setOption(params, update)
return chart;
} }
const clearOption = () => { const clearOption = () => {
console.log('clearooooooooooooooooo')
// 将series设置为空数组可以清空图表内容 // 将series设置为空数组可以清空图表内容
chart.setOption({ chart.setOption({
series:[] series:[]

View File

@@ -10,25 +10,14 @@
:key="index" :key="index"
@click="handleItem(item)" @click="handleItem(item)"
> >
<HlsPlayer :url="item.hlsUrl" />
<div class="item-unfollow" @click.stop="handleUnfollow(item.cameraIndexCode, index)">取消关注</div>
<!-- <div>
<p class="item-title--primary">
{{ item.cameraName || item.cameraIndexCode }}
</p>
<video <video
class="item-img" class="item-img"
:id="'video' + index" :id="'video' + index"
muted muted
autoplay autoplay
:controls="false" :controls="false"
:src="item.hlsUrl" ></video>
controlsList="nodownload" <div class="item-unfollow" @click.stop="handleUnfollow(item.cameraIndexCode, index)">取消关注</div>
>
<source src="" type="application/x-mpegURL" />
</video>
</div> -->
</li> </li>
</ul> </ul>
</div> </div>
@@ -50,6 +39,7 @@
import { useWebSocket } from '@/hooks/socket' import { useWebSocket } from '@/hooks/socket'
import { mode, socketBaseUrl, proSocketBaseUrl } from '@/utils/config' import { mode, socketBaseUrl, proSocketBaseUrl } from '@/utils/config'
import mpegtsjs from "mpegts.js";
const { dataRes } = useWebSocket( const { dataRes } = useWebSocket(
`${mode == 'dev' ? socketBaseUrl : proSocketBaseUrl}/ws/securityAlerts` `${mode == 'dev' ? socketBaseUrl : proSocketBaseUrl}/ws/securityAlerts`
@@ -73,7 +63,6 @@ let isCollect = ref(0)
let timer = null let timer = null
let isDiy = ref(0) let isDiy = ref(0)
const handleItem = (item) => { const handleItem = (item) => {
console.log(item,'1111111111111111111111111')
src.value = item.hlsUrl src.value = item.hlsUrl
cameraIndexCode.value = item.cameraIndexCode cameraIndexCode.value = item.cameraIndexCode
isCollect.value = item.isCollect isCollect.value = item.isCollect
@@ -82,12 +71,12 @@ let isCollect = ref(0)
} }
const postVideoRemain = () => { const postVideoRemain = () => {
timer = setInterval(() => { // timer = setInterval(() => {
if(!list.value.length) return false; // if(!list.value.length) return false;
postVideoRemainApi({ // postVideoRemainApi({
cameraIndexCode: list.value.map((item) => item.cameraIndexCode) // cameraIndexCode: list.value.map((item) => item.cameraIndexCode)
}) // })
}, 1500) // }, 1500)
} }
const getPreviewUrl = async (code) => { const getPreviewUrl = async (code) => {
@@ -104,28 +93,32 @@ let isCollect = ref(0)
pageSize: 5 pageSize: 5
}) })
list.value = res.data list.value = res.data
postVideoRemain()
if (timer) clearInterval(timer) if (timer) clearInterval(timer)
// console.log(list.value,'list.valuelist.valuelist.valuelist.value') nextTick(() => {
// nextTick(() => { list.value.forEach(async (item, index) => {
// list.value.forEach(async (item, index) => { var videoElement = document.getElementById(`video${index}`)
// var video = document.getElementById(`video${index}`) const player = mpegtsjs.createPlayer({
// const hls = new Hls({ url: item.hlsUrl,
// enableWorker: false, // 禁用 Worker 来避免额外的线程 type: 'flv',
// enableSoftwareAES: true, // 使用软件解码器以避免硬件解码的额外请求 isLive: true,
// cache: true, // 启用缓存 hasAudio: false
// maxBufferLength: 10, // 最大缓冲长度(秒) })
// maxMaxBufferLength: 15, // 缓冲区长度的上限 player.attachMediaElement(videoElement)
// maxBufferSize: 20 * 1000 * 1000 // 最大缓冲大小(字节) player.load()
// }) player.play()
// hls.loadSource(item.hlsUrl)
// hls.attachMedia(video) // 错误处理和重连机制
// hls.on(Hls.Events.MANIFEST_PARSED, () => { player.on('error', (err) => {
// video.play() console.error('播放器错误:', err)
// }) // 3 秒后尝试重新加载
// hlsRefs.push(hls) setTimeout(() => {
// }) player.load()
// }) player.play()
}, 3000)
})
hlsRefs.push(player)
})
})
} }
watch( watch(
() => list.value, () => list.value,

View File

@@ -16,7 +16,7 @@ export const useScenicStore = defineStore('scenic', () => {
infoList: [ infoList: [
{ name: '游玩舒适度', type: 0, value: '空闲' }, { name: '游玩舒适度', type: 0, value: '空闲' },
// { name: '景区安全', type: 0, value: '安全' }, // { name: '景区安全', type: 0, value: '安全' },
{ name: '通景交通', type: 0, value: '通畅' }, { name: '交通拥堵度', type: 0, value: '通畅' },
{ name: '停车场负荷', type: 0, value: '空闲' } { name: '停车场负荷', type: 0, value: '空闲' }
] ]
}) })

View File

@@ -93,6 +93,7 @@
import Hls from 'hls.js' import Hls from 'hls.js'
import emptyIco from '@/assets/images/n-icon.png' import emptyIco from '@/assets/images/n-icon.png'
import { debounce } from 'lodash' import { debounce } from 'lodash'
import mpegtsjs from "mpegts.js";
const Z00M_IN = 'ZOOM_IN' // 焦距变大 const Z00M_IN = 'ZOOM_IN' // 焦距变大
const Z00M_OUT = 'ZOOM_OUT' // 焦距变小 const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
const UP = 'UP' // 上转 const UP = 'UP' // 上转
@@ -287,6 +288,26 @@
it.videos.forEach((item,index)=>{ it.videos.forEach((item,index)=>{
setTimeout(() => { setTimeout(() => {
const video = document.getElementById(`monitorVideo${item.cameraIndexCode}`) const video = document.getElementById(`monitorVideo${item.cameraIndexCode}`)
if(item.hlsUrl.startsWith('ws')){
const player = mpegtsjs.createPlayer({
url: item.hlsUrl,
type: 'flv',
isLive: true,
hasAudio: false
})
player.attachMediaElement(video)
player.load()
player.play()
// 错误处理和重连机制
player.on('error', (err) => {
// 3 秒后尝试重新加载
setTimeout(() => {
player.load()
player.play()
}, 3000)
})
}else{
if(item.hlsUrl){ if(item.hlsUrl){
const hls = new Hls({ const hls = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒) maxBufferLength: 10, // 最大缓冲长度(秒)
@@ -300,6 +321,8 @@
}) })
hlsRefs.push(hls) hlsRefs.push(hls)
} }
}
}, 1000) }, 1000)
}) })
@@ -318,18 +341,18 @@
) )
// 更新视频 // 更新视频
const postVideoRemain = async () => { const postVideoRemain = async () => {
timer = setInterval(() => { // timer = setInterval(() => {
clearInterval(timer) // clearInterval(timer)
videoList.value.forEach((items,index)=>{ // videoList.value.forEach((items,index)=>{
setTimeout(()=>{ // setTimeout(()=>{
postVideoRemainApi({ // postVideoRemainApi({
cameraIndexCode: items.videos.map((item) => item.cameraIndexCode) // cameraIndexCode: items.videos.map((item) => item.cameraIndexCode)
}) // })
},1500) // },1500)
//
}) // })
//
}, 1500) // }, 1500)
} }
const getVideoRegions = async () => { const getVideoRegions = async () => {
let res = await getVideoRegionsApi({ let res = await getVideoRegionsApi({

View File

@@ -138,7 +138,23 @@
} else { } else {
params.series[0].data = getSeriesData() params.series[0].data = getSeriesData()
} }
setOption(params) const chart = setOption(params)
chart.on('legendselectchanged', function (e) {
var echartsArr = [];
for (let key in e.selected) {
if (e.selected[key]) {
echartsArr.push(key)
}
}
var echartsNum = 0;
props.list.forEach(item => {
if(echartsArr.includes(item.name)){
echartsNum += parseInt(item.count)
}
})
params.series[0].label.formatter = `{label|拥堵次数}` + '\n' + `{value|${echartsNum}}`;
setOption(params);
});
} }
onMounted(() => {}) onMounted(() => {})

View File

@@ -129,8 +129,10 @@
</video> </video>
<div class="action-box"> <div class="action-box">
<div class="action-item"> <div class="action-item">
<span class="item-sc" @click="handleCollect(thisVideo.cameraIndexCode, thisVideo.isCollect, index)" v-if="thisVideo.isCollect==0">关注</span> <span class="item-sc" @click="handleCollect(thisVideo.cameraIndexCode, thisVideo.isCollect, index)"
<span class="item-sc" @click="handleCollect(thisVideo.cameraIndexCode, thisVideo.isCollect, index)" v-else="thisVideo.isCollect==1">取消关注</span> v-if="thisVideo.isCollect==0">关注</span>
<span class="item-sc" @click="handleCollect(thisVideo.cameraIndexCode, thisVideo.isCollect, index)"
v-else="thisVideo.isCollect==1">取消关注</span>
</div> </div>
<div class="action-item"> <div class="action-item">
<img src="@/assets/images/plus.png" title="焦距变大" @click="handleAction(Z00M_IN)"/> <img src="@/assets/images/plus.png" title="焦距变大" @click="handleAction(Z00M_IN)"/>
@@ -150,8 +152,10 @@
<img src="@/assets/images/right.png" title="右转" @click="handleAction(RIGHT)"/> <img src="@/assets/images/right.png" title="右转" @click="handleAction(RIGHT)"/>
</div> </div>
<div class="action-item"> <div class="action-item">
<span class="item-sc" @click="handleCollectAdd(thisVideo.cameraIndexCode, thisVideo.isDiy, index)" v-if="thisVideo.isDiy==0">收藏</span> <span class="item-sc" @click="handleCollectAdd(thisVideo.cameraIndexCode, thisVideo.isDiy, index)"
<span class="item-sc" @click="handleCollectAdd(thisVideo.cameraIndexCode, thisVideo.isDiy, index)" v-else="thisVideo.isDiy==1">取消收藏</span> v-if="thisVideo.isDiy==0">收藏</span>
<span class="item-sc" @click="handleCollectAdd(thisVideo.cameraIndexCode, thisVideo.isDiy, index)"
v-else="thisVideo.isDiy==1">取消收藏</span>
</div> </div>
</div> </div>
</div> </div>
@@ -195,6 +199,7 @@
<script setup> <script setup>
import {getVideoListApi, getPreviewUrlApi, getColletListApi, getColletDiyListApi, getColletDiyApi} from '@/api/home' import {getVideoListApi, getPreviewUrlApi, getColletListApi, getColletDiyListApi, getColletDiyApi} from '@/api/home'
import mpegtsjs from 'mpegts.js'
import { import {
getVideoTypeApi, getVideoTypeApi,
getVideoRegionsApi, getVideoRegionsApi,
@@ -206,6 +211,7 @@
import {debounce} from 'lodash' import {debounce} from 'lodash'
import pubSub from 'pubsub-js' import pubSub from 'pubsub-js'
import Hls from 'hls.js' import Hls from 'hls.js'
import HlsPlayer from "@/components/HlsPlayer/index.vue";
const Z00M_IN = 'ZOOM_IN' // 焦距变大 const Z00M_IN = 'ZOOM_IN' // 焦距变大
const Z00M_OUT = 'ZOOM_OUT' // 焦距变小 const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
@@ -249,30 +255,75 @@
let timer = null let timer = null
const postVideoRemain = async () => { const postVideoRemain = async () => {
if(!videoList.value.length) return false; // if (!videoList.value.length) return false;
timer = setInterval(() => { // timer = setInterval(() => {
postVideoRemainApi({ // postVideoRemainApi({
cameraIndexCode: videoList.value.map((item) => item.cameraIndexCode) // cameraIndexCode: videoList.value.map((item) => item.cameraIndexCode)
}) // })
}, 2000) // }, 2000)
} }
const initVideo = () => { const initVideo = () => {
clearHlsRefs() clearHlsRefs()
nextTick(() => { nextTick(() => {
videoList.value.forEach(async (item, index) => { videoList.value.forEach(async (item, index) => {
const video = document.getElementById(`monitorVideo${index}`) const videoElement = document.getElementById(`monitorVideo${index}`)
const hls = new Hls({ if(item.hlsUrl.startsWith("ws")){
const player = mpegtsjs.createPlayer({
url: item.hlsUrl,
type: 'flv',
isLive: true,
hasAudio: false
})
player.attachMediaElement(videoElement)
player.load()
player.play()
// 错误处理和重连机制
player.on('error', (err) => {
console.error('播放器错误:', err)
// 3 秒后尝试重新加载
setTimeout(() => {
player.load()
player.play()
}, 3000)
})
hlsRefs.push(player)
}else{
const player = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒) maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限 maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节) maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
}) })
hls.loadSource(item.hlsUrl) player.loadSource(item.hlsUrl)
hls.attachMedia(video) player.attachMedia(videoElement)
hls.on(Hls.Events.MANIFEST_PARSED, () => { player.on(Hls.Events.MANIFEST_PARSED, () => {
video.play() videoElement.play()
}) })
hlsRefs.push(hls) player.on(Hls.Events.ERROR, (event, data) => {
// console.error('HLS 播放器遇到错误:', data);
// hls.startLoad();
// initVideo()
// 根据错误类型进行处理
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.log('网络错误,尝试重新加载');
player.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log('媒体错误,尝试修复');
player.recoverMediaError();
break;
default:
console.log('无法恢复的错误,销毁播放器');
// hls.destroy();
break;
}
}
})
hlsRefs.push(player)
}
}) })
}) })
} }
@@ -343,6 +394,26 @@
videoLog.value = 2 videoLog.value = 2
cameraIndexCode.value = code cameraIndexCode.value = code
setTimeout(() => { setTimeout(() => {
if(url.startsWith('ws')){
hlsRef = mpegtsjs.createPlayer({
url: url,
type: 'flv',
isLive: true,
hasAudio: false
})
hlsRef.attachMediaElement(videoRef.value)
hlsRef.load()
hlsRef.play()
// 错误处理和重连机制
hlsRef.on('error', (err) => {
console.error('播放器错误:', err)
// 3 秒后尝试重新加载
setTimeout(() => {
hlsRef.load()
hlsRef.play()
}, 3000)
})
}else{
hlsRef = new Hls({ hlsRef = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒) maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限 maxMaxBufferLength: 15, // 缓冲区长度的上限
@@ -353,6 +424,8 @@
hlsRef.on(Hls.Events.MANIFEST_PARSED, () => { hlsRef.on(Hls.Events.MANIFEST_PARSED, () => {
videoRef.value.play() videoRef.value.play()
}) })
}
if (type == 100) initVideo() if (type == 100) initVideo()
}, 1000) }, 1000)
} }
@@ -457,18 +530,41 @@
// postVideoRemain() // postVideoRemain()
nextTick(() => { nextTick(() => {
videoList.value.forEach(async (x, index) => { videoList.value.forEach(async (x, index) => {
const video = document.getElementById(`monitorVideo${index}`) const videoElement = document.getElementById(`monitorVideo${index}`)
const hls = new Hls({ if(x.hlsUrl.startsWith('ws')){
const player = mpegtsjs.createPlayer({
url: x.hlsUrl,
type: 'flv',
isLive: true,
hasAudio: false
})
player.attachMediaElement(videoElement)
player.load()
player.play()
// 错误处理和重连机制
player.on('error', (err) => {
console.error('播放器错误:', err)
// 3 秒后尝试重新加载
setTimeout(() => {
player.load()
player.play()
}, 3000)
})
hlsRefs.push(player)
}else{
const player = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒) maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限 maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节) maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
}) })
hls.loadSource(x.hlsUrl) player.loadSource(x.hlsUrl)
hls.attachMedia(video) player.attachMedia(videoElement)
hls.on(Hls.Events.MANIFEST_PARSED, () => { player.on(Hls.Events.MANIFEST_PARSED, () => {
video.play() videoElement.play()
}) })
hls.on(Hls.Events.ERROR, (event, data) => { player.on(Hls.Events.ERROR, (event, data) => {
// console.error('HLS 播放器遇到错误:', data); // console.error('HLS 播放器遇到错误:', data);
// hls.startLoad(); // hls.startLoad();
// initVideo() // initVideo()
@@ -477,22 +573,21 @@
switch (data.type) { switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR: case Hls.ErrorTypes.NETWORK_ERROR:
console.log('网络错误,尝试重新加载'); console.log('网络错误,尝试重新加载');
hls.startLoad(); player.startLoad();
break; break;
case Hls.ErrorTypes.MEDIA_ERROR: case Hls.ErrorTypes.MEDIA_ERROR:
console.log('媒体错误,尝试修复'); console.log('媒体错误,尝试修复');
hls.recoverMediaError(); player.recoverMediaError();
break; break;
default: default:
console.log('无法恢复的错误,销毁播放器'); console.log('无法恢复的错误,销毁播放器');
// hls.destroy(); // hls.destroy();
break; break;
} }
} }
}) })
hlsRefs.push(player)
hlsRefs.push(hls) }
}) })
}) })
} else { } else {
@@ -594,11 +689,13 @@
position: relative; position: relative;
// left:vw(-15); // left:vw(-15);
} }
.tree-item__child-item { .tree-item__child-item {
.new-title:last-child { .new-title:last-child {
margin: 0; margin: 0;
} }
} }
.action { .action {
&-box { &-box {
margin-top: vh(16); margin-top: vh(16);
@@ -607,37 +704,44 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
&-item { &-item {
padding: vw(16); padding: vw(16);
display: flex; display: flex;
align-items: center; align-items: center;
background: #0a4190; background: #0a4190;
border-radius: vw(8); border-radius: vw(8);
> img { > img {
cursor: pointer; cursor: pointer;
width: vw(34); width: vw(34);
height: auto; height: auto;
} }
> span { > span {
margin: 0 vw(16); margin: 0 vw(16);
font-weight: 400; font-weight: 400;
font-size: vw(16); font-size: vw(16);
color: #ffffff; color: #ffffff;
} }
.pause { .pause {
margin: 0 vw(10); margin: 0 vw(10);
} }
.item-sc { .item-sc {
padding: vw(10); padding: vw(10);
} }
} }
} }
//背景色设置为透明 //背景色设置为透明
:deep(.el-input__wrapper) { :deep(.el-input__wrapper) {
background-color: rgba(0, 0, 0, 0); background-color: rgba(0, 0, 0, 0);
border: none; border: none;
box-shadow: none; box-shadow: none;
} }
//输入框颜色 //输入框颜色
:deep(.el-input__inner) { :deep(.el-input__inner) {
background-color: rgba(0, 0, 0, 0) !important; background-color: rgba(0, 0, 0, 0) !important;
@@ -650,17 +754,21 @@
display: flex; display: flex;
flex: 1; flex: 1;
} }
:deep(.el-input__inner) { :deep(.el-input__inner) {
height: vh(36); height: vh(36);
font-size: vw(16); font-size: vw(16);
color: #ffffff; color: #ffffff;
} }
.left-nav { .left-nav {
margin: 0 vw(8); margin: 0 vw(8);
width: vw(250); width: vw(250);
background: linear-gradient(321deg, #0b2f64 0%, #062b57 91%, rgba(5, 40, 79, 0) 100%); background: linear-gradient(321deg, #0b2f64 0%, #062b57 91%, rgba(5, 40, 79, 0) 100%);
.bom-box { .bom-box {
margin-top: vh(20); margin-top: vh(20);
.search-box { .search-box {
border-radius: vw(2); border-radius: vw(2);
height: vh(36); height: vh(36);
@@ -689,15 +797,18 @@
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: vw(4); /* 滚动条的宽度 */ width: vw(4); /* 滚动条的宽度 */
} }
/* 滚动条轨道 */ /* 滚动条轨道 */
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: 'transparent'; /* 轨道的背景色 */ background: 'transparent'; /* 轨道的背景色 */
} }
/* 滚动条滑块 */ /* 滚动条滑块 */
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */ background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */
border-radius: 5px; /* 滑块的圆角 */ border-radius: 5px; /* 滑块的圆角 */
} }
.tree-item { .tree-item {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
@@ -706,20 +817,24 @@
&:nth-child(1) { &:nth-child(1) {
padding-top: 0; padding-top: 0;
} }
&__node { &__node {
position: relative; position: relative;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
} }
&__icon { &__icon {
margin-left: vw(-8); margin-left: vw(-8);
width: vw(16); width: vw(16);
height: auto; height: auto;
} }
&__icon-up { &__icon-up {
@extend .tree-item__icon; @extend .tree-item__icon;
transform: rotate(180deg); transform: rotate(180deg);
} }
&__name { &__name {
padding: 0 vw(5); padding: 0 vw(5);
display: block; display: block;
@@ -730,12 +845,14 @@
font-style: normal; font-style: normal;
text-transform: none; text-transform: none;
} }
&__child { &__child {
position: relative; position: relative;
margin-top: vh(20); margin-top: vh(20);
// margin-left: vw(20); // margin-left: vw(20);
// border-left: vw(2) solid #37d8fc; // border-left: vw(2) solid #37d8fc;
} }
&-top__icon { &-top__icon {
position: absolute; position: absolute;
left: vw(-8); left: vw(-8);
@@ -743,6 +860,7 @@
width: vw(16); width: vw(16);
height: vw(16); height: vw(16);
} }
&-bottom__icon { &-bottom__icon {
position: absolute; position: absolute;
left: vw(-8); left: vw(-8);
@@ -750,6 +868,7 @@
width: vw(16); width: vw(16);
height: vw(16); height: vw(16);
} }
&__child-item { &__child-item {
padding: vh(0) vw(20) vh(20) vw(10); padding: vh(0) vw(20) vh(20) vw(10);
cursor: pointer; cursor: pointer;
@@ -762,6 +881,7 @@
// display: flex; // display: flex;
align-items: flex-start; align-items: flex-start;
padding-right: 0; padding-right: 0;
&:nth-last-of-type(1) { &:nth-last-of-type(1) {
// padding: vh(0) vw(20); // padding: vh(0) vw(20);
// padding-right:0; // padding-right:0;
@@ -771,6 +891,7 @@
} }
} }
} }
.ul { .ul {
font-weight: 400; font-weight: 400;
font-size: vw(18); font-size: vw(18);
@@ -783,15 +904,18 @@
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: vw(4); /* 滚动条的宽度 */ width: vw(4); /* 滚动条的宽度 */
} }
/* 滚动条轨道 */ /* 滚动条轨道 */
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: 'transparent'; /* 轨道的背景色 */ background: 'transparent'; /* 轨道的背景色 */
} }
/* 滚动条滑块 */ /* 滚动条滑块 */
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */ background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */
border-radius: 5px; /* 滑块的圆角 */ border-radius: 5px; /* 滑块的圆角 */
} }
.li { .li {
cursor: pointer; cursor: pointer;
width: vw(250); width: vw(250);
@@ -802,6 +926,7 @@
background: url('/src/assets/images/m-nav-bg-1.png'); background: url('/src/assets/images/m-nav-bg-1.png');
background-size: 100% 100%; background-size: 100% 100%;
} }
.active { .active {
background: url('/src/assets/images/m-nav-bg-2.png'); background: url('/src/assets/images/m-nav-bg-2.png');
background-size: 100% 100%; background-size: 100% 100%;
@@ -818,6 +943,7 @@
box-sizing: border-box; box-sizing: border-box;
background-image: url('/src/assets/images/log-v-bg.png'); background-image: url('/src/assets/images/log-v-bg.png');
background-size: 100% 100%; background-size: 100% 100%;
.pagination { .pagination {
padding: vh(10) vw(30); padding: vh(10) vw(30);
position: absolute; position: absolute;
@@ -825,12 +951,14 @@
bottom: vh(20); bottom: vh(20);
} }
} }
&-list { &-list {
gap: vw(8); gap: vw(8);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-content: flex-start; align-content: flex-start;
} }
&-item { &-item {
position: relative; position: relative;
// width: vw(686); // width: vw(686);
@@ -839,12 +967,14 @@
padding: vh(10) vw(10); padding: vh(10) vw(10);
background-image: url('/src/assets/images/item-primary.png'); background-image: url('/src/assets/images/item-primary.png');
background-size: 100% 100%; background-size: 100% 100%;
&:hover { &:hover {
.video-item__follow { .video-item__follow {
display: block !important; display: block !important;
} }
} }
} }
&-item__follow { &-item__follow {
cursor: pointer; cursor: pointer;
display: none; display: none;
@@ -862,6 +992,7 @@
background-image: url('@/assets/images/unfollow.png'); background-image: url('@/assets/images/unfollow.png');
background-size: 100% 100%; background-size: 100% 100%;
} }
&-item__unfollow { &-item__unfollow {
@extend .video-item__follow; @extend .video-item__follow;
background-image: url('@/assets/images/unfollow.png'); background-image: url('@/assets/images/unfollow.png');
@@ -870,6 +1001,7 @@
&-item__inner { &-item__inner {
position: relative; position: relative;
} }
&-item__title { &-item__title {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -883,24 +1015,29 @@
text-overflow: ellipsis; text-overflow: ellipsis;
z-index: 999; z-index: 999;
} }
&-item__title--error { &-item__title--error {
@extend .video-item__title; @extend .video-item__title;
background-color: rgba(226, 27, 27, 0.72); background-color: rgba(226, 27, 27, 0.72);
} }
&-item__title--primary { &-item__title--primary {
@extend .video-item__title; @extend .video-item__title;
background-color: rgba(4, 30, 69, 0.72); background-color: rgba(4, 30, 69, 0.72);
} }
&-item__video { &-item__video {
width: 100%; width: 100%;
height: vh(366); height: vh(366);
object-fit: cover; object-fit: cover;
} }
&-detail { &-detail {
margin-left: vw(10); margin-left: vw(10);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
&-detail__wrapper { &-detail__wrapper {
position: relative; position: relative;
padding: vh(40) vw(50); padding: vh(40) vw(50);
@@ -909,6 +1046,7 @@
background-image: url('/src/assets/images/one-video-bg.png'); background-image: url('/src/assets/images/one-video-bg.png');
background-size: 100% 100%; background-size: 100% 100%;
} }
&-detail__title { &-detail__title {
position: absolute; position: absolute;
left: vw(50); left: vw(50);
@@ -923,33 +1061,39 @@
justify-content: space-between; justify-content: space-between;
background: rgba(4, 30, 69, 0.5); background: rgba(4, 30, 69, 0.5);
} }
&-detail__video { &-detail__video {
width: 100%; width: 100%;
height: vh(820); height: vh(820);
object-fit: contain; object-fit: contain;
background-color: #000; background-color: #000;
} }
&-right { &-right {
margin-left: vw(8); margin-left: vw(8);
width: vw(440); width: vw(440);
height: vh(956); height: vh(956);
background: #082f5a; background: #082f5a;
.back-box { .back-box {
cursor: pointer; cursor: pointer;
padding-right: vw(20); padding-right: vw(20);
display: flex; display: flex;
align-items: center; align-items: center;
.icon { .icon {
width: vw(30); width: vw(30);
height: auto; height: auto;
margin-right: vw(10); margin-right: vw(10);
} }
& > span { & > span {
font-weight: bold; font-weight: bold;
font-size: vw(20); font-size: vw(20);
color: #ffffff; color: #ffffff;
} }
} }
.list { .list {
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
@@ -982,9 +1126,11 @@
padding: vw(10); padding: vw(10);
background-image: url('@/assets/images/item-primary.png'); background-image: url('@/assets/images/item-primary.png');
background-size: 100% 100%; background-size: 100% 100%;
& > div { & > div {
position: relative; position: relative;
} }
&-title { &-title {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@@ -997,14 +1143,17 @@
text-overflow: ellipsis; text-overflow: ellipsis;
z-index: 999; z-index: 999;
} }
&-title--error { &-title--error {
@extend .item-title; @extend .item-title;
background-color: rgba(226, 27, 27, 0.72); background-color: rgba(226, 27, 27, 0.72);
} }
&-title--primary { &-title--primary {
@extend .item-title; @extend .item-title;
background-color: rgba(4, 30, 69, 0.72); background-color: rgba(4, 30, 69, 0.72);
} }
&-img { &-img {
width: 100%; width: 100%;
height: vh(164); height: vh(164);
@@ -1013,6 +1162,7 @@
} }
} }
} }
.video-live { .video-live {
.video-rt { .video-rt {
width: vw(400); width: vw(400);
@@ -1028,29 +1178,35 @@
border-image: linear-gradient(180deg, rgba(0, 150, 255, 1), rgba(0, 90, 153, 0)) 1 1; border-image: linear-gradient(180deg, rgba(0, 150, 255, 1), rgba(0, 90, 153, 0)) 1 1;
margin-left: vw(10); margin-left: vw(10);
padding: vw(20); padding: vw(20);
.rt-v-box { .rt-v-box {
overflow-y: auto; overflow-y: auto;
/* 滚动条整体样式 */ /* 滚动条整体样式 */
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: vw(4); /* 滚动条的宽度 */ width: vw(4); /* 滚动条的宽度 */
} }
/* 滚动条轨道 */ /* 滚动条轨道 */
&::-webkit-scrollbar-track { &::-webkit-scrollbar-track {
background: 'transparent'; /* 轨道的背景色 */ background: 'transparent'; /* 轨道的背景色 */
} }
/* 滚动条滑块 */ /* 滚动条滑块 */
&::-webkit-scrollbar-thumb { &::-webkit-scrollbar-thumb {
background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */ background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */
border-radius: 5px; /* 滑块的圆角 */ border-radius: 5px; /* 滑块的圆角 */
} }
height: 100%; height: 100%;
} }
.title { .title {
background-image: url('/src/assets/images/nav-l-t-bg.png'); background-image: url('/src/assets/images/nav-l-t-bg.png');
background-size: 100% 100%; background-size: 100% 100%;
margin-bottom: vh(10); margin-bottom: vh(10);
position: relative; position: relative;
left: vw(-20); left: vw(-20);
span { span {
margin-left: vw(30); margin-left: vw(30);
font-weight: 800; font-weight: 800;
@@ -1067,6 +1223,7 @@
color: transparent; color: transparent;
} }
} }
.rt-video { .rt-video {
width: 100%; width: 100%;
height: vh(300); height: vh(300);
@@ -1076,6 +1233,7 @@
box-sizing: border-box; box-sizing: border-box;
margin-bottom: vh(2); margin-bottom: vh(2);
position: relative; position: relative;
.desc { .desc {
position: absolute; position: absolute;
width: 100%; width: 100%;
@@ -1094,9 +1252,11 @@
text-transform: none; text-transform: none;
} }
} }
.v-error-bg { .v-error-bg {
background-image: url('/src/assets/images/v-item-bg-1.png'); background-image: url('/src/assets/images/v-item-bg-1.png');
background-size: 100% 100%; background-size: 100% 100%;
.desc { .desc {
background: rgba(226, 27, 27, 0.5); background: rgba(226, 27, 27, 0.5);
} }

View File

@@ -151,7 +151,23 @@
params.series[0].label.formatter = formatLabel() params.series[0].label.formatter = formatLabel()
params.series[0].data = getSeriesData() params.series[0].data = getSeriesData()
} }
setOption(params) const chart = setOption(params);
chart.on('legendselectchanged', function (e) {
var echartsArr = [];
for (let key in e.selected) {
if (e.selected[key]) {
echartsArr.push(key)
}
}
var echartsNum = 0;
props.list.forEach(item => {
if(echartsArr.includes(item.name)){
echartsNum += parseInt(item.count)
}
})
params.series[0].label.formatter = `{value|${echartsNum}}` + '\n' + `{name|${props.subTitle}}`;
setOption(params);
});
} }
</script> </script>

View File

@@ -531,6 +531,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
height: vh(40); height: vh(40);
cursor: pointer;
&:nth-child(2n + 1) { &:nth-child(2n + 1) {
background: rgba(3, 78, 153, 0.3); background: rgba(3, 78, 153, 0.3);
} }

View File

@@ -29,7 +29,7 @@
<!-- <div class="header-left__camera" @click="videoShow = true">道路监控</div> --> <!-- <div class="header-left__camera" @click="videoShow = true">道路监控</div> -->
<!-- <div class="header-left__point" @click="videoShow = true">3号点位</div> --> <!-- <div class="header-left__point" @click="videoShow = true">3号点位</div> -->
</div> </div>
<div class="header-status">{{ item.congestLevelText }} </div> <div class="header-status" v-if="item.congestLevel>0">{{ item.congestLevelText }} </div>
</div> </div>
<div class="statistics"> <div class="statistics">
<div class="statistics-item"> <div class="statistics-item">