Compare commits

37 Commits

Author SHA1 Message Date
046aea0f55 类型:开发
描述:
2026-02-09 12:26:18 +08:00
3e9f7133de 类型:开发
描述:
2026-02-09 10:23:14 +08:00
affc881aaa 类型:开发
描述:
2026-02-04 23:15:21 +08:00
87c115ab99 类型:开发
描述:
2026-02-04 17:26:47 +08:00
6fe84fe853 类型:开发
描述:
2026-02-03 23:46:55 +08:00
b7499f12d1 类型:开发
描述:
2026-02-03 11:55:41 +08:00
724dc4a561 类型:开发
描述:
2026-02-03 10:14:46 +08:00
duanliang
cee259db65 fix 字体大小 2026-02-02 18:50:36 +08:00
3853793886 类型:开发
描述:
2026-01-28 18:54:50 +08:00
aa75cd9cb9 类型:开发
描述:
2026-01-28 18:22:36 +08:00
duanliang
f09d8c972b fix 优化 2026-01-28 09:56:47 +08:00
be75778018 类型:开发
描述:
2026-01-23 00:06:29 +08:00
72fcf0b531 类型:开发
描述:
2026-01-21 00:23:19 +08:00
406d502e60 Merge remote-tracking branch 'origin/video2' into video2 2026-01-20 20:02:13 +08:00
duanliang
cbedd59743 feat 游客总数弹窗 2026-01-20 15:07:30 +08:00
85a24f2147 类型:开发
描述:
2026-01-14 01:06:04 +08:00
04b25ab162 类型:开发
描述:
2026-01-14 00:25:38 +08:00
402e3c6e09 类型:开发
描述:
2026-01-14 00:24:38 +08:00
c392ce026d 类型:开发
描述:
2025-12-19 20:16:43 +08:00
aa18dc7a4c 类型:开发
描述:
2025-12-11 17:27:40 +08:00
duanliang
467915db82 fix 核心视频 2025-12-11 17:25:42 +08:00
b56a35cad4 类型:开发
描述:
2025-12-11 17:01:09 +08:00
duanliang
937ab3a230 fix pieRow 2025-12-11 14:00:06 +08:00
duanliang
b459464052 fix pieRow 2025-12-11 13:39:08 +08:00
9d134a2b9d 类型:开发
描述:
2025-12-11 11:04:06 +08:00
duanliang
afc316bbab fix scenic-box3 2025-12-11 10:46:30 +08:00
1b4226d325 类型:开发
描述:
2025-12-11 10:20:01 +08:00
20049de695 类型:开发
描述:
2025-12-10 18:23:42 +08:00
095cd64373 类型:开发
描述:
2025-12-10 16:20:11 +08:00
d6c4bd0406 类型:开发
描述:
2025-12-05 22:29:08 +08:00
03e0ea4cb8 类型:开发
描述:
2025-12-04 16:52:58 +08:00
caa474a5a9 类型:开发
描述:
2025-11-26 02:37:32 +08:00
7edb14c031 类型:开发
描述:
2025-11-25 20:48:02 +08:00
b1161d7479 类型:开发
描述:
2025-11-20 01:17:16 +08:00
d1411314a5 类型:开发
描述:
2025-11-13 21:18:45 +08:00
a23660efd6 类型:开发
描述:
2025-09-25 15:57:37 +08:00
90347ba87a 类型:开发
描述:
2025-08-12 14:37:50 +08:00
47 changed files with 3355 additions and 1474 deletions

Binary file not shown.

Binary file not shown.

56
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"hls.js": "^1.5.18",
"jssip": "^3.10.1",
"lodash": "^4.17.21",
"mpegts.js": "^1.8.0",
"pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.2.0",
"pubsub-js": "^1.9.5",
@@ -23,7 +24,8 @@
"vue-echarts": "^7.0.3",
"vue-router": "^4.4.5",
"vue3-seamless-scroll": "^2.0.1",
"vuedraggable": "^4.1.0"
"vuedraggable": "^4.1.0",
"whepts": "^1.1.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
@@ -3201,6 +3203,12 @@
"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": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
@@ -3327,6 +3335,12 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -4272,6 +4286,16 @@
"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": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
@@ -4306,6 +4330,21 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/nanostores": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.1.0.tgz",
"integrity": "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -6143,6 +6182,21 @@
"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/whepts": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/whepts/-/whepts-1.1.12.tgz",
"integrity": "sha512-KllGslriMAhD6BDBbG6X653M8XgeYqxnjuEyaB7U/1nIZN6NoAGSWP3Av46g690r2UaaI69bNzfPn4efJBd+kA==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.4",
"nanostores": "^1.1.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -16,6 +16,7 @@
"hls.js": "^1.5.18",
"jssip": "^3.10.1",
"lodash": "^4.17.21",
"mpegts.js": "^1.8.0",
"pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.2.0",
"pubsub-js": "^1.9.5",
@@ -24,7 +25,8 @@
"vue-echarts": "^7.0.3",
"vue-router": "^4.4.5",
"vue3-seamless-scroll": "^2.0.1",
"vuedraggable": "^4.1.0"
"vuedraggable": "^4.1.0",
"whepts": "^1.1.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",

View File

@@ -50,6 +50,9 @@ export function getRegionsListApi(data){
// 获取视频播放地址
export function getPreviewUrlApi(data) {
if(data.cameraIndexCode==undefined || !data.cameraIndexCode){
return null;
}
return request({
url: '/fjtcc-api/api/largeScreen/video/getPreviewUrl',
method: 'post',
@@ -132,3 +135,11 @@ export function getVideCollectCateSort(data) {
data
})
}
//今年游客总数
export function getSpotVisitor(data) {
return request({
url: '/fjtcc-api/api/largeScreen/spot/spotVisitor',
method: 'get',
data
})
}

View File

@@ -1,14 +1,14 @@
import request from './request'
export function getTrafficEventsApi() {
export function getTrafficEventsApi(type) {
return request({
url: '/fjtcc-api/api/largeScreen/monitor/trafficEvents',
url: '/fjtcc-api/api/largeScreen/monitor/trafficEvents?type='+type,
method: 'get'
})
}
export function getVideoEventApi(scenicSpotId) {
export function getVideoEventApi(scenicSpotId,type) {
return request({
url: '/fjtcc-api/api/largeScreen/monitor/videoEvents?scenicSpotId='+scenicSpotId,
url: '/fjtcc-api/api/largeScreen/monitor/videoEvents?type='+type+'&scenicSpotId='+scenicSpotId,
method: 'get'
})
}

View File

@@ -12,6 +12,7 @@ const instance = axios.create({
timeout: 100000,
headers: {
Authorization: mode == 'dev' ? devToken : proToken,
'User-Type': '1',
'Content-Type': 'application/json;charset=UTF-8'
}
})
@@ -40,10 +41,11 @@ instance.interceptors.response.use(
if (res.data.code == 200) {
return res.data
} else {
ElMessage({
message: res.data.msg,
type: 'error'
})
console.error("接口请求错误:"+res.data.msg)
// ElMessage({
// message: res.data.msg,
// type: 'error'
// })
return Promise.reject(res.data)
}
},

View File

@@ -7,7 +7,13 @@ export function getListApi() {
method: 'get'
})
}
// 统计
export function getDetailApi(id) {
return request({
url: '/fjtcc-api/api/largeScreen/workorder/detail/'+id,
method: 'get'
})
}
// 统计
export function getTotalApi() {
return request({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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,11 +2,12 @@
<div v-show="isActive" class="myVideo-container">
<video
class="myVideo"
id="video"
ref="videoElement"
preload="auto"
muted
playsinline
autoplay
:controls="false"
disablePictureInPicture
></video>
<!-- <div v-if="loading" class="loading-overlay pointer-events-none">
<div class="loading-text">{{ loadingText }}</div>
@@ -16,7 +17,7 @@
<script>
import Hls from 'hls.js'
import WebRTCWhep from 'whepts'
export default {
name: 'HlsPlayer',
props: {
@@ -120,6 +121,7 @@
immediateCleanup() {
// 立即停止网络请求
if (this.hls) {
this.hls.close();
this.hls.stopLoad()
this.hls.detachMedia()
}
@@ -158,59 +160,77 @@
initializePlayer() {
if (!this.isActive || !this.url) return
// 如果是重新初始化播放器,先清理已存在的资源
if (this.hls) {
this.immediateCleanup()
if(this.url.startsWith('http://192.168.77.200:8050/')){
this.hls = new WebRTCWhep({
url: this.url, // WHEP 服务器地址
container: this.video, // 视频播放容器
iceServers: [{ urls: 'turn:192.168.77.200:3478',username: 'ZLMediaKit',credential: 'ZLMediaKit'}]
})
this.hls.on('error', (error) => {
console.error('错误:', error.message, error.type)
if(error.type ==='REQUEST_ERROR' || error.type ==='NOT_FOUND_ERROR'){
this.initializePlayer();
}
})
this.hls.on('play:failed', (err) => {
// this.initializePlayer();
})
}else{
// 如果是重新初始化播放器,先清理已存在的资源
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()
})
}
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()

View File

@@ -93,7 +93,7 @@
},
grid: {
left: '2%',
right: '2%',
right: '5%',
bottom: '5%',
top: '10%',
containLabel: true

View File

@@ -48,10 +48,7 @@
}
})
const { id, setOption,chartVal,dispose ,clearOption} = useEchart()
const { id,chart, setOption,chartVal,dispose ,clearOption} = useEchart()
let condShow = ref(0)
let aIndex = 1
var colorList = []
@@ -61,38 +58,33 @@
(newVal) => {
aIndex+=1
if(aIndex>=3&&!newVal.length){
condShow.value = 1
condShow.value = 1
}
if (newVal.length > 0) {
console.log(colorList.value,'colorList')
condShow.value = 2
nextTick(() => {
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
// })
})
colorList = []
nextTick(() => {
clearOption();
init()
})
}else{
}
},
{ immediate: true }
)
const init = ()=>{
if(condShow.value===2){
return;
}
clearOption()
colorList = [];
condShow.value = 2
const validDataList = props.dataList.filter(item => {
return item && item.name && (item.value || item.value === 0);
});
props.dataList.forEach((item,index)=>{
if(item.name=='负面'){
@@ -121,7 +113,17 @@
textStyle: {
color: '#ffffff',
fontSize: fitChartSize(14)
}
},
formatter: (name) => {
// let percent = props.dataList.find((item) => item.name == name).value
const item = validDataList.find((item) => item.name === name);
const percent = item ? item.value : 0;
const displayName = name || '';
return `${displayName}\u3000${percent}%`;
},
},
series: [
{
@@ -155,22 +157,50 @@
labelLine: {
show: false
},
data: []
data: validDataList
}
]
}
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.series[0].data = props.dataList
// defaultCofig.legend.formatter = (name) => {
// let percent = props.dataList.find((item) => item.name == name).value
// if(name){
// return name + '\u3000' + `${percent}%`
// }else{
// return name + '\u3000' + `${0}%`
// }
// }
// defaultCofig.series[0].label.formatter = () => {
// return `{value|${props.total}}` + '\n' + `{name|${props.label} }`
// }
setOption({},true)
const changeChart = setOption({
...defaultCofig,
...props.config
})
changeChart.off('legendselectchanged');
changeChart.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}}`;
setOption({
...defaultCofig,
...props.config
})
});
}
</script>

View File

@@ -49,8 +49,8 @@
</div>
<div class="action-item">
<div class="video-follow" @click="handleCollectAdd(cameraIndexCode, isDiy, index)" v-if="isDiy==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==0">收藏</div>
<div class="video-follow" @click="handleCollectAdd()" v-if="isDayCurr==1">取消收藏</div>
</div>
</div>
@@ -66,13 +66,20 @@
import { postVideoControlApi,postVideoCollectApi } from '@/api/monitor'
import { getColletDiyApi } from '@/api/home'
import pubSub from 'pubsub-js'
const Z00M_IN = 'ZOOM_IN' // 焦距变大
const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
const UP = 'UP' // 上转
const DOWN = 'DOWN' // 下转
const LEFT = 'LEFT' // 左转
const RIGHT = 'RIGHT' // 右转
const STOP = 'STOP' // 停止操作
// const Z00M_IN = 'ZOOM_IN' // 焦距变大
// const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
// const UP = 'UP' // 上转
// const DOWN = 'DOWN' // 下转
// const LEFT = 'LEFT' // 左转
// const RIGHT = 'RIGHT' // 右转
// const STOP = 'STOP' // 停止操作
const Z00M_IN = 'zoomin' // 焦距变大
const Z00M_OUT = 'zoomout' // 焦距变小
const UP = 'up' // 上转
const DOWN = 'down' // 下转
const LEFT = 'left' // 左转
const RIGHT = 'right' // 右转
const STOP = 'stop' // 停止操作
let ACTION = '0'
let command = ref('')
@@ -103,8 +110,8 @@
watch(
() =>modelValue.value,
(val) => {
// colletCond.value = props.isCollect
// isDayCurr.value = props.isDiy
colletCond.value = props.isCollect
isDayCurr.value = props.isDiy
console.log(props.isDiy,'val[0].value')
},
@@ -114,7 +121,7 @@
)
const emit = defineEmits(['isDiyChange']);
// 收藏
const handleCollectAdd = async (id, status, index) => {
const handleCollectAdd = async () => {
await getColletDiyApi({
cameraIndexCode:props.cameraIndexCode,
isDiy: props.isDiy == 0 ? 1 : 0
@@ -149,6 +156,7 @@
const handleAction = async (e) => {
if (e == STOP) {
ACTION = '1'
command.value = e
} else {
ACTION = '0'
command.value = e

View File

@@ -8,7 +8,7 @@
v-for="(item, index) in navList"
:key="index"
>
{{ item.dictLabel }}
{{ item }}
</div>
</div>
</div>

View File

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

View File

@@ -3,7 +3,7 @@ export function useWebSocket(url) {
let socket = ref(null) // socket对象
let isConnected = ref(false) // 是否连接成功
let dataRes = ref(null) // 存储推送数据
let timer=null;
const connectWebSocket = () => {
socket.value = new WebSocket(url, 'echo-protocol', {
headers: {
@@ -28,6 +28,19 @@ export function useWebSocket(url) {
socket.value.onclose = (event) => {
isConnected.value = false
console.log('WebSocket close',url, event)
if(!timer){
timer =setInterval(() => {
if(!isConnected.value){
console.log('重连中...')
connectWebSocket()
}else if(isConnected.value){
clearInterval(timer)
timer = null;
}
}, 5000);
}
}
}

View File

@@ -12,7 +12,7 @@
@click="handleItem(item)"
>
<div class="item-unfollow" @click.stop="handleCollect(item.cameraIndexCode, index)">取消关注</div>
<video class="item-video" :id="'video' + index" muted autoplay :controls="false">
<video class="item-video" :id="'videoall' + index" muted autoplay :controls="false">
<source type="application/x-mpegURL" />
</video>
<p>
@@ -36,18 +36,20 @@
</template>
<script setup>
import { getVideoListApi,getColletListApi } from '@/api/home'
import {getVideoListApi, getColletListApi, getPreviewUrlApi} from '@/api/home'
import { postVideoCollectApi } from '@/api/monitor'
import primary from '@/assets/images/item-primary.png'
import Hls from 'hls.js'
import pubSub from 'pubsub-js'
import WebRTCWhep from "whepts";
let modelValue = defineModel()
let isCollect = ref(0)
let list = ref([])
let hlsRefs = []
let webrtcRefs = []
let total = ref(0)
let src = ref('')
let cameraIndexCode = ref('')
@@ -75,11 +77,16 @@
getVideoList()
})
}
const handleItem = (item) => {
console.log(item,'iscollect')
src.value = item.hlsUrl
isCollect.value = item.isCollect
isDiy.value = item.isDiy
const handleItem = async (item) => {
console.log(item, 'iscollect')
let res = await getPreviewUrlApi({
cameraIndexCode: item.cameraIndexCode,
type: 'hls',
subStream:0
})
src.value = res.data.url
isCollect.value = item.isCollect
isDiy.value = item.isDiy
cameraIndexCode.value = item.cameraIndexCode
videoShow.value = true
}
@@ -104,33 +111,84 @@
})
hlsRefs = []
}
if (webrtcRefs.length > 0) {
webrtcRefs.map((item) => {
item.close()
})
webrtcRefs = []
}
}
const pageNumChange = () => {
clearHlsRefs()
list.value = []
getVideoList()
}
const createPlayer = (cameraIndexCode,videoElement) => {
getPreviewUrlApi({
type: 'hls',
cameraIndexCode: cameraIndexCode,
subStream:1
}).then(res=>{
const url = res.data.url;
if(url.startsWith('http://192.168.77.200:8050/')){
const player = new WebRTCWhep({
url:url, // WHEP 服务器地址
container: videoElement, // 视频播放容器
iceServers: [{ urls: 'turn:192.168.77.200:3478',username: 'ZLMediaKit',credential: 'ZLMediaKit'}]
})
player.on('error', (error) => {
console.error('错误:', error.message, error.type)
if(error.type ==='REQUEST_ERROR' || error.type ==='NOT_FOUND_ERROR'){
createPlayer(cameraIndexCode,videoElement)
}
})
player.on('play:failed', (err) => {
// createPlayer(cameraIndexCode,videoElement);
})
webrtcRefs.push(player)
}
else{
const player = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
player.loadSource(url)
player.attachMedia(videoElement)
player.on(Hls.Events.MANIFEST_PARSED, () => {
videoElement.play()
})
player.on(Hls.Events.ERROR, (event, data) => {
// 根据错误类型进行处理
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)
}
})
}
const getVideoList = async () => {
let res = await getColletListApi(params)
list.value = res.data
total.value = res.total
nextTick(() => {
list.value.forEach(async (item, index) => {
var video = document.getElementById(`video${index}`)
const hls = new Hls({
enableWorker: false, // 禁用 Worker 来避免额外的线程
enableSoftwareAES: true, // 使用软件解码器以避免硬件解码的额外请求
cache: true, // 启用缓存
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 20 * 1000 * 1000 // 最大缓冲大小(字节)
})
hls.loadSource(item.hlsUrl)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play()
})
hlsRefs.push(hls)
var video = document.getElementById(`videoall${index}`)
createPlayer(item.cameraIndexCode,video);
})
})
}

View File

@@ -10,25 +10,14 @@
:key="index"
@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"
:id="'video' + index"
muted
autoplay
:controls="false"
:src="item.hlsUrl"
controlsList="nodownload"
>
<source src="" type="application/x-mpegURL" />
</video>
</div> -->
></video>
<div class="item-unfollow" @click.stop="handleUnfollow(item.cameraIndexCode, index)">取消关注</div>
</li>
</ul>
</div>
@@ -50,6 +39,7 @@
import { useWebSocket } from '@/hooks/socket'
import { mode, socketBaseUrl, proSocketBaseUrl } from '@/utils/config'
import WebRTCWhep from 'whepts'
const { dataRes } = useWebSocket(
`${mode == 'dev' ? socketBaseUrl : proSocketBaseUrl}/ws/securityAlerts`
@@ -69,63 +59,111 @@ let isCollect = ref(0)
let cameraIndexCode = ref('')
let videoShow = ref(false)
let allShow = ref(false)
let webrtcRefs = []
let hlsRefs = []
let timer = null
let isDiy = ref(0)
const handleItem = (item) => {
console.log(item,'1111111111111111111111111')
src.value = item.hlsUrl
const handleItem = async (item) => {
let res = await getPreviewUrlApi({
cameraIndexCode: item.cameraIndexCode,
type: 'hls',
subStream:0
})
src.value = res.data.url
cameraIndexCode.value = item.cameraIndexCode
isCollect.value = item.isCollect
isDiy.value = item.isDiy
isCollect.value = item.isCollect
isDiy.value = item.isDiy
videoShow.value = true
}
const postVideoRemain = () => {
timer = setInterval(() => {
if(!list.value.length) return false;
postVideoRemainApi({
cameraIndexCode: list.value.map((item) => item.cameraIndexCode)
})
}, 1500)
// timer = setInterval(() => {
// if(!list.value.length) return false;
// postVideoRemainApi({
// cameraIndexCode: list.value.map((item) => item.cameraIndexCode)
// })
// }, 1500)
}
const getPreviewUrl = async (code) => {
let res = await getPreviewUrlApi({
cameraIndexCode: code,
type: 'hls'
type: 'hls',
subStream:1
})
src.value = res.data.url
videoShow.value = true
}
const createPlayer = (cameraIndexCode,videoElement) => {
getPreviewUrlApi({
type: 'hls',
cameraIndexCode: cameraIndexCode,
subStream:0
}).then(res=>{
const url = res.data.url;
if(url.startsWith('http://192.168.77.200:8050/')){
const player = new WebRTCWhep({
url:url, // WHEP 服务器地址
container: videoElement, // 视频播放容器
iceServers: [{ urls: 'turn:192.168.77.200:3478',username: 'ZLMediaKit',credential: 'ZLMediaKit'}]
})
player.on('error', (error) => {
if(error.type ==='REQUEST_ERROR' || error.type ==='NOT_FOUND_ERROR'){
createPlayer(cameraIndexCode,videoElement);
}
})
player.on('play:failed', (err) => {
// createPlayer(cameraIndexCode,videoElement);
})
webrtcRefs.push(player)
}
else{
const player = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
player.loadSource(url)
player.attachMedia(videoElement)
player.on(Hls.Events.MANIFEST_PARSED, () => {
videoElement.play()
})
player.on(Hls.Events.ERROR, (event, data) => {
// 根据错误类型进行处理
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)
}
})
}
const getVideoList = async () => {
let res = await getColletListApi({
pageNum: 1,
pageSize: 5
})
list.value = res.data
postVideoRemain()
if (timer) clearInterval(timer)
// console.log(list.value,'list.valuelist.valuelist.valuelist.value')
// nextTick(() => {
// list.value.forEach(async (item, index) => {
// var video = document.getElementById(`video${index}`)
// const hls = new Hls({
// enableWorker: false, // 禁用 Worker 来避免额外的线程
// enableSoftwareAES: true, // 使用软件解码器以避免硬件解码的额外请求
// cache: true, // 启用缓存
// maxBufferLength: 10, // 最大缓冲长度(秒)
// maxMaxBufferLength: 15, // 缓冲区长度的上限
// maxBufferSize: 20 * 1000 * 1000 // 最大缓冲大小(字节)
// })
// hls.loadSource(item.hlsUrl)
// hls.attachMedia(video)
// hls.on(Hls.Events.MANIFEST_PARSED, () => {
// video.play()
// })
// hlsRefs.push(hls)
// })
// })
nextTick(() => {
list.value.forEach(async (item, index) => {
var videoElement = document.getElementById(`video${index}`)
createPlayer(item.cameraIndexCode,videoElement);
})
})
}
watch(
() => list.value,
@@ -143,12 +181,22 @@ let isCollect = ref(0)
})
hlsRefs = []
}
if(webrtcRefs.length>0){
webrtcRefs.map((item) => {
try{
item.close()
}catch (e) {
}
})
webrtcRefs = [];
}
getVideoList()
}
const onVideoCollect = () => {
pubSub.subscribe('videoCollect', () => {
clearHlsRefs()
// getVideoList()
getVideoList()
})
}

View File

@@ -191,7 +191,6 @@
// 点击导航
const handleNav = (item) => {
console.log(item,router.currentRoute.value.path,'测试一下 ')
if (isSkip.value) {
router.push(item.path)
} else {
@@ -214,7 +213,6 @@
otherLeftLabel.value = '其他酒店'
otherRightLabel.value = '其他场馆'
pubSub.publish('hotelChange', item)
// console.log(item,'hotelChange')
break
}
}
@@ -264,8 +262,7 @@
isSkip.value = false
isBack.value = true
let res = await getSpotListApi()
navLeft.value = res.data
navLeft.value = res.data.slice(0,3)
current.value = res.data[0].id
title.value = navLeft.value[0].name
pubSub.publish('scenicChange', navLeft.value[0])
@@ -309,7 +306,7 @@
scenicSpotId: '',
id:10086,
},
...spotRes.data
...spotRes.data.slice(0,3)
]
if(monitorDefaultData.value){
current.value = monitorDefaultData.value.id
@@ -370,12 +367,11 @@
isBack.value = true
let hotelRes = await getHotelListApi({ hotelStadiumType: 1 })
navLeft.value = hotelRes.data.slice(0, 3)
current.value = navLeft.value[0].id
otherLeftNav.value = hotelRes.data.slice(3, hotelRes.data.length - 1)
current.value = navLeft.value[0].id
otherLeftNav.value = hotelRes.data.slice(3, hotelRes.data.length)
let venueRes = await getHotelListApi({ hotelStadiumType: 2 })
// console.log(venueRes,'venueRes')
navRight.value = venueRes.data.slice(0, 3)
otherRightNav.value = venueRes.data.slice(3, venueRes.data.length - 1)
otherRightNav.value = venueRes.data.slice(3, venueRes.data.length)
pubSub.publish('hotelChange', hotelRes.data[0])
break
}

View File

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

View File

@@ -7,6 +7,6 @@ export const proSocketBaseUrl = 'ws://192.168.77.200:8060'
export const mode = 'pro' // 测试 dev 正式 pro
export const devToken =
'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImE1OWFmNWYwLTU3OWItNDJkNy1hZDJhLTY0Y2JlODA5ZWI1NiJ9.BTxvu6jUWbN0qONWf5K6VzXopE8T8qXzKuX-mij21VJT4U0LdgnqToyqeNDQ2OyJ6cvpdJBzQ9mEEb-dnwrTpQ'
'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImE1OWFmNWYwLTU3OWItNDJkNy1hZDJhLTY0Y2JlODA5ZWI1NiJ9.BTxvu6jUWbN0qONWf5K6VzXopE8T8qXzKuX-mij21VJT4U0LdgnqToyqeNDQ2OyJ6cvpdJBzQ9mEEb-dnwrTpQ11'
export const proToken =
'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImE1OWFmNWYwLTU3OWItNDJkNy1hZDJhLTY0Y2JlODA5ZWI1NiJ9.dSLZekRsYf5ZZDCYqFEOgHTi4GeHD0m10gGHXrbgpc-hD52Zt7Vw05cxhQ-lzY29yf2IxH0oYi28DBfHdtf9SA'

View File

@@ -35,14 +35,14 @@
</div>
<video
class="video-item__video"
:id="'monitorVideo' + element.cameraIndexCode"
:id="'collectmonitorVideo' + element.cameraIndexCode"
preload="auto"
muted
autoplay
:controls="false"
/>
<p class="video-item__title--primary">
{{ element.cameraName || element.scenicAreaId }}
{{ element.cameraName || element.cameraIndexCode }}
</p>
</div>
</div>
@@ -52,29 +52,7 @@
</div>
</div>
<!-- <div class="pagination">
<el-pagination
v-model:current-page="params.pageNum"
:page-size="params.pageSize"
:total="total"
background
layout="prev, pager, next"
@current-change="currentChange"
/>
</div> -->
</div>
<!-- <ul class="videos">
<li class="video-item" v-for="item in 8" :key="item">
<img src="@/assets/images/sxzd.png" alt="" />
<p>
<span>核心路段这是一条信息说明</span>
</p>
</li>
</ul> -->
<!-- <div class="pagination-box">
<el-pagination background layout="prev, pager, next" :total="1000" />
</div> -->
</div>
<VideoDialog v-model="show" :cameraIndexCode="cameraIndexCode" @isDiyChange="isDiyChange" :isDiy="isDiy" :isCollect="isCollect" :src="videoSrc" />
</template>
@@ -93,16 +71,25 @@
import Hls from 'hls.js'
import emptyIco from '@/assets/images/n-icon.png'
import { debounce } from 'lodash'
const Z00M_IN = 'ZOOM_IN' // 焦距变大
const Z00M_OUT = 'ZOOM_OUT' // 焦距变
const UP = 'UP' // 上转
const DOWN = 'DOWN' //
const LEFT = 'LEFT' //
const RIGHT = 'RIGHT' //
const STOP = 'STOP' // 停止操作
import WebRTCWhep from 'whepts'
// const Z00M_IN = 'ZOOM_IN' // 焦距变
// const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
// const UP = 'UP' //
// const DOWN = 'DOWN' //
// const LEFT = 'LEFT' //
// const RIGHT = 'RIGHT' // 右转
// const STOP = 'STOP' // 停止操作
const Z00M_IN = 'zoomin' // 焦距变大
const Z00M_OUT = 'zoomout' // 焦距变小
const UP = 'up' // 上转
const DOWN = 'down' // 下转
const LEFT = 'left' // 左转
const RIGHT = 'right' // 右转
const STOP = 'stop' // 停止操作
let cond = ref(false)
let ACTION = '0'
let hlsRefs = []
let webrtcRefs = []
let hlsRef = null
let timer = null
let videoLog = ref(1)
@@ -169,7 +156,6 @@
params.businessVideoDisplayPosition = ''
let res = await getVideCollectCate(params)
videoList.value = res.data
console.log(res,videoList.value.length,'ressssssssssssss')
if(videoList.value.length<=3){
grad.value = 3
}else if(videoList.value.length<=6){
@@ -205,6 +191,7 @@
const handleAction = async (e) => {
if (e == STOP) {
ACTION = '1'
command.value = e
} else {
ACTION = '0'
command.value = e
@@ -237,7 +224,8 @@
show.value = true
let res = await getPreviewUrlApi({
type: 'hls',
cameraIndexCode:itemCode
cameraIndexCode:itemCode,
subStream:0
})
cameraIndexCode.value = itemCode;
isCollect.value = resource.isCollect
@@ -246,12 +234,26 @@
}
//清除 hls
const clearHlsRefs = () => {
if (hlsRefs.length > 0) {
hlsRefs.map((item) => {
item.destroy()
})
hlsRefs = []
}
if (hlsRefs.length > 0) {
hlsRefs.map((item) => {
try{
item.destroy()
}catch (e) {
}
})
hlsRefs = []
}
if(webrtcRefs.length>0){
webrtcRefs.map((item) => {
try{
item.close()
}catch (e) {
}
})
webrtcRefs = [];
}
}
// 分页
const currentChange = (e) => {
@@ -279,6 +281,64 @@
if (type == 100) initVideo()
}, 1000)
}
const createPlayer = (cameraIndexCode,videoElement) => {
getPreviewUrlApi({
type: 'hls',
cameraIndexCode: cameraIndexCode,
subStream:1
}).then(res=>{
const url = res.data.url;
if(url.startsWith('http://192.168.77.200:8050/')){
const player = new WebRTCWhep({
url: url, // WHEP 服务器地址
container: videoElement, // 视频播放容器
iceServers: [{ urls: 'turn:192.168.77.200:3478',username: 'ZLMediaKit',credential: 'ZLMediaKit'}]
})
player.on('error', (error) => {
console.error('错误:', error.message, error.type)
if(error.type ==='REQUEST_ERROR' || error.type ==='NOT_FOUND_ERROR'){
createPlayer(cameraIndexCode,videoElement);
}
})
player.on('play:failed', (err) => {
// console.log('播放失败:', err)
// createPlayer(cameraIndexCode,videoElement);
})
webrtcRefs.push(player)
}else{
const player = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
player.loadSource(url)
player.attachMedia(videoElement)
player.on(Hls.Events.MANIFEST_PARSED, () => {
videoElement.play()
})
player.on(Hls.Events.ERROR, (event, data) => {
// 根据错误类型进行处理
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)
}
})
}
const initVideo = () => {
clearHlsRefs()
nextTick(() => {
@@ -286,20 +346,8 @@
it.videos.forEach((item,index)=>{
setTimeout(() => {
const video = document.getElementById(`monitorVideo${item.cameraIndexCode}`)
if(item.hlsUrl){
const hls = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
hls.loadSource(item.hlsUrl)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play()
})
hlsRefs.push(hls)
}
const video = document.getElementById(`collectmonitorVideo${item.cameraIndexCode}`)
createPlayer(item.cameraIndexCode,video);
}, 1000)
})
@@ -318,18 +366,18 @@
)
// 更新视频
const postVideoRemain = async () => {
timer = setInterval(() => {
clearInterval(timer)
videoList.value.forEach((items,index)=>{
setTimeout(()=>{
postVideoRemainApi({
cameraIndexCode: items.videos.map((item) => item.cameraIndexCode)
})
},1500)
})
}, 1500)
// timer = setInterval(() => {
// clearInterval(timer)
// videoList.value.forEach((items,index)=>{
// setTimeout(()=>{
// postVideoRemainApi({
// cameraIndexCode: items.videos.map((item) => item.cameraIndexCode)
// })
// },1500)
//
// })
//
// }, 1500)
}
const getVideoRegions = async () => {
let res = await getVideoRegionsApi({

File diff suppressed because one or more lines are too long

View File

@@ -138,7 +138,23 @@
} else {
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(() => {})

View File

@@ -1,25 +1,6 @@
<template>
<div class="video-box">
<div class="left-nav">
<div v-if="showNav" class="ul">
<div
class="li"
:class="{ active: current == index }"
v-for="(item, index) in navList"
:key="index"
@click="handleNav(index)"
>
<span v-if="!params.businessScenicArea && index == 0"> 核心路段 </span>
<span v-else>
{{ item.dictLabel }}
</span>
</div>
<div
:class="{ active: current == 3 }"
@click="handleNav(3)" class="li">
<span>自定义</span>
</div>
</div>
<div class="bom-box">
<Title2 title="检索" />
<div class="search-box">
@@ -55,7 +36,7 @@
<div v-if="videoLog == 1" class="video-wrapper">
<div class="video-list">
<div class="empty-box" v-if="videoList.length==0&&cond">
未接入
{{cond?'未接入':'加载中...'}}
</div>
<div
class="video-item"
@@ -76,18 +57,19 @@
@click.stop="handleCollect(item.cameraIndexCode, item.isCollect, index)"
>关注
</div>
<video
class="video-item__video"
:id="'monitorVideo' + index"
preload="auto"
muted
autoplay
:controls="false"
>
<source type="application/x-mpegURL" />
</video>
<video
:id="'hotelmonitorVideo' + index"
preload="auto"
class="video-item__video"
muted
autoplay
:controls="false"
>
<source type="application/x-mpegURL"/>
</video>
<p class="video-item__title--primary">
{{ item.cameraName || item.scenicAreaId }}
{{ item.cameraName || item.cameraIndexCode }}
</p>
</div>
</div>
@@ -146,15 +128,15 @@
class="item"
v-for="(item, index) in videoList"
:key="index"
@click="handleItemVideo(item.hlsUrl, 101, item.handleItemVideo,item)"
@click="handleItemVideo(item.hlsUrl, 101, item.cameraIndexCode,item)"
>
<div>
<p class="item-title--primary">
{{ item.cameraName || item.scenicAreaId }}
{{ item.cameraName || item.cameraIndexCode }}
</p>
<video
class="item-img"
:id="'monitorVideo' + index"
:id="'hotelmonitorVideo' + index"
muted
autoplay
:controls="false"
@@ -196,16 +178,26 @@
import Hls from 'hls.js'
import emptyIco from '@/assets/images/n-icon.png'
import { debounce } from 'lodash'
const Z00M_IN = 'ZOOM_IN' // 焦距变大
const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
const UP = 'UP' // 上转
const DOWN = 'DOWN' // 下转
const LEFT = 'LEFT' //
const RIGHT = 'RIGHT' //
const STOP = 'STOP' // 停止操作
// import mpegtsjs from "mpegts.js";
import WebRTCWhep from "whepts";
// const Z00M_IN = 'ZOOM_IN' // 焦距变大
// const Z00M_OUT = 'ZOOM_OUT' // 焦距变小
// const UP = 'UP' //
// const DOWN = 'DOWN' //
// const LEFT = 'LEFT' // 左转
// const RIGHT = 'RIGHT' // 右转
// const STOP = 'STOP' // 停止操作
const Z00M_IN = 'zoomin' // 焦距变大
const Z00M_OUT = 'zoomout' // 焦距变小
const UP = 'up' // 上转
const DOWN = 'down' // 下转
const LEFT = 'left' // 左转
const RIGHT = 'right' // 右转
const STOP = 'stop' // 停止操作
let cond = ref(false)
let ACTION = '0'
let hlsRefs = []
let webrtcRefs = []
let hlsRef = null
let timer = null
let videoLog = ref(1)
@@ -242,11 +234,14 @@
isCollect: status == 0 ? 1 : 0
})
if (status == 0) {
thisVideo.value.isCollect=1
if(thisVideo.value){
thisVideo.value.isCollect=1
}
videoList.value[index].isCollect = 1
} else {
thisVideo.value.isCollect=0
if(thisVideo.value){
thisVideo.value.isCollect=0
}
videoList.value[index].isCollect = 0
}
pubSub.publish('videoCollect', id)
@@ -255,6 +250,7 @@
const handleAction = async (e) => {
if (e == STOP) {
ACTION = '1'
command.value = e
} else {
ACTION = '0'
command.value = e
@@ -275,7 +271,12 @@
// 返回列表
const handleBack = () => {
videoLog.value = 1
hlsRef.destroy()
try{
hlsRef.destroy()
}catch (e) {
}
initVideo()
}
let isCollect = ref(0)
@@ -285,7 +286,8 @@
show.value = true
let res = await getPreviewUrlApi({
type: 'hls',
cameraIndexCode:itemCode
cameraIndexCode:itemCode,
subStream:0
})
cameraIndexCode.value = itemCode;
isCollect.value = resource.isCollect
@@ -296,10 +298,21 @@
const clearHlsRefs = () => {
if (hlsRefs.length > 0) {
hlsRefs.map((item) => {
item.destroy()
try{
item.destroy()
}catch (e) {
}
})
hlsRefs = []
}
if (webrtcRefs.length > 0) {
webrtcRefs.map((item) => {
item.close()
})
webrtcRefs = []
}
}
// 分页
const currentChange = (e) => {
@@ -307,6 +320,66 @@
videoList.value = []
getRegionsList()
}
const createPlayer = (cameraIndexCode,videoElement) => {
getPreviewUrlApi({
type: 'hls',
cameraIndexCode: cameraIndexCode,
subStream:0
}).then(res=>{
const url = res.data.url;
console.log('url',url)
if(url.startsWith('http://192.168.77.200:8050/')){
const player = new WebRTCWhep({
url:url, // WHEP 服务器地址
container: videoElement, // 视频播放容器
iceServers: [{ urls: 'turn:192.168.77.200:3478',username: 'ZLMediaKit',credential: 'ZLMediaKit'}]
})
player.on('error', (error) => {
console.error('错误:', error.message, error.type)
if(error.type ==='REQUEST_ERROR' || error.type ==='NOT_FOUND_ERROR'){
createPlayer(cameraIndexCode,videoElement);
}
})
player.on('play:failed', (err) => {
console.error('错误:',err)
// createPlayer(cameraIndexCode,videoElement);
})
webrtcRefs.push(player)
}
else{
const player = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
player.loadSource(url)
player.attachMedia(videoElement)
player.on(Hls.Events.MANIFEST_PARSED, () => {
videoElement.play()
})
player.on(Hls.Events.ERROR, (event, data) => {
// 根据错误类型进行处理
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)
}
})
}
//获取酒店视频列表
const getRegionsList = async()=>{
clearHlsRefs()
@@ -321,39 +394,8 @@
nextTick(() => {
if(!videoList.value.length) return false
videoList.value.forEach(async (x, index) => {
const video = document.getElementById(`monitorVideo${index}`)
const hls = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
hls.loadSource(x.hlsUrl)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play()
})
hls.on(Hls.Events.ERROR, (event, data) => {
console.error('HLS 播放器遇到错误:', data);
// 根据错误类型进行处理
// initVideo()
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.log('网络错误,尝试重新加载');
hls.startLoad();
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.log('媒体错误,尝试修复');
hls.recoverMediaError();
break;
default:
console.log('无法恢复的错误,销毁播放器');
// hls.destroy();
break;
}
}
})
hlsRefs.push(hls)
const video = document.getElementById(`hotelmonitorVideo${index}`)
createPlayer(x.cameraIndexCode,video);
})
})
} else {
@@ -368,40 +410,55 @@
}
let thisVideo = ref(null)
const handleItemVideo = (url, type, code,item) => {
thisVideo.value = item
videoLog.value = 2
cameraIndexCode.value = code
setTimeout(() => {
hlsRef = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
hlsRef.loadSource(url)
hlsRef.attachMedia(videoRef.value)
hlsRef.on(Hls.Events.MANIFEST_PARSED, () => {
videoRef.value.play()
})
if (type == 100) initVideo()
}, 1000)
}
const handleItemVideo = async (url, type, code, item) => {
let res = await getPreviewUrlApi({
cameraIndexCode: code,
type: 'hls',
subStream:0
})
url = res.data.url
thisVideo.value = item
videoLog.value = 2
cameraIndexCode.value = code
setTimeout(() => {
if (url.startsWith('http://192.168.77.200:8050/')) {
hlsRef = new WebRTCWhep({
url: url, // WHEP 服务器地址
container: videoRef.value, // 视频播放容器
iceServers: [{ urls: 'turn:192.168.77.200:3478',username: 'ZLMediaKit',credential: 'ZLMediaKit'}]
})
hlsRef.on('error', (error) => {
console.error('错误:', error.message, error.type)
if(error.type ==='REQUEST_ERROR' || error.type ==='NOT_FOUND_ERROR'){
handleItemVideo(url, type, code, item);
}
})
hlsRef.on('play:failed', (err) => {
console.log('播放失败,尝试重新加载');
// handleItemVideo(url, type, code, item);
})
} else {
hlsRef = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
hlsRef.loadSource(url)
hlsRef.attachMedia(videoRef.value)
hlsRef.on(Hls.Events.MANIFEST_PARSED, () => {
videoRef.value.play()
})
}
if (type == 100) initVideo()
}, 1000)
}
const initVideo = () => {
clearHlsRefs()
nextTick(() => {
videoList.value.forEach(async (item, index) => {
const video = document.getElementById(`monitorVideo${index}`)
const hls = new Hls({
maxBufferLength: 10, // 最大缓冲长度(秒)
maxMaxBufferLength: 15, // 缓冲区长度的上限
maxBufferSize: 30 * 1000 * 1000 // 最大缓冲大小(字节)
})
hls.loadSource(item.hlsUrl)
hls.attachMedia(video)
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play()
})
hlsRefs.push(hls)
const video = document.getElementById(`hotelmonitorVideo${index}`)
createPlayer(item.cameraIndexCode,video);
})
})
}
@@ -416,25 +473,25 @@
)
// 更新视频
const postVideoRemain = async () => {
timer = setInterval(() => {
postVideoRemainApi({
cameraIndexCode: videoList.value.map((item) => item.cameraIndexCode)
})
}, 1500)
// timer = setInterval(() => {
// postVideoRemainApi({
// cameraIndexCode: videoList.value.map((item) => item.cameraIndexCode)
// })
// }, 1500)
}
const getVideoRegions = async () => {
let res = await getVideoRegionsApi({
cameraName: cameraName.value,
businessScenicArea: params.businessScenicArea
})
console.log(res,11111111111111)
regionList.value = res.data
regionList.value.forEach((item,index)=>{
// item.show = true
item.videoResources=item.resourcesList[0].videoResources
})
regionList.value[0].show = true
if(res.data.length>0){
regionList.value = res.data[0].resourcesList;
regionList.value[0].show = false
}
// regionList.value.forEach((item,index)=>{
// // item.show = true
// item.videoResources=item.resourcesList[0].videoResources
// })
}
const handleRegions = (e) => {
regionList.value[e].show = !regionList.value[e].show
@@ -447,6 +504,7 @@
hotelChange = pubSub.subscribe('hotelChange', (msg, data) => {
cameraName.value = ''
params.businessScenicArea = data.name
regionList.value=[];
getVideoRegions()
})

View File

@@ -19,7 +19,7 @@
},
total: {
type: Number,
default: () => 123456
default: () => 0
},
colors: {
type: Array,
@@ -42,7 +42,14 @@
itemGap: fitChartSize(6),
formatter: (name) => {
let obj = props.dataList.find((item) => item.name == name)
return `{name|${name}} {value|${obj?.value}}{value|%}`
let total = getTotal();
if(total==0){
return `{name|${name}} {value|0}{value|%}`
}else{
let value = ((obj?.value/total).toFixed(4)*100).toFixed(2);
return `{name|${name}} {value|${value}}{value|%}`
}
},
textStyle: {
rich: {
@@ -126,7 +133,26 @@
return `{value|${getTotal()}}` + '\n' + `{name|告警总数}`
}
}
setOption(params)
const changeChart = setOption(params)
changeChart.off('legendselectchanged');
changeChart.on('legendselectchanged', function (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)
}
})
params.series[0].label.formatter = () => {
return `{value|${echartsNum}}` + '\n' + `{name|告警总数}`
}
setOption(params)
});
}
</script>

View File

@@ -17,7 +17,7 @@
:end-val="aiAnalyzeData.allAnalysisPoints"
/>
</div>
<div class="traffic-item" @click="showAbnormalList">
<div class="traffic-item" @click="showAbnormalList('curr')">
<span class="traffic-item__title">异常点位</span>
<countup :class="aiAnalyzeData.abnormalPoints>0?'traffic-item__value--error':'traffic-item__value--success'" :end-val="aiAnalyzeData.abnormalPoints" />
</div>
@@ -28,15 +28,15 @@
<span class="scenic-item__label">核心景区分析点位</span>
<countup class="scenic-item__value" :end-val="scenicAiAnalyzeData.coreMonitoringPoints" />
</div>
<div class="scenic-item" @click="showAbnormalList" style="cursor: pointer;" >
<div class="scenic-item" @click="showAbnormalList('curr')" style="cursor: pointer;" >
<span class="scenic-item__label">异常点位</span>
<countup class="scenic-item__value" :end-val="scenicAiAnalyzeData.abnormalPoints" />
</div>
<div class="scenic-item" @click="showAbnormalList" style="cursor: pointer;" >
<div class="scenic-item" @click="showAbnormalList('curr')" style="cursor: pointer;" >
<span class="scenic-item__label">异常告警</span>
<countup class="scenic-item__value" :end-val="scenicAiAnalyzeData.abnormalAlarm" />
</div>
<div class="scenic-item">
<div class="scenic-item" @click="showAbnormalList('deal')" style="cursor: pointer;">
<span class="scenic-item__label">已解除</span>
<countup class="scenic-item__value" :end-val="scenicAiAnalyzeData.handled" />
</div>
@@ -67,15 +67,15 @@
:end-val="trafficAiAnalyzeData.coreMonitoringPoints"
/>
</div>
<div class="scenic-item" style="cursor: pointer" @click="showTrafficEvent">
<div class="scenic-item" style="cursor: pointer" @click="showTrafficEvent('curr')">
<span class="scenic-item__label">拥堵点位</span>
<countup class="scenic-item__value" :end-val="trafficAiAnalyzeData.abnormalPoints" />
</div>
<div class="scenic-item" style="cursor: pointer" @click="showTrafficEvent">
<div class="scenic-item" style="cursor: pointer" @click="showTrafficEvent('curr')">
<span class="scenic-item__label">拥堵告警</span>
<countup class="scenic-item__value" :end-val="trafficAiAnalyzeData.abnormalWarnings" />
</div>
<div class="scenic-item">
<div class="scenic-item" style="cursor: pointer" @click="showTrafficEvent('deal')">
<span class="scenic-item__label">已解除</span>
<countup class="scenic-item__value" :end-val="trafficAiAnalyzeData.handled" />
</div>
@@ -105,9 +105,9 @@
alt=""
/>
<img v-if="scenicSpotId == '龙桥河'" class="map-img" src="@/assets/images/lqh.jpg" alt="" /> -->
<iframe v-if="scenicSpotId == 'root000000'" width="100%" height="100%" src="/map/sxzd/bdc.html"></iframe>
<iframe v-if="scenicSpotId == 'root00000000'" width="100%" height="100%" src="/map/sxzd/sxzd.html"></iframe>
<iframe v-if="scenicSpotId == '龙桥河'" width="100%" height="100%" src="/map/lqh/lqh.html"></iframe>
<iframe v-if="scenicSpotId == 'root000000'" width="100%" height="100%" src="http://192.168.77.200/map/sxzd/bdc.html"></iframe>
<iframe v-if="scenicSpotId == 'root00000000'" width="100%" height="100%" src="http://192.168.77.200/map/sxzd/sxzd.html"></iframe>
<iframe v-if="scenicSpotId == '龙桥河'" width="100%" height="100%" src="http://192.168.77.200/map/lqh/lqh.html"></iframe>
</div>
<div class="flex">
<div class="monitor">
@@ -130,7 +130,7 @@
:end-val="pointAlarmData.analysisPoints"
/>
</div>
<div class="monitor-statistics-item" @click="showAbnormalList" style="cursor: pointer;" >
<div class="monitor-statistics-item" @click="showAbnormalList('curr')" style="cursor: pointer;" >
<span class="monitor-statistics-item__label">异常点位</span>
<countup
class="monitor-statistics-item__value"
@@ -140,7 +140,7 @@
</div>
</div>
<div class="bg" style="cursor: pointer;" >
<Title3 title="今日异常告警" @click="showAbnormalList" />
<Title3 title="今日异常告警" @click="showAbnormalList('all')" />
<Line
:width="370"
:height="180"
@@ -155,20 +155,20 @@
</div>
<div class="traffic-alarm-statistics">
<img class="traffic-alarm-statistics-icon" src="@/assets/images/t-ico-2.png" />
<div class="traffic-alarm-statistics-item" @click="showAbnormalList" style="cursor: pointer;">
<div class="traffic-alarm-statistics-item" @click="showAbnormalList('curr')" style="cursor: pointer;">
<span class="traffic-alarm-statistics-item__label">当前告警</span>
<countup
:class="pointAlarmData.abnormalAlarm>0?'traffic-alarm-statistics-item__error':'traffic-alarm-statistics-item__value'"
:end-val="pointAlarmData.abnormalAlarm"
/>
</div>
<div class="traffic-alarm-statistics-item">
<div class="traffic-alarm-statistics-item" @click="showAbnormalList('all')" style="cursor: pointer;">
<span class="traffic-alarm-statistics-item__label">异常告警</span>
<countup class="traffic-alarm-statistics-item__value"
:end-val="pointAlarmData.allAbnormalAlarm"
/>
</div>
<div class="traffic-alarm-statistics-item">
<div class="traffic-alarm-statistics-item" @click="showAbnormalList('deal')" style="cursor: pointer;">
<span class="traffic-alarm-statistics-item__label">已解除告警</span>
<countup
class="traffic-alarm-statistics-item__value traffic-alarm-statistics-item__value"
@@ -188,8 +188,8 @@
</template>
</div>
<video-dialog v-model="videoShow" :src="src" :cameraIndexCode="cameraIndexCode" />
<warn-list v-model="warnShow" :scenicSpotId="scenicSpotId" />
<traffic-list v-model="trafficEventShow" />
<warn-list v-model="warnShow" :type="warnType" :scenicSpotId="scenicSpotId" />
<traffic-list v-model="trafficEventShow" :type="trafficEventType" />
</template>
<script setup>
@@ -247,17 +247,22 @@
let cameraIndexCode = ref('')
let videoShow = ref(false)
let warnShow = ref(false)
let warnType = ref('')
let trafficEventShow = ref(false)
const showAbnormalList = function (){
let trafficEventType = ref('curr')
const showAbnormalList = function (type){
warnType.value = type;
warnShow.value = true;
}
const showTrafficEvent = function(){
const showTrafficEvent = function(type){
trafficEventType.value = type;
trafficEventShow.value = true;
}
window.addEventListener("message", async(e) => {
let {code,data} = await getPreviewUrlApi({
type: 'hls',
cameraIndexCode:e.data.cameraIndexCode
cameraIndexCode:e.data.cameraIndexCode,
subStream:0
})
if(code===200){
src.value = data.url

File diff suppressed because it is too large Load Diff

View File

@@ -72,11 +72,12 @@
show: true, // 显示dataZoom组件
xAxisIndex: [0], // 控制第一个x轴
start: 0, // 初始起始位置
end: 50 // 初始结束位置,可以根据数据量调整
end: 60 // 初始结束位置,可以根据数据量调整
}
],
yAxis: {
type: 'value',
offset: 15,
axisLabel: {
show: true,
fontSize: fitChartSize(12),
@@ -94,7 +95,7 @@
{
type: 'bar',
data: getSeriesData(),
barWidth: fitChartSize(40),
barWidth: fitChartSize(20),
label: {
show: true,
position: 'top',

View File

@@ -1,9 +1,9 @@
<template>
<div class="z-dialog">
<el-dialog title="核心路段拥堵告警" v-model="modelValue" align-center :modal="false" :show-close="false">
<el-dialog :title="props.type=='deal'?'已解除告警':'核心路段拥堵告警'" v-model="modelValue" align-center :modal="false" :show-close="false">
<img class="close" src="@/assets/images/close.png" @click="handleClose" />
<div class="no-data" v-if="cond&&!list.length">
暂无拥堵告警
{{props.type=='deal'?'暂无数据':'暂无拥堵告警'}}
</div>
<div v-else>
<ul class="list" >
@@ -32,6 +32,10 @@
import pubSub from 'pubsub-js'
let props = defineProps({
type: {
type: String,
default: 'curr'
},
events: {
type: String,
default: ''
@@ -127,7 +131,7 @@
}
let cond = ref(false)
const getVideoList = async () => {
let res = await getTrafficEventsApi();
let res = await getTrafficEventsApi(props.type);
list.value = res.data
setTimeout(()=>{cond.value = true},1500)
}

View File

@@ -1,12 +1,12 @@
<template>
<div class="z-dialog">
<el-dialog title="异常点位告警" v-model="modelValue" align-center :modal="false" :show-close="false">
<el-dialog :title="props.type=='deal'?'已解除异常':'异常点位告警'" v-model="modelValue" align-center :modal="false" :show-close="false">
<img class="close" src="@/assets/images/close.png" @click="handleClose" />
<div class="no-data" v-if="cond&&!list.length">
暂无异常情况
</div>
<div class="no-data" v-if="cond&&!list.length">
{{props.type=='deal'?'暂无数据':'暂无异常情况'}}
</div>
<div v-else>
<ul class="list" >
<ul class="list">
<li
class="item"
:style="{ backgroundImage: `url(${primary})` }"
@@ -44,6 +44,10 @@
type: String,
default: ''
},
type: {
type: String,
default: 'curr'
},
events: {
type: String,
default: ''
@@ -135,7 +139,7 @@
}
let cond = ref(false)
const getVideoList = async () => {
let res = await getVideoEventApi(props.scenicSpotId);
let res = await getVideoEventApi(props.scenicSpotId,props.type);
list.value = res.data
setTimeout(()=>{cond.value = true},1500)
nextTick(() => {
@@ -227,7 +231,21 @@
display: flex;
flex-wrap: wrap;
align-content: flex-start;
padding:vw(10);
padding:vw(10);
overflow: auto;
/* 滚动条整体样式 */
&::-webkit-scrollbar {
height: vh(4); /* 滚动条的宽度 */
}
/* 滚动条轨道 */
&::-webkit-scrollbar-track {
background: 'transparent'; /* 轨道的背景色 */
}
/* 滚动条滑块 */
&::-webkit-scrollbar-thumb {
background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */
border-radius: 5px; /* 滑块的圆角 */
}
.item {
position: relative;
width: 33%;

View File

@@ -22,12 +22,12 @@
</div>
<div class="main">
<a @click="hanldeToDetails" class="look-btn">
查看详情
<!-- <img src="@/assets/images/d-ico-1.png" /> -->
<!-- 查看详情-->
<img src="@/assets/images/d-icon-1.png" />
</a>
<iframe v-if="scenicSpotId == 'root000000'" width="100%" height="100%" src="/map/sxzd/bdc.html"></iframe>
<iframe v-if="scenicSpotId == 'root00000000'" width="100%" height="100%" src="/map/sxzd/sxzd.html"></iframe>
<iframe v-if="scenicSpotId == '龙桥河'" width="100%" height="100%" src="/map/lqh/lqh.html"></iframe>
<iframe v-if="scenicSpotId == 'root000000'" width="100%" height="100%" src="http://192.168.77.200/map/sxzd/bdc.html"></iframe>
<iframe v-if="scenicSpotId == 'root00000000'" width="100%" height="100%" src="http://192.168.77.200//map/sxzd/sxzd.html"></iframe>
<iframe v-if="scenicSpotId == '龙桥河'" width="100%" height="100%" src="http://192.168.77.200/map/lqh/lqh.html"></iframe>
</div>
<div class="footer">
<div class="flex">
@@ -107,12 +107,21 @@
<div class="header">
<div>订票时间</div>
<div>订票数量</div>
<template v-if="scenicSpotId == 'root00000000'">
<div>徒步订票</div>
</template>
<div>团队订票</div>
<div>散客订票</div>
</div>
<div class="list">
<div class="item" v-for="(item, index) in gridData" :key="index">
<div>{{ item.time }}</div>
<div>{{ item.value }}</div>
<template v-if="scenicSpotId == 'root00000000'">
<div>{{ item.walk }}</div>
</template>
<div>{{ item.group }}</div>
<div>{{ item.single }}</div>
</div>
</div>
</div>
@@ -165,7 +174,8 @@ import pubSub from 'pubsub-js'
window.addEventListener("message", async(e) => {
let {code,data} = await getPreviewUrlApi({
type: 'hls',
cameraIndexCode:e.data.cameraIndexCode
cameraIndexCode:e.data.cameraIndexCode,
subStream:0
})
if(code===200){
src.value = data.url
@@ -177,11 +187,9 @@ import pubSub from 'pubsub-js'
let gridData = ref([])
let gridTitle = ref('')
const handlePiaoPop = async()=>{
console.log(777777)
dialogTableVisible.value = true
let res = await getSpotTicketDate({scenicSpotId:props.scenicSpotId})
gridData.value = res.data
console.log(res,'res')
}
let myElement = ref(false)
onMounted(() => {
@@ -318,14 +326,14 @@ import pubSub from 'pubsub-js'
padding: vw(20);
display: flex;
align-items: center;
background: #0a4190;
//background: #0a4190;
border-radius: vw(4);
font-size: vw(16);
font-weight:700;
color: #fff;
z-index: 999;
img{
width:vw(100);
width:vw(80);
// height:;
}
}
@@ -415,11 +423,11 @@ import pubSub from 'pubsub-js'
}
.tag--important {
@extend .tag;
background: #feae00;
background:#d9011b;
}
.tag--warn {
@extend .tag;
background: #d9011b;
background: #feae00;
}
.tag--normal {
@extend .tag;
@@ -428,7 +436,7 @@ import pubSub from 'pubsub-js'
.content {
margin-left: vw(4);
padding: 0 vw(10);
width: vw(760);
width: vw(460);
height: vh(24);
line-height: vh(24);
white-space: nowrap;

View File

@@ -119,10 +119,10 @@
</div>
<div class="box-2">
<Title1 title="异常信息 " />
<!-- @click="handleToWorkOrder" -->
<div style="cursor: pointer;" class="count-box flex">
<count-item
v-for="item in headList"
@click="showAbnormalList(item)"
:label="item.name"
:count="item.count"
:type="item.type"
@@ -347,12 +347,14 @@
/>
<traMap :longitude="longitude" :latitude="latitude" v-model="traMapShow"></traMap>
<all-list :events="scenicSpotId" v-model="allShow" />
<warn-list v-model="warnShow" :type="warnType" :scenicSpotId="scenicSpotId" />
</template>
<script setup>
import countup from 'vue-countup-v3'
import PubSub from 'pubsub-js'
import allList from './allList'
import warnList from './warnList.vue'
import carIcon from '@/assets/images/car.png'
import carStopIcon from '@/assets/images/car-stop.png'
import carOfflineIcon from '@/assets/images/car-offline.png'
@@ -381,6 +383,7 @@
//景区排队
let allShow = ref(false)
let events = ref({})
let warnType = ref("curr")
const handleLineUp = (item)=>{
allShow.value = true
@@ -393,10 +396,11 @@
hIndex.value = index;
}
const handlePop2 = async()=>{
dialogTableVisible2.value = true
let res = await getSpotPassengerFlow({scenicSpotId:props.scenicSpotId})
tjArr.value = res.data
console.log(res,'res')
if(props.scenicSpotId=="root00000000"){
dialogTableVisible2.value = true
let res = await getSpotPassengerFlow({scenicSpotId:props.scenicSpotId})
tjArr.value = res.data
}
}
let show = ref(false)
let scenicChange = null
@@ -406,6 +410,17 @@
//查看交通信息
let latitude = ref('')
let longitude = ref('')
let warnShow = ref(false)
const showAbnormalList = function (item){
if(item.name=='今日异常总数'){
warnType.value = 'all'
}else if(item.name=='当前异常数'){
warnType.value = 'curr'
}else if(item.name=='已解除异常数'){
warnType.value = 'deal'
}
warnShow.value = true;
}
const hanldeLookMap = () => {
router.push('/traffic')
// traMapShow.value = true

View File

@@ -151,7 +151,23 @@
params.series[0].label.formatter = formatLabel()
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>

View File

@@ -0,0 +1,311 @@
<template>
<div class="z-dialog">
<el-dialog :title="props.type=='deal'?'已解除异常':'异常点位告警'" v-model="modelValue" align-center :modal="false" :show-close="false">
<img class="close" src="@/assets/images/close.png" @click="handleClose" />
<div class="no-data" v-if="cond&&!list.length">
{{props.type=='deal'?'暂无数据':'暂无异常情况'}}
</div>
<div v-else>
<ul class="list" >
<li
class="item"
:style="{ backgroundImage: `url(${primary})` }"
v-for="(item, index) in list"
:key="index"
@click="handleImage(item)"
>
<div
class="item-title-box"
><div class="item-title">{{ item.analyzeTypeName}}</div><div class="item-num">{{ item.number}}</div></div>
<img style="width: 100%;height: 100%;" :src="item.imgurl || imageEmpty">
<p style="display: flex;justify-content: space-between;">
<span> {{ item.cameraName || item.cameraIndexCode }}</span>
<span class="cursorPointer" @click.stop="handleItem(item.cameraIndexCode)">查看监控></span>
</p>
</li>
</ul>
</div>
</el-dialog>
</div>
<video-dialog v-model="videoShow" :src="src" :isDiy="isDiy" :isCollect="isCollect" :cameraIndexCode="cameraIndexCode" />
<image-dialog v-model="imageShow" :src="imgSrc"/>
</template>
<script setup>
import { getVideoEventApi,getVideoEventObjImgApi,getPreviewUrlSubApi } from '@/api/monitor'
import primary from '@/assets/images/item-primary.png'
import imageEmpty from '@/assets/images/imgloading.png'
import pubSub from 'pubsub-js'
let props = defineProps({
type: {
type: String,
default: 'curr'
},
scenicSpotId: {
type: String,
default: ''
},
events: {
type: String,
default: ''
},
})
let modelValue = defineModel()
let isCollect = ref(0)
let list = ref([])
let hlsRefs = []
let total = ref(0)
let src = ref('')
let imgSrc = ref('')
let imageShow = ref(false)
let cameraIndexCode = ref('')
let videoShow = ref(false)
let isDiy = ref(0)
let params = reactive({
pageNum: 1,
pageSize: 6,
businessScenicArea:'',
})
watch(
() => modelValue.value,
(val) => {
if (val) {
params.pageNum = 1
cond.value = false
params.businessScenicArea = props.events
list.value = []
setTimeout(() => {
getVideoList()
}, 1000)
}
}
)
const onVideoCollect = () => {
pubSub.subscribe('videoCollect', () => {
clearHlsRefs()
getVideoList()
})
}
const handleImage = (item) => {
getVideoEventObjImgApi(item.objId).then(res=>{
imgSrc.value = res.data.url
imageShow.value = true
});
}
const handleItem = async (id) => {
let {code,data} = await getPreviewUrlSubApi({
type: 'hls',
cameraIndexCode:id
})
if(code===200){
src.value = data.url
isCollect.value = data.isCollect
isDiy.value = data.isDiy
cameraIndexCode.value = id
videoShow.value = true
}
}
const handleCollect = async (id, status) => {
await postVideoCollectApi({
cameraIndexCode:id,
isCollect: status == 0 ? 1 : 0
})
clearHlsRefs()
params.pageNum = 1
getVideoList()
pubSub.publish('videoCollect')
}
const handleClose = () => {
clearHlsRefs()
modelValue.value = false
}
const clearHlsRefs = () => {
if (hlsRefs.length > 0) {
hlsRefs.map((item) => {
item.destroy()
})
hlsRefs = []
}
}
const pageNumChange = () => {
cond.value = false
clearHlsRefs()
list.value = []
getVideoList()
}
let cond = ref(false)
const getVideoList = async () => {
let res = await getVideoEventApi(props.scenicSpotId,props.type);
list.value = res.data
setTimeout(()=>{cond.value = true},1500)
nextTick(() => {
list.value.forEach(async (item, index) => {
getVideoEventObjImgApi(item.objId).then(res=>{
list.value[index].imgurl = res.data.url
});
})
})
}
onMounted(() => {
onVideoCollect()
})
onUnmounted(() => {})
</script>
<style scoped lang="scss">
.z-dialog {
:deep(.el-dialog) {
width: vw(2100);
padding: vw(8);
background-image: url('@/assets/images/dialog-bg.png') !important;
background-size: 100% 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: vw(4); /* 滚动条的宽度 */
}
/* 滚动条轨道 */
&::-webkit-scrollbar-track {
background: 'transparent'; /* 轨道的背景色 */
}
/* 滚动条滑块 */
&::-webkit-scrollbar-thumb {
background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */
border-radius: 5px; /* 滑块的圆角 */
}
}
:deep(.el-dialog__header) {
padding-bottom: 0 !important;
margin-top: vw(10);
text-align: center;
}
:deep(.el-dialog__title) {
color: #fff;
font-weight: bold;
}
}
.item-title-box{
font-weight: 400;
z-index: 99;
height: vw(50);
font-size: vw(24);
color: #ffffff;
position: absolute;
left: 3%;
.item-title{
padding: vw(10);
display: inline-block;
background-image: url('@/assets/images/unfollow.png');
background-size: 100% 100%;
}
.item-num{
margin-left: vw(6);
padding: vw(5);
display: inline-block;
color: #ffffff;
background-color:red;
border-radius: 15%;
font-weight: bolder;
}
}
.no-data{
height:vh(860);
line-height: vh(860);
text-align: center;
font-size:vw(30);
color:#02f9fa;
}
.cursorPointer{
cursor: pointer
}
.list {
//margin-top: vw(5);
gap: vw(8);
height: vh(860);
overflow: auto;
/* 滚动条整体样式 */
&::-webkit-scrollbar {
width: vw(4); /* 滚动条的宽度 */
}
/* 滚动条轨道 */
&::-webkit-scrollbar-track {
background: 'transparent'; /* 轨道的背景色 */
}
/* 滚动条滑块 */
&::-webkit-scrollbar-thumb {
background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */
border-radius: 5px; /* 滑块的圆角 */
}
display: flex;
flex-wrap: wrap;
align-content: flex-start;
padding:vw(10);
.item {
position: relative;
width: 33%;
height: vh(400);
padding: vw(12);
margin-top:vh(20);
box-sizing: border-box;
background-image: url('@/assets/images/item-primary.png');
background-size: 100% 100%;
&-video {
width: 100%;
height: 100%;
object-fit: cover;
}
> p {
position: absolute;
bottom: vw(12);
width: calc(100% - vw(24));
padding: vw(10) 0;
background: rgba(4, 30, 69, 0.72);
> span {
padding-left: vw(10);
font-weight: 400;
font-size: vw(14);
line-height: vw(14);
color: #ffffff;
}
}
&-unfollow {
cursor: pointer;
display: none;
position: absolute;
right: vw(4);
top: vw(4);
z-index: 99;
width: vw(64);
height: vw(30);
text-align: center;
line-height: vw(30);
font-weight: 400;
font-size: vw(12);
color: #ffffff;
background-image: url('@/assets/images/unfollow.png');
background-size: 100% 100%;
}
}
}
.pagination {
padding-top: vw(20);
margin-right: vw(20);
padding-bottom: vw(20);
display: flex;
justify-content: flex-end;
}
.close {
cursor: pointer;
position: absolute;
right: vw(20);
top: vw(20);
width: vw(60);
z-index: 9999;
}
</style>

View File

@@ -144,6 +144,13 @@
scenicSpotId: scenicSpotId.value
})
)
sendMessage(
JSON.stringify({
action: 'start',
type: 'secureData',
scenicSpotId: scenicSpotId.value
})
)
sendMessage(
JSON.stringify({
action: 'start',

View File

@@ -56,11 +56,11 @@
font-size: vw(13);
color: #0084ff;
height: vh(40);
display: flex;
align-items: center;
background: rgba(3, 78, 153, 0.3);
& > p {
flex: 1;
color: #ffffff;
text-align: center;
}
}

View File

@@ -531,6 +531,7 @@
display: flex;
align-items: center;
height: vh(40);
cursor: pointer;
&:nth-child(2n + 1) {
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__point" @click="videoShow = true">3号点位</div> -->
</div>
<div class="header-status">{{ item.congestLevelText }} </div>
<div class="header-status" v-if="item.congestLevel>0">{{ item.congestLevelText }} </div>
</div>
<div class="statistics">
<div class="statistics-item">
@@ -191,7 +191,8 @@
const getPreviewUrl = async (code) => {
let res = await getPreviewUrlApi({
cameraIndexCode: code,
type: 'hls'
type: 'hls',
subStream:0
})
src.value = res.data.url
videoShow.value = true

View File

@@ -8,24 +8,236 @@
<span :class="item.level">{{ item.level_text }}</span>
<p>{{ item.title }}</p>
<span class="time">{{ item.time }}</span>
<span class="btn" @click="showDetail(item.id)">详情</span>
</div>
</div>
<el-dialog :title="'工单详情'" center v-model="dialogVisible">
<div class="bom-box">
<div class="modal-body">
<div class="m-modal">
<div class="item-title--primary">工单信息</div>
<div class="item-col">
<div class="detail-item">
<span class="label">所属景区</span>
<span class="value">{{ detail.road }}</span>
</div>
<div class="detail-item">
<span class="label">工单类型</span>
<span class="value">{{ detail.type }}</span>
</div>
<div class="detail-item">
<span class="label">紧急程度</span>
<span :class="'value '+detail.level">{{ detail.levelText }}</span>
</div>
</div>
<div class="item-col">
<div class="detail-item">
<span class="label">具体地点</span>
<span class="value">{{ detail.address }}</span>
</div>
<div class="detail-item">
<span class="label">创建时间</span>
<span class="value">{{ detail.createTime }}</span>
</div>
<div class="detail-item">
<span class="label">完成期限</span>
<span class="value">{{ detail.completionDeadline }}</span>
</div>
</div>
<div class="item-col">
<div class="detail-item">
<span class="label">创建人</span>
<span class="value">{{ detail.unidName }}</span>
</div>
<div class="detail-item">
<span class="label">状态</span>
<span class="value important" v-if="detail.status==0">待处理</span>
<span class="value warn" v-else-if="detail.status==1">进行中</span>
<span class="value normal" v-else-if="detail.status==2">已完成</span>
<span class="value" v-else>已关闭</span>
</div>
<div class="detail-item">
<span class="label">工单内容</span>
<span class="value">{{ detail.title }}</span>
</div>
</div>
<div class="detail-item" v-if="detail.imgs">
<el-image class="img" :preview-src-list="detail.imgs.split(',')" :src="src" v-for="(src, index) in detail.imgs.split(',')"></el-image>
</div>
<div class="item-title--warning" v-if="detail.follow && detail.follow.length>0">跟进信息</div>
<template v-for="item in detail.follow">
<div class="item-col" >
<div class="detail-item">
<span class="label">跟进人</span>
<span class="value">{{ item.unidName }}</span>
</div>
<div class="detail-item">
<span class="label">跟进地点</span>
<span class="value">{{ item.address }}</span>
</div>
<div class="detail-item">
<span class="label">完成率</span>
<span class="value">{{ item.rate }}%</span>
</div>
</div>
<div class="item-col">
<div class="detail-item" style="flex: 2">
<span class="label">跟进内容</span>
<span class="value">{{ item.title }}</span>
</div>
<div class="detail-item">
<span class="label">跟进时间</span>
<span class="value">{{ item.createTime }}</span>
</div>
</div>
<div class="detail-item" v-if="item.img">
<el-image class="img" :preview-src-list="item.img.split(',')" :src="src" v-for="(src, index) in item.img.split(',')"></el-image>
</div>
<div style="border-bottom: 1px solid #ffffff;margin: 10px 0;"></div>
</template>
<div class="item-title--warning" v-if="detail.evaluate && detail.evaluate.length>0">评价信息</div>
<template v-for="item in detail.evaluate">
<div class="item-col" >
<div class="detail-item">
<span class="label">评价人</span>
<span class="value warn">{{ item.unidName }}</span>
</div>
<div class="detail-item">
<span class="label">评价内容</span>
<span class="value">{{ item.title }}</span>
</div>
<div class="detail-item">
<span class="label">评价时间</span>
<span class="value">{{ item.createTime }}</span>
</div>
</div>
<div class="detail-item" v-if="item.img">
<el-image class="img" :preview-src-list="item.img.split(',')" :src="src" v-for="(src, index) in item.img.split(',')"></el-image>
</div>
<div style="border-bottom: 1px solid #ffffff;margin: 10px 0;"></div>
</template>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { getListApi } from '@/api/workOrder'
import {getDetailApi, getListApi} from '@/api/workOrder'
let dialogVisible = ref(false)
let list = ref([])
let detail = ref({})
const getList = async () => {
let res = await getListApi()
list.value = res.data
}
const showDetail = async (id) => {
dialogVisible.value = true
let res = await getDetailApi(id)
console.log( res.data);
detail.value = res.data
}
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
:deep(.el-dialog__headerbtn .el-dialog__close){
color:#fff;
font-size:40px;
position:relative;
top:15px;
right:15px;
}
:deep(.el-dialog__header.show-close){
padding-right:0;
}
:deep(.el-dialog) {
background: url('/src/assets/images/map-bg-2.png') no-repeat top center;
background-size: 100% 100%;
width:vw(1500);
}
:deep(.el-dialog__title) {
color: #fff;
font-weight: bold;
}
.m-modal{
padding: vw(30);
.normal {
padding:3px 10px;
background: #2380fb;
}
.warn {
padding:3px 10px;
background: #feae00;
}
.important {
padding:3px 10px;
background: #d9011b;
}
.item-title{
font-size: 18px;
margin-bottom: vw(30);
&--primary {
@extend .item-title;
color: #02f9fa;
background-image: url('@/assets/images/mask-primary.png');
background-size: 100% 100%;
}
&--error {
@extend .item-title;
background-image: url('@/assets/images/mask-error.png');
background-size: 100% 100%;
}
&--warning {
@extend .item-title;
background-image: url('@/assets/images/mask-warning.png');
background-size: 100% 100%;
}
&--success {
@extend .item-title;
background-image: url('@/assets/images/mask-success.png');
background-size: 100% 100%;
}
}
.item-col{
display: flex;
justify-content: space-between;
}
}
.detail-item {
flex:1;
margin-bottom: 12px;
line-height: 1.5;
.img{
width:vw(200);
height: vw(200);
margin: 10px;
}
}
.bom-box {
.modal-body {
padding: 16px;
color:#fff;
height:vh(780);
overflow:auto;
/* 滚动条整体样式 */
&::-webkit-scrollbar {
width: vw(4); /* 滚动条的宽度 */
}
/* 滚动条轨道 */
&::-webkit-scrollbar-track {
background: 'transparent'; /* 轨道的背景色 */
}
/* 滚动条滑块 */
&::-webkit-scrollbar-thumb {
background: rgba(0, 150, 255, 0.63); /* 滑块的背景色 */
border-radius: 5px; /* 滑块的圆角 */
}
}
}
.work-box-1 {
width: vw(815);
height: vh(950);
@@ -61,6 +273,7 @@
align-items: center;
justify-content: flex-start;
padding: vh(8) vh(10);
font-size: vw(14);
.label {
width: vw(60);
height: vh(24);
@@ -84,7 +297,7 @@
@extend .label;
background: #d9011b;
}
.time {
.time,.btn {
font-weight: 400;
font-size: vw(12);
color: rgba(255, 255, 255, 0.6);
@@ -94,9 +307,15 @@
text-transform: none;
margin-left: vw(30);
}
.btn{
font-size: vw(18);
font-weight: bolder;
color: #2380fb;
cursor: pointer;
}
p {
font-weight: 400;
font-size: vw(15);
//font-size: vw(15);
color: #ffffff;
text-align: left;
font-style: normal;

View File

@@ -12,12 +12,13 @@
<div class="item item1">
工单完成数 <span class="color1"><countup :end-val="totalData.complete" /></span>
</div>
<div class="item item3">
紧急工单数 <span class=""><countup :end-val="totalData.warn" /></span>
</div>
<div class="item item2">
重要工单数 <span class=""><countup :end-val="totalData.important" /></span>
</div>
<div class="item item3">
紧急工单数 <span class=""><countup :end-val="totalData.warn" /></span>
</div>
<div class="item item1">
普通工单数 <span class=""><countup :end-val="totalData.normal" /></span>
</div>
@@ -94,24 +95,24 @@
<p class="value">{{ item.value }}%</p>
</li>
</ul>
<div v-if="pointRankData.length > 0" class="alarm">
<Title2 title="异常点位告警排名" />
<ul class="alarm__wrapper">
<li class="alarm-item" v-for="(item, index) in pointRankData" :key="index">
<p
class="alarm-item__rank"
:class="{
'alarm-item__rank--error': index == 0,
'alarm-item__rank--warning': index == 1,
'alarm-item__rank--primary': index == 2
}"
>
{{ index + 1 }}
</p>
<p class="alarm-item__content">{{ item.link_title }}</p>
</li>
</ul>
</div>
<!-- <div v-if="pointRankData.length > 0" class="alarm">-->
<!-- <Title2 title="异常点位告警排名" />-->
<!-- <ul class="alarm__wrapper">-->
<!-- <li class="alarm-item" v-for="(item, index) in pointRankData" :key="index">-->
<!-- <p-->
<!-- class="alarm-item__rank"-->
<!-- :class="{-->
<!-- 'alarm-item__rank&#45;&#45;error': index == 0,-->
<!-- 'alarm-item__rank&#45;&#45;warning': index == 1,-->
<!-- 'alarm-item__rank&#45;&#45;primary': index == 2-->
<!-- }"-->
<!-- >-->
<!-- {{ index + 1 }}-->
<!-- </p>-->
<!-- <p class="alarm-item__content">{{ item.link_title }}</p>-->
<!-- </li>-->
<!-- </ul>-->
<!-- </div>-->
</div>
</div>
</div>
@@ -288,7 +289,6 @@
height: vh(58);
line-height: vh(58);
padding-left: vw(10);
text-align: center;
margin: 0 vw(15);
font-weight: 400;
font-size: vw(14);
@@ -296,6 +296,7 @@
text-align: left;
font-style: normal;
text-transform: none;
align-items: baseline;
span {
font-size: vw(24);
position: relative;
@@ -311,11 +312,11 @@
background-size: 100% 100%;
}
.item2 {
background-image: url('@/assets/images/work-n-bg-2.png');
background-image: url('@/assets/images/work-n-bg-3.png');
background-size: 100% 100%;
}
.item3 {
background-image: url('@/assets/images/work-n-bg-3.png');
background-image: url('@/assets/images/work-n-bg-2.png');
background-size: 100% 100%;
}
}

View File

@@ -16,20 +16,20 @@
<div class="statistics-item">
<div class="flex align-center">
<img class="statistics-item__icon" src="@/assets/images/dot-error.svg" />
<span class="statistics-item__label">紧急消息</span>
<span class="statistics-item__label">重要消息</span>
</div>
<div class="statistics-item__value--error">
<countup :end-val="countInfo.warn" />
<countup :end-val="countInfo.important" />
<span class="statistics-item__value-suffix"></span>
</div>
</div>
<div class="statistics-item">
<div class="flex align-center">
<img class="statistics-item__icon" src="@/assets/images/dot-warning.svg" />
<span class="statistics-item__label">重要消息</span>
<span class="statistics-item__label">紧急消息</span>
</div>
<div class="statistics-item__value--warning">
<countup :end-val="countInfo.important" />
<countup :end-val="countInfo.warn" />
<span class="statistics-item__value-suffix"></span>
</div>
</div>
@@ -45,14 +45,22 @@
</div>
</div>
<div class="chart-box">
<pie
v-for="(item, index) in newsStateList"
:key="index"
:value="item.value"
:label="item.name"
:width="150"
:height="150"
/>
<!-- <pie-->
<!-- v-for="(item, index) in newsStateList"-->
<!-- :key="index"-->
<!-- :value="item.value"-->
<!-- :label="item.name"-->
<!-- :width="150"-->
<!-- :height="150"-->
<!-- />-->
<newsRate :dataList="newsStateList" />
<ul class="chart__legend">
<li class="chart__legend-item" v-for="(item, index) in newsStateList" :key="index">
<p class="dot" :style="{ background: colors[index] }" />
<p class="name">{{ item.name }}</p>
<p class="value">{{ item.value }}</p>
</li>
</ul>
</div>
</div>
<div class="work-box-1">
@@ -79,7 +87,8 @@
import countup from 'vue-countup-v3'
import pie from './pie.vue'
import { getNewsListApi, getNewsStateApi, getNewsTotalApi } from '@/api/news'
import NewsRate from "@/views/workOrder/components/newsRate.vue";
const colors = ['#FDC40A', '#FF5232', '#50F0A6', '#5FDFFA']
let list = ref([])
let countInfo = ref({
important: 0,
@@ -109,6 +118,69 @@
</script>
<style lang="scss" scoped>
.chart {
display: flex;
justify-content: space-between;
padding: vw(20) vw(15);
&__wrapper {
width: vw(740);
height: vh(370);
padding: 0 vw(20);
background-image: url('@/assets/images/bg-3.png');
background-size: 100% 100%;
}
&__inner {
display: flex;
align-items: center;
}
&__legend {
flex: 1;
&-item {
position: relative;
width: 100%;
height: vh(40);
display: flex;
align-items: center;
margin-bottom: vh(8);
background: linear-gradient(
90deg,
rgba(0, 150, 255, 0.34) 0%,
rgba(0, 150, 255, 0) 100%
);
&::before {
position: absolute;
content: '';
width: vw(4);
height: vh(40);
background-color: #0096ff;
}
.dot {
width: vw(10);
height: vw(10);
margin: 0 vw(16);
}
.name {
font-weight: 400;
font-size: vw(12);
color: #ffffff;
width: vw(130);
}
.value {
font-weight: bold;
font-size: vw(15);
color: #ffffff;
}
}
}
}
.work-box-3 {
width: vw(840);
margin-top: vh(120);

View File

@@ -0,0 +1,164 @@
<template>
<div class="alarmRate" :id="id" />
</template>
<script setup>
import { fitChartSize } from '@/utils/dataUtil'
import { useEchart } from '@/hooks/echart'
const props = defineProps({
config: {
type: Object,
default: () => {
return {}
}
},
dataList: {
type: Array,
default: () => []
},
total: {
type: Number,
default: () => 0
},
colors: {
type: Array,
default: () => ['#FDC40A', '#FF5232', '#50F0A6', '#5FDFFA']
}
})
const { id, chart, setOption } = useEchart()
let params = null
let defaultCofig = {
color: [],
legend: {
orient: 'vertical',
y: 'center',
left: '50%',
itemWidth: fitChartSize(12),
itemHeight: fitChartSize(12),
itemGap: fitChartSize(6),
formatter: (name) => {
let obj = props.dataList.find((item) => item.name == name)
let total = getTotal();
if(total==0){
return `{name|${name}} {value|0}{value|%}`
}else{
let value = ((obj?.value/total).toFixed(4)*100).toFixed(2);
return `{name|${name}} {value|${value}}{value|%}`
}
},
textStyle: {
rich: {
name: {
color: '#fff',
fontSize: fitChartSize(12)
},
value: {
color: '#fff',
fontSize: fitChartSize(12)
}
}
}
},
series: [
{
type: 'pie',
center: ['24%', '50%'],
radius: ['35%', '50%'],
itemStyle: {
borderWidth: fitChartSize(4),
borderColor: '#093672'
},
label: {
show: true,
position: 'center',
fontWeight: 'bold',
rich: {
value: {
color: '#fff',
fontSize: fitChartSize(16),
fontWeight: 'bold',
padding: [0, 0, 5, 0]
},
name: {
color: '#fff',
fontSize: fitChartSize(12)
}
}
},
labelLine: {
show: false
},
data: []
}
]
}
let getTotal = () => {
return props.dataList.reduce((per, cur) => {
return per + cur.value
}, 0)
}
watch(
() => props.dataList,
() => {
if (props.dataList.length > 0) {
setTimeout(() => {
init()
}, 1000)
}
},
{ immediate: true }
)
const init = () => {
if (!params) {
defaultCofig.color = props.colors
defaultCofig.series[0].data = props.dataList
defaultCofig.series[0].label.formatter = () => {
return `{value|${getTotal()}}` + '\n' + `{name|消息总数}`
}
params = {
...defaultCofig,
...props.config
}
} else {
params.series[0].data = props.dataList
params.series[0].label.formatter = () => {
return `{value|${getTotal()}}` + '\n' + `{name|消息总数}`
}
}
const changeChart = setOption(params)
changeChart.off('legendselectchanged');
changeChart.on('legendselectchanged', function (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)
}
})
params.series[0].label.formatter = () => {
return `{value|${echartsNum}}` + '\n' + `{name|告警总数}`
}
setOption(params)
});
}
</script>
<style scoped lang="scss">
.alarmRate {
width: vw(380);
height: vh(180);
}
</style>

View File

@@ -33,7 +33,7 @@ export default defineConfig({
// target: 'http://localhost:63343/',
// changeOrigin: true
// },
// '/lqh': {
// '/map/lqh': {
// // 目标服务器的地址
// target: 'http://localhost:63343/',
// changeOrigin: true