实现大部分功能

This commit is contained in:
renna 2025-06-06 19:05:32 +08:00
parent 93db9389a2
commit 36aef50419
15 changed files with 2222 additions and 418 deletions

View File

@ -114,4 +114,7 @@ npm run build
"clientip": "{\"osName\":\"Windows\",\"osVersion\":\"10\",\"browserName\":\"Chrome\",\"browserVersion\":\"137.0.0.0\",\"SDK_VERSION\":\"1.0.9\"}",
"sdp": "v=0\r\no=...", // SDP内容
"streamurl": "webrtc://qvs-live.thirdmonitor.concoai.com:447/2xenzwqy2ppcl/31011500991180041301_34020000001320000001"
}
}
计划修改:
CarouselVideoPlayer修改逻辑 找不到流的时候 页面显示空状态 也进行轮播 而不是找不到匹配的视角或视角没有流,找第一个有效视角

View File

@ -1,4 +1,4 @@
import request, { concoaiService } from '@/utils/request'
import request, { concoaiService,service } from '@/utils/request'
// 机器人相关接口
export const robotApi = {
@ -28,7 +28,7 @@ export const robotApi = {
// 机器人详情
getRobotDetail: (robotId) => {
return concoaiService({
url: `/robot/detail/${robotId} `,
url: `/robot/detail/${robotId}`,
method: 'get',
})
},
@ -49,22 +49,59 @@ export const robotApi = {
})
},
// 一键处理事件接口
handleAlarmEvent: (data) => {
return concoaiService({
url: `/event/handleEventBatch`,
// handleAlarmEvent: (data) => {
// return concoaiService({
// url: `/event/handleEventBatch`,
// method: 'post',
// data
// })
// },
// 获取告警信息数量接口添加
getAlarmEventCount: (params) => {
return service({
url: `/api/v1/events/alertCount`,
method: 'get',
params
})
},
// 获取未处理告警消息列表
getUnhandledAlarmMessages: (params) => {
return service({
url: `/api/v1/events/messagesUnhandled`,
method: 'get',
params
})
},
// 获取已处理告警消息列表
getHandledAlarmMessages: (params) => {
return service({
url: `/api/v1/events/messagesHandled`,
method: 'get',
params
})
},
// 一键处理告警事件
handleOcrAlerts: (params) => {
return service({
url: `/api/v1/events/handleOcrAlerts`,
method: 'get',
params
})
},
// 处理单个告警事件
handleSingleAlarmEvent: (data) => {
return service({
url: `/api/v1/events/handleOcrAlert`,
method: 'post',
data
})
},
// 单个事件查看详情
getAlarmEventDetail: (eventId) => {
return concoaiService({
url: `/event/eventInfo/${eventId}`,
// 查看每一条消息的详情
getAlarmEventDetail: (messageId) => {
return service({
url: `/api/v1/events/alertFront/${messageId}`,
method: 'get',
})
}
}

View File

@ -63,5 +63,12 @@ export const homeApi = {
url: '/api/v1/events/getMonitor',
method: 'get',
})
},
//查看告警消息详情
getAlarmEventDetail:(messageId)=>{
return service({
url: `/api/v1/events/alertFront/${messageId}`,
method: 'get',
})
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -31,56 +31,56 @@
<!-- 告警列表 -->
<template v-else>
<!-- 避障警报 -->
<!-- 避障警报 -->
<div v-if="shouldShowAlarm('obstacle')" class="alarm-category">
<div class="category-header">
<div class="category-icon"></div>
<div class="category-name">避障警报</div>
</div>
<div class="category-header">
<div class="category-icon"></div>
<div class="category-name">避障警报</div>
</div>
<div v-for="(item, index) in filteredObstacleAlarms" :key="'obstacle-'+item.id+index" class="alarm-item" @click="showAlarmDetail(item)">
<div class="item-event">{{ item.event }}</div>
<div class="item-robot-id">{{ item.robotId }}</div>
<div class="item-time">{{ item.time }}</div>
<div class="item-status">
<div class="item-event">{{ item.event }}</div>
<div class="item-robot-id">{{ item.robotId }}</div>
<div class="item-time">{{ item.time }}</div>
<div class="item-status">
<button v-if="!item.handled" class="handle-btn" @click.stop="handleAlarm(item)">处理</button>
<span v-else class="handled-text">已处理</span>
</div>
<span v-else class="handle-btn">已处理</span>
</div>
</div>
<!-- 高温感知报警 -->
</div>
<!-- 高温感知报警 -->
<div v-if="shouldShowAlarm('temperature')" class="alarm-category">
<div class="category-header high-temp">
<div class="category-icon"></div>
<div class="category-name">高温感知报警</div>
</div>
<div class="category-header high-temp">
<div class="category-icon"></div>
<div class="category-name">高温感知报警</div>
</div>
<div v-for="(item, index) in filteredTemperatureAlarms" :key="'temp-'+item.id+index" class="alarm-item" @click="showAlarmDetail(item)">
<div class="item-event">{{ item.event }}</div>
<div class="item-robot-id">{{ item.robotId }}</div>
<div class="item-time">{{ item.time }}</div>
<div class="item-status">
<div class="item-event">{{ item.event }}</div>
<div class="item-robot-id">{{ item.robotId }}</div>
<div class="item-time">{{ item.time }}</div>
<div class="item-status">
<button v-if="!item.handled" class="handle-btn" @click.stop="handleAlarm(item)">处理</button>
<span v-else class="handled-text">已处理</span>
</div>
<span v-else class="handled-text">已处理</span>
</div>
</div>
<!-- 日常巡检 -->
</div>
<!-- 日常巡检 -->
<div v-if="shouldShowAlarm('routine')" class="alarm-category">
<div class="category-header">
<div class="category-icon"></div>
<div class="category-name">日常巡检</div>
</div>
<div class="category-header">
<div class="category-icon"></div>
<div class="category-name">日常巡检</div>
</div>
<div v-for="(item, index) in filteredRoutineAlarms" :key="'routine-'+item.id+index" class="alarm-item" @click="showAlarmDetail(item)">
<div class="item-event">{{ item.event }}</div>
<div class="item-robot-id">{{ item.robotId }}</div>
<div class="item-time">{{ item.time }}</div>
<div class="item-status">
<div class="item-event">{{ item.event }}</div>
<div class="item-robot-id">{{ item.robotId }}</div>
<div class="item-time">{{ item.time }}</div>
<div class="item-status">
<button v-if="!item.handled" class="handle-btn" @click.stop="handleAlarm(item)">处理</button>
<span v-else class="handled-text">已处理</span>
</div>
<span v-else class="handled-text">已处理</span>
</div>
</div>
</div>
</template>
</div>
@ -415,20 +415,72 @@ const stopScroll = () => {
const showModal = ref(false);
const currentAlarm = ref({});
const showAlarmDetail = (alarm) => {
currentAlarm.value = {
...alarm,
title: alarm.event, // 使
mainImage: alarm.image,
subImages: alarm.subImages || [], // 使API
type: eventTypeMap[alarm.type] || alarm.event, // 使
time: alarm.time,
robotName: alarm.robotId,
status: alarm.handled ? '已处理' : '未处理',
temperature: alarm.temperature || '',
messageId: alarm.id //
};
showModal.value = true;
const showAlarmDetail = async (alarm) => {
try {
// API
const res = await homeApi.getAlarmEventDetail(alarm.id);
if (res.code === 200 && res.data) {
console.log('获取告警详情成功:', res.data);
// API
const detailData = res.data;
currentAlarm.value = {
...alarm,
title: alarm.event, // 使
mainImage: alarm.image, // 使
subImages: alarm.subImages || [], // 使API
type: eventTypeMap[alarm.type] || alarm.event, // 使
time: alarm.time,
robotName: alarm.robotId,
status: alarm.handled ? '已处理' : '未处理',
temperature: alarm.temperature || '',
messageId: alarm.id, //
//
rawDetail: detailData
};
showModal.value = true;
} else {
console.error('获取告警详情失败:', res);
// API使
currentAlarm.value = {
...alarm,
title: alarm.event,
mainImage: alarm.image,
subImages: alarm.subImages || [],
type: eventTypeMap[alarm.type] || alarm.event,
time: alarm.time,
robotName: alarm.robotId,
status: alarm.handled ? '已处理' : '未处理',
temperature: alarm.temperature || '',
messageId: alarm.id
};
showModal.value = true;
}
} catch (err) {
console.error('获取告警详情错误:', err);
// 使
currentAlarm.value = {
...alarm,
title: alarm.event,
mainImage: alarm.image,
subImages: alarm.subImages || [],
type: eventTypeMap[alarm.type] || alarm.event,
time: alarm.time,
robotName: alarm.robotId,
status: alarm.handled ? '已处理' : '未处理',
temperature: alarm.temperature || '',
messageId: alarm.id
};
showModal.value = true;
}
};
const handleConfirm = async (data) => {

View File

@ -0,0 +1,379 @@
<template>
<div class="carousel-video-player">
<div class="video-container">
<!-- 视频播放器 -->
<VideoPlayer
v-if="currentStream"
:streamUrl="currentStream"
:title="title"
fillMode="stretch-height"
/>
<EmptyState
v-else
subtitle="暂无信息"
:iconSrc="empty"
class="video-empty-state"
/>
<!-- 左右箭头 -->
<div class="carousel-arrows" v-if="hasValidViews">
<div class="arrow left-arrow" @click="prevView">
<i class="arrow-icon left"></i>
</div>
<div class="arrow right-arrow" @click="nextView">
<i class="arrow-icon right"></i>
</div>
</div>
<!-- 视角指示器 -->
<div class="view-indicators" v-if="hasValidViews">
<div
v-for="(view, index) in validViews"
:key="index"
class="indicator"
:class="{ active: currentViewIndex === validViewsIndices[index] }"
@click="setView(validViewsIndices[index])"
></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import VideoPlayer from './VideoPlayer.vue';
import EmptyState from './EmptyState.vue';
import empty from '../../assets/img/empty.png';
const props = defineProps({
// [{ name: '1', streamUrl: 'url1' }, { name: '2', streamUrl: 'url2' }]
views: {
type: Array,
default: () => []
},
//
interval: {
type: Number,
default: 10000
},
//
title: {
type: String,
default: ''
},
//
autoplay: {
type: Boolean,
default: true
},
// v-model
modelValue: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:modelValue']);
//
const currentViewIndex = ref(0);
// streamUrl
const validViews = computed(() => {
return props.views.filter(view => view.streamUrl);
});
//
const validViewsIndices = computed(() => {
return props.views.map((view, index) => view.streamUrl ? index : -1).filter(index => index !== -1);
});
//
const hasValidViews = computed(() => {
return validViews.value.length > 0;
});
// URL
const currentStream = computed(() => {
if (props.views.length === 0) return '';
return props.views[currentViewIndex.value]?.streamUrl || '';
});
//
const viewList = computed(() => {
return props.views.map(view => view.name);
});
// modelValue
watch(() => props.modelValue, (newValue) => {
if (newValue && props.views.length > 0) {
const index = props.views.findIndex(view => view.name === newValue);
if (index !== -1) {
currentViewIndex.value = index;
}
}
}, { immediate: true });
// currentViewIndexmodelValue
watch(currentViewIndex, (newIndex) => {
if (props.views.length > 0 && newIndex >= 0 && newIndex < props.views.length) {
const viewName = props.views[newIndex].name;
if (viewName !== props.modelValue) {
emit('update:modelValue', viewName);
}
}
});
//
let carouselTimer = null;
//
const startCarousel = () => {
if (!props.autoplay || validViews.value.length <= 1) return;
stopCarousel(); //
carouselTimer = setInterval(() => {
nextView();
}, props.interval);
};
//
const stopCarousel = () => {
if (carouselTimer) {
clearInterval(carouselTimer);
carouselTimer = null;
}
};
//
const nextView = () => {
if (validViews.value.length <= 1) return;
//
let nextIndex = currentViewIndex.value;
do {
nextIndex = (nextIndex + 1) % props.views.length;
} while (!props.views[nextIndex].streamUrl && nextIndex !== currentViewIndex.value);
if (props.views[nextIndex].streamUrl) {
currentViewIndex.value = nextIndex;
restartCarousel();
}
};
//
const prevView = () => {
if (validViews.value.length <= 1) return;
//
let prevIndex = currentViewIndex.value;
do {
prevIndex = (prevIndex - 1 + props.views.length) % props.views.length;
} while (!props.views[prevIndex].streamUrl && prevIndex !== currentViewIndex.value);
if (props.views[prevIndex].streamUrl) {
currentViewIndex.value = prevIndex;
restartCarousel();
}
};
//
const setView = (index) => {
if (index >= 0 && index < props.views.length && props.views[index].streamUrl) {
currentViewIndex.value = index;
restartCarousel();
}
};
//
const restartCarousel = () => {
if (props.autoplay) {
stopCarousel();
startCarousel();
}
};
//
watch(() => props.views, () => {
//
if (props.modelValue && props.views.length > 0) {
const index = props.views.findIndex(view => view.name === props.modelValue);
if (index !== -1 && props.views[index].streamUrl) {
currentViewIndex.value = index;
} else {
//
const firstValidIndex = props.views.findIndex(view => view.streamUrl);
if (firstValidIndex !== -1) {
currentViewIndex.value = firstValidIndex;
emit('update:modelValue', props.views[firstValidIndex].name);
} else {
//
currentViewIndex.value = 0;
if (props.views.length > 0) {
emit('update:modelValue', props.views[0].name);
}
}
}
} else {
// modelValue
const firstValidIndex = props.views.findIndex(view => view.streamUrl);
if (firstValidIndex !== -1) {
currentViewIndex.value = firstValidIndex;
emit('update:modelValue', props.views[firstValidIndex].name);
} else {
//
currentViewIndex.value = 0;
if (props.views.length > 0) {
emit('update:modelValue', props.views[0].name);
}
}
}
restartCarousel();
}, { deep: true });
//
watch(() => props.autoplay, (newValue) => {
if (newValue) {
startCarousel();
} else {
stopCarousel();
}
});
//
onMounted(() => {
startCarousel();
});
//
onUnmounted(() => {
stopCarousel();
});
</script>
<style scoped>
.carousel-video-player {
width: 100%;
height: 100%;
position: relative;
}
.video-container {
width: 100%;
height: 100%;
position: relative;
}
/* 左右箭头样式 */
.carousel-arrows {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none; /* 避免干扰视频点击 */
}
.arrow {
width: 30px;
height: 30px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 0 10px;
pointer-events: auto; /* 恢复箭头点击 */
transition: opacity 0.3s;
opacity: 0;
}
.video-container:hover .arrow {
opacity: 0.8;
}
.arrow:hover {
opacity: 1;
background: rgba(0, 0, 0, 0.7);
}
.arrow-icon {
width: 10px;
height: 10px;
border-top: 2px solid #fff;
border-right: 2px solid #fff;
display: block;
}
.arrow-icon.left {
transform: rotate(-135deg);
margin-left: 2px;
}
.arrow-icon.right {
transform: rotate(45deg);
margin-right: 2px;
}
/* 视角指示器样式 */
.view-indicators {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 8px;
z-index: 10;
opacity: 0;
transition: opacity 0.3s;
}
.video-container:hover .view-indicators {
opacity: 1;
}
.indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s;
}
.indicator.active {
background: #fff;
transform: scale(1.2);
}
.video-empty-state {
background: #033347;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
:deep(.empty-icon) {
height: 46px !important;
}
:deep(.empty-title) {
display: none;
}
:deep(.empty-subtitle) {
color: #fff;
font-size: 10px;
letter-spacing: 1px;
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,372 @@
<template>
<div class="carousel-webrtc-player">
<div class="video-container">
<!-- WebRTC视频播放器 -->
<CustomWebRTCPlayer
v-if="currentStream"
:streamUrl="currentStream"
/>
<EmptyState
v-else
subtitle="暂无信息"
:iconSrc="empty"
class="video-empty-state"
/>
<!-- 左右箭头 -->
<div class="carousel-arrows" v-if="hasValidViews">
<div class="arrow left-arrow" @click="prevView">
<i class="arrow-icon left"></i>
</div>
<div class="arrow right-arrow" @click="nextView">
<i class="arrow-icon right"></i>
</div>
</div>
<!-- 视角指示器 -->
<div class="view-indicators" v-if="hasValidViews">
<div
v-for="(view, index) in validViews"
:key="index"
class="indicator"
:class="{ active: currentViewIndex === validViewsIndices[index] }"
@click="setView(validViewsIndices[index])"
></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
import CustomWebRTCPlayer from './CustomWebRTCPlayer.vue';
import EmptyState from './EmptyState.vue';
import empty from '../../assets/img/empty.png';
const props = defineProps({
// [{ name: '1', streamUrl: 'url1' }, { name: '2', streamUrl: 'url2' }]
views: {
type: Array,
default: () => []
},
//
interval: {
type: Number,
default: 10000
},
//
autoplay: {
type: Boolean,
default: true
},
// v-model
modelValue: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:modelValue']);
//
const currentViewIndex = ref(0);
// streamUrl
const validViews = computed(() => {
return props.views.filter(view => view.streamUrl);
});
//
const validViewsIndices = computed(() => {
return props.views.map((view, index) => view.streamUrl ? index : -1).filter(index => index !== -1);
});
//
const hasValidViews = computed(() => {
return validViews.value.length > 0;
});
// URL
const currentStream = computed(() => {
if (props.views.length === 0) return '';
return props.views[currentViewIndex.value]?.streamUrl || '';
});
//
const viewList = computed(() => {
return props.views.map(view => view.name);
});
// modelValue
watch(() => props.modelValue, (newValue) => {
if (newValue && props.views.length > 0) {
const index = props.views.findIndex(view => view.name === newValue);
if (index !== -1) {
currentViewIndex.value = index;
}
}
}, { immediate: true });
// currentViewIndexmodelValue
watch(currentViewIndex, (newIndex) => {
if (props.views.length > 0 && newIndex >= 0 && newIndex < props.views.length) {
const viewName = props.views[newIndex].name;
if (viewName !== props.modelValue) {
emit('update:modelValue', viewName);
}
}
});
//
let carouselTimer = null;
//
const startCarousel = () => {
if (!props.autoplay || validViews.value.length <= 1) return;
stopCarousel(); //
carouselTimer = setInterval(() => {
nextView();
}, props.interval);
};
//
const stopCarousel = () => {
if (carouselTimer) {
clearInterval(carouselTimer);
carouselTimer = null;
}
};
//
const nextView = () => {
if (validViews.value.length <= 1) return;
//
let nextIndex = currentViewIndex.value;
do {
nextIndex = (nextIndex + 1) % props.views.length;
} while (!props.views[nextIndex].streamUrl && nextIndex !== currentViewIndex.value);
if (props.views[nextIndex].streamUrl) {
currentViewIndex.value = nextIndex;
restartCarousel();
}
};
//
const prevView = () => {
if (validViews.value.length <= 1) return;
//
let prevIndex = currentViewIndex.value;
do {
prevIndex = (prevIndex - 1 + props.views.length) % props.views.length;
} while (!props.views[prevIndex].streamUrl && prevIndex !== currentViewIndex.value);
if (props.views[prevIndex].streamUrl) {
currentViewIndex.value = prevIndex;
restartCarousel();
}
};
//
const setView = (index) => {
if (index >= 0 && index < props.views.length && props.views[index].streamUrl) {
currentViewIndex.value = index;
restartCarousel();
}
};
//
const restartCarousel = () => {
if (props.autoplay) {
stopCarousel();
startCarousel();
}
};
//
watch(() => props.views, () => {
//
if (props.modelValue && props.views.length > 0) {
const index = props.views.findIndex(view => view.name === props.modelValue);
if (index !== -1 && props.views[index].streamUrl) {
currentViewIndex.value = index;
} else {
//
const firstValidIndex = props.views.findIndex(view => view.streamUrl);
if (firstValidIndex !== -1) {
currentViewIndex.value = firstValidIndex;
emit('update:modelValue', props.views[firstValidIndex].name);
} else {
//
currentViewIndex.value = 0;
if (props.views.length > 0) {
emit('update:modelValue', props.views[0].name);
}
}
}
} else {
// modelValue
const firstValidIndex = props.views.findIndex(view => view.streamUrl);
if (firstValidIndex !== -1) {
currentViewIndex.value = firstValidIndex;
emit('update:modelValue', props.views[firstValidIndex].name);
} else {
//
currentViewIndex.value = 0;
if (props.views.length > 0) {
emit('update:modelValue', props.views[0].name);
}
}
}
restartCarousel();
}, { deep: true });
//
watch(() => props.autoplay, (newValue) => {
if (newValue) {
startCarousel();
} else {
stopCarousel();
}
});
//
onMounted(() => {
startCarousel();
});
//
onUnmounted(() => {
stopCarousel();
});
</script>
<style scoped>
.carousel-webrtc-player {
width: 100%;
height: 100%;
position: relative;
}
.video-container {
width: 100%;
height: 100%;
position: relative;
}
/* 左右箭头样式 */
.carousel-arrows {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: center;
pointer-events: none; /* 避免干扰视频点击 */
}
.arrow {
width: 30px;
height: 30px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin: 0 10px;
pointer-events: auto; /* 恢复箭头点击 */
transition: opacity 0.3s;
opacity: 0;
}
.video-container:hover .arrow {
opacity: 0.8;
}
.arrow:hover {
opacity: 1;
background: rgba(0, 0, 0, 0.7);
}
.arrow-icon {
width: 10px;
height: 10px;
border-top: 2px solid #fff;
border-right: 2px solid #fff;
display: block;
}
.arrow-icon.left {
transform: rotate(-135deg);
margin-left: 2px;
}
.arrow-icon.right {
transform: rotate(45deg);
margin-right: 2px;
}
/* 视角指示器样式 */
.view-indicators {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 8px;
z-index: 10;
opacity: 0;
transition: opacity 0.3s;
}
.video-container:hover .view-indicators {
opacity: 1;
}
.indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.3s;
}
.indicator.active {
background: #fff;
transform: scale(1.2);
}
.video-empty-state {
background: #033347;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
:deep(.empty-icon) {
height: 46px !important;
}
:deep(.empty-title) {
display: none;
}
:deep(.empty-subtitle) {
color: #fff;
font-size: 10px;
letter-spacing: 1px;
margin-top: 5px;
}
</style>

View File

@ -1,28 +1,31 @@
<template>
<div class="webrtc-player">
<EmptyState
v-if="!streamUrl"
subtitle="暂无信息"
:iconSrc="empty"
class="video-empty-state"
/>
<div v-if="!streamUrl" class="empty-state">
<EmptyState
subtitle="暂无信息"
:iconSrc="empty"
class="video-empty-state"
/>
</div>
<div v-else class="player-container">
<video
ref="videoRef"
autoplay
muted
:class="['video-element', fillModeClass]"
class="video-element"
></video>
<div v-if="isLoading" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { ref, onMounted, onUnmounted, watch } from 'vue';
import 'webrtc-adapter';
import EmptyState from './EmptyState.vue';
import empty from '../../assets/img/empty.png';
@ -31,45 +34,36 @@ const props = defineProps({
type: String,
default: ''
},
title: {
type: String,
default: ''
},
fillMode: {
type: String,
default: 'fill', // 'fill', 'cover' 'stretch-height'
validator: (value) => ['fill', 'cover', 'stretch-height'].includes(value)
},
clientIp: {
type: String,
default: '127.0.0.1'
default: JSON.stringify({
osName: 'Windows',
osVersion: '10',
browserName: 'Chrome',
browserVersion: navigator.userAgent.match(/Chrome\/([0-9.]+)/)?.[1] || 'Unknown',
SDK_VERSION: '1.0.0'
})
}
});
const videoRef = ref(null);
const isLoading = ref(false);
const error = ref('');
const pc = ref(null); // WebRTC
// CSS
const fillModeClass = computed(() => {
switch (props.fillMode) {
case 'fill': return 'video-fill';
case 'cover': return 'video-cover';
case 'stretch-height': return 'video-stretch-height';
default: return 'video-fill';
}
});
// WebRTC
const initWebRTC = async () => {
if (!props.streamUrl || !videoRef.value) return;
try {
isLoading.value = true;
error.value = '';
//
cleanupWebRTC();
console.log('初始化WebRTC连接...');
// RTCPeerConnection
pc.value = new RTCPeerConnection({
iceServers: [
@ -81,48 +75,103 @@ const initWebRTC = async () => {
pc.value.onicecandidate = (event) => {
if (event.candidate) {
console.log('ICE候选:', event.candidate);
} else {
console.log('ICE收集完成');
}
};
//
pc.value.onconnectionstatechange = () => {
console.log('连接状态:', pc.value.connectionState);
if (pc.value.connectionState === 'connected') {
console.log('WebRTC连接成功');
}
};
// ICE
pc.value.oniceconnectionstatechange = () => {
console.log('ICE连接状态:', pc.value.iceConnectionState);
//
if (pc.value.iceConnectionState === 'failed' || pc.value.iceConnectionState === 'disconnected') {
console.log('WebRTC连接失败尝试重新连接...');
setTimeout(() => {
initWebRTC();
}, 2000);
if (pc.value.iceConnectionState === 'checking') {
console.log('正在检查ICE连接...');
} else if (pc.value.iceConnectionState === 'connected') {
console.log('ICE连接成功');
} else if (pc.value.iceConnectionState === 'completed') {
console.log('ICE连接完成');
} else if (pc.value.iceConnectionState === 'failed' || pc.value.iceConnectionState === 'disconnected') {
console.error('ICE连接失败或断开');
error.value = 'ICE连接失败请检查网络';
//
if (pc.value) {
console.log('WebRTC连接失败尝试重新连接...');
setTimeout(() => {
initWebRTC();
}, 2000);
}
}
};
//
pc.value.addTransceiver('audio', { direction: 'recvonly' });
pc.value.addTransceiver('video', { direction: 'recvonly' });
//
pc.value.ontrack = (event) => {
console.log('收到媒体轨道:', event.track.kind);
if (videoRef.value && event.streams && event.streams[0]) {
console.log('设置视频源:', event.streams[0]);
videoRef.value.srcObject = event.streams[0];
isLoading.value = false;
console.log(`WebRTC流 ${props.title} 连接成功`);
//
videoRef.value.onloadedmetadata = () => {
console.log('视频元数据加载完成');
videoRef.value.play().then(() => {
console.log('视频开始播放');
isLoading.value = false;
}).catch(err => {
console.error('视频播放失败:', err);
error.value = '视频播放失败: ' + err.message;
isLoading.value = false;
});
};
videoRef.value.onerror = (e) => {
console.error('视频错误:', e);
error.value = '视频错误: ' + (e.message || '未知错误');
isLoading.value = false;
};
}
};
// offer
const offer = await pc.value.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true
});
console.log('创建SDP offer...');
const offer = await pc.value.createOffer();
console.log('Offer SDP:', offer.sdp);
//
await pc.value.setLocalDescription(offer);
console.log('本地描述设置完成');
// API URL
// API URL
const apiUrl = 'https://qvs-live.thirdmonitor.concoai.com:447/rtc/v1/play';
// streamUrl
let streamUrlValue = props.streamUrl;
// streamUrlWebRTC URL
if (!streamUrlValue.startsWith('webrtc://')) {
// URLURL
streamUrlValue = `webrtc://qvs-live.thirdmonitor.concoai.com:447/2xenzwqy2ppcl/${streamUrlValue}`;
}
console.log('发送WebRTC请求:', {
url: apiUrl,
streamUrl: streamUrlValue
});
// offeranswer
const response = await fetch(apiUrl, {
method: 'POST',
@ -132,7 +181,7 @@ const initWebRTC = async () => {
body: JSON.stringify({
clientip: props.clientIp,
sdp: pc.value.localDescription.sdp,
streamurl: props.streamUrl
streamurl: streamUrlValue
})
});
@ -146,22 +195,37 @@ const initWebRTC = async () => {
throw new Error('服务器未返回有效SDP');
}
console.log('收到WebRTC应答:', data);
// SDP
if (!data.sdp.includes('m=video')) {
console.warn('警告: 返回的SDP中没有视频媒体行');
}
if (!data.sdp.includes('m=audio')) {
console.warn('警告: 返回的SDP中没有音频媒体行');
}
//
console.log('设置远程描述...');
await pc.value.setRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp: data.sdp
}));
console.log('远程描述设置完成');
//
setTimeout(() => {
if (isLoading.value) {
isLoading.value = false;
console.error('WebRTC连接超时');
error.value = 'WebRTC连接超时请刷新重试';
isLoading.value = false;
}
}, 10000);
}, 15000);
} catch (err) {
console.error('WebRTC连接错误:', err);
error.value = '连接错误: ' + err.message;
isLoading.value = false;
}
};
@ -169,33 +233,42 @@ const initWebRTC = async () => {
// WebRTC
const cleanupWebRTC = () => {
if (pc.value) {
console.log('关闭WebRTC连接');
pc.value.close();
pc.value = null;
}
if (videoRef.value) {
if (videoRef.value && videoRef.value.srcObject) {
console.log('停止所有媒体轨道');
const tracks = videoRef.value.srcObject.getTracks();
tracks.forEach(track => track.stop());
videoRef.value.srcObject = null;
}
isLoading.value = false;
error.value = '';
};
// URL
watch(() => props.streamUrl, (newUrl) => {
if (newUrl) {
console.log('流URL变化重新初始化WebRTC:', newUrl);
initWebRTC();
} else {
console.log('流URL为空清理WebRTC连接');
cleanupWebRTC();
}
});
}, { immediate: true });
onMounted(() => {
console.log('CustomWebRTCPlayer组件挂载');
if (props.streamUrl) {
initWebRTC();
}
});
onUnmounted(() => {
console.log('CustomWebRTCPlayer组件卸载');
cleanupWebRTC();
});
</script>
@ -208,8 +281,42 @@ onUnmounted(() => {
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
}
.empty-state {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.5);
font-size: 14px;
}
.video-empty-state {
background: #033347;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
:deep(.empty-icon) {
height: 46px !important;
}
:deep(.empty-title) {
display: none;
}
:deep(.empty-subtitle) {
color: #fff;
font-size: 10px;
letter-spacing: 1px;
margin-top: 5px;
}
.player-container {
@ -221,25 +328,7 @@ onUnmounted(() => {
.video-element {
width: 100%;
height: 100%;
}
.video-fill {
object-fit: fill; /* 拉伸填充,可能会变形 */
}
.video-cover {
object-fit: cover; /* 保持比例填充,可能会裁剪 */
}
.video-stretch-height {
object-fit: none; /* 不调整大小 */
width: 100%;
height: auto !important; /* 自动调整高度 */
min-height: 100%; /* 确保至少填满容器高度 */
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%); /* 垂直居中 */
object-fit: cover;
}
.loading-overlay {
@ -248,51 +337,44 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 100%;
background: rgba(3, 51, 71, 0.7);
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
justify-content: center;
color: white;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(42, 185, 208, 0.3);
border-top: 3px solid #2AB9D0;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #00a8ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
color: #B9E8FF;
font-size: 14px;
color: #fff;
}
.error-message {
position: absolute;
bottom: 10px;
left: 10px;
right: 10px;
background: rgba(255, 0, 0, 0.7);
color: white;
padding: 8px;
border-radius: 4px;
font-size: 12px;
text-align: center;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.video-empty-state {
padding-top: 30px;
background: #033347;
}
:deep(.empty-icon) {
width: 100px;
height: 74px;
}
:deep(.empty-title) {
display: none;
}
:deep(.empty-subtitle) {
color: #fff;
font-size: 10px;
letter-spacing: 1px;
}
</style>

View File

@ -1,8 +1,8 @@
<template>
<div class="empty-state">
<div class="empty-icon" :style="{ backgroundImage: `url(${iconSrc})` }"></div>
<div class="empty-title" v-if="title">{{ title }}</div>
<div class="empty-subtitle" v-if="subtitle">{{ subtitle }}</div>
<div v-if="title && title.trim()" class="empty-title">{{ title }}</div>
<div v-if="subtitle" class="empty-subtitle" :class="{ 'no-title': !title || !title.trim() }">{{ subtitle }}</div>
</div>
</template>
@ -15,11 +15,11 @@ defineProps({
},
title: {
type: String,
default: '暂无告警事件'
default: ''
},
subtitle: {
type: String,
default: '所有告警已处理完成'
default: ''
}
});
</script>
@ -40,7 +40,7 @@ defineProps({
height: 80px;
background-repeat: no-repeat;
background-size: 100% 100%;
margin-bottom: 15px;
margin-bottom: 10px;
}
.empty-title {
@ -48,10 +48,15 @@ defineProps({
font-size: 20px;
font-weight: bold;
margin-bottom: 5px;
min-height: 20px;
}
.empty-subtitle {
color: #2AB9D0;
font-size: 16px;
}
.empty-subtitle.no-title {
margin-top: 0;
}
</style>

View File

@ -354,7 +354,7 @@ onUnmounted(() => {
}
:deep(.empty-icon) {
width: 100px;
/* width: 100px; */
height: 74px;
}

View File

@ -3,13 +3,16 @@
<div class="modal-content">
<!-- 标题 -->
<div class="modal-header">
<span class="title">{{ alarmData.title || '告警详情' }}</span>
<img src="../../assets/img/close.png" class="close-icon" @click="handleClose" />
<span class="title">{{ alarmData.title || "告警详情" }}</span>
<img
src="../../assets/img/close.png"
class="close-icon"
@click="handleClose"
/>
</div>
<!-- 左侧图片区域 -->
<div class="modal-body">
<div class="image-section">
<TitleBlock title="监控视图">监控视图</TitleBlock>
<div class="images-container">
@ -17,7 +20,28 @@
<div class="camera-wrapper">
<div class="camera-title">主监控</div>
<div class="camera-view">
<img :src="alarmData.mainImage" alt="" onerror="this.style.display='none'" />
<template v-if="alarmData.mainImage">
<img
:src="alarmData.mainImage"
alt=""
@error="imgLoadError.main = true"
v-show="!imgLoadError.main"
/>
<EmptyState
v-if="imgLoadError.main"
subtitle="暂无信息"
title=""
iconSrc="empty"
class="video-empty-state"
/>
</template>
<EmptyState
v-else
subtitle="暂无信息"
title=""
:iconSrc="empty"
class="video-empty-state"
/>
</div>
</div>
</div>
@ -26,7 +50,32 @@
<div class="camera-wrapper">
<div class="camera-title">云台监控</div>
<div class="camera-view">
<img :src="alarmData.subImages[0]" alt="" onerror="this.style.display='none'" />
<template
v-if="
alarmData.subImages && alarmData.subImages.length > 0
"
>
<img
:src="alarmData.subImages[0]"
alt=""
@error="imgLoadError.sub1 = true"
v-show="!imgLoadError.sub1"
/>
<EmptyState
v-if="imgLoadError.sub1"
subtitle="暂无信息"
title=""
:iconSrc="empty"
class="video-empty-state"
/>
</template>
<EmptyState
v-else
subtitle="暂无信息"
title=""
:iconSrc="empty"
class="video-empty-state"
/>
</div>
</div>
</div>
@ -34,7 +83,32 @@
<div class="camera-wrapper">
<div class="camera-title">热成像</div>
<div class="camera-view">
<img :src="alarmData.subImages[1]" alt="" onerror="this.style.display='none'" />
<template
v-if="
alarmData.subImages && alarmData.subImages.length > 1
"
>
<img
:src="alarmData.subImages[1]"
alt=""
@error="imgLoadError.sub2 = true"
v-show="!imgLoadError.sub2"
/>
<EmptyState
v-if="imgLoadError.sub2"
title=""
subtitle="暂无信息"
:iconSrc="empty"
class="video-empty-state"
/>
</template>
<EmptyState
v-else
title=""
subtitle="暂无信息"
:iconSrc="empty"
class="video-empty-state"
/>
</div>
</div>
</div>
@ -61,7 +135,13 @@
</div>
<div class="info-item">
<p class="label">状态:</p>
<p class="value" :class="alarmData.status">{{ alarmData.status }}</p>
<p class="value" :class="alarmData.status">
{{ alarmData.status }}
</p>
</div>
<div class="info-item" v-if="alarmData.temperature">
<p class="label">温度:</p>
<p class="value">{{ alarmData.temperature }}</p>
</div>
</div>
</div>
@ -69,7 +149,13 @@
<div class="info-group">
<TitleBlock title="备注">备注</TitleBlock>
<div class="remark-content">
<div class="remark-box" contenteditable="true" data-placeholder="请输入备注信息......" @input="updateRemark"></div>
<div
class="remark-box"
contenteditable="true"
data-placeholder="请输入备注信息......"
@input="updateRemark"
v-html="alarmData.remark || ''"
></div>
</div>
</div>
</div>
@ -86,46 +172,94 @@
</template>
<script setup>
import { ref } from 'vue';
import TitleBlock from '../common/TitleBlock.vue';
import { ref, onMounted, watch } from "vue";
import TitleBlock from "../common/TitleBlock.vue";
import EmptyState from "../common/EmptyState.vue";
import empty from "../../assets/img/empty.png";
const props = defineProps({
visible: {
type: Boolean,
default: false
default: false,
},
alarmData: {
type: Object,
default: () => ({
title: '',
mainImage: '',
title: "",
mainImage: "",
subImages: [],
type: '',
time: '',
robotName: '',
status: ''
})
type: "",
time: "",
robotName: "",
status: "",
messageId: "",
remark: "",
}),
},
});
//
const imgLoadError = ref({
main: false,
sub1: false,
sub2: false,
});
//
const resetImgLoadError = () => {
imgLoadError.value = {
main: false,
sub1: false,
sub2: false,
};
};
//
watch(
() => props.visible,
(newValue) => {
if (newValue) {
//
resetImgLoadError();
}
}
);
//
onMounted(() => {
if (props.visible) {
resetImgLoadError();
}
});
const emit = defineEmits(['update:visible', 'confirm', 'report']);
const emit = defineEmits(["update:visible", "confirm", "report"]);
const remarkText = ref('');
const remarkText = ref("");
// alarmDataremark
watch(
() => props.alarmData.remark,
(newValue) => {
remarkText.value = newValue || "";
},
{ immediate: true }
);
const handleClose = () => {
emit('update:visible', false);
emit("update:visible", false);
};
const handleConfirm = () => {
emit('confirm', {
emit("confirm", {
...props.alarmData,
remark: remarkText.value
remark: remarkText.value,
});
};
const handleReport = () => {
emit('report', {
emit("report", {
...props.alarmData,
remark: remarkText.value
remark: remarkText.value,
});
};
@ -158,16 +292,15 @@ const updateRemark = (e) => {
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
color: #B9E8FF;
color: #b9e8ff;
font-size: 28px;
letter-spacing: 4px;
padding-left: 20px;
@ -184,7 +317,6 @@ const updateRemark = (e) => {
display: flex;
gap: 20px;
/* padding-top: 20px; */
}
.image-section {
@ -195,7 +327,7 @@ const updateRemark = (e) => {
/* margin-top: 20px; */
gap: 10px;
border-radius: 4px;
border:1px solid rgba(0,206,234,0.7);
border: 1px solid rgba(0, 206, 234, 0.7);
}
.images-container {
@ -214,7 +346,7 @@ const updateRemark = (e) => {
min-height: 240px;
/* border: 1px solid rgba(0,206,234,0.7); */
border-radius: 4px;
/* background: rgba(0, 21, 31, 0.3); */
background: #033347; /* 设置底部矩形的背景色 */
}
.sub-cameras {
@ -236,10 +368,10 @@ const updateRemark = (e) => {
top: 0px;
left: 0px;
z-index: 1;
color: #B9E8FF;
color: #b9e8ff;
font-size: 12px;
padding: 4px 8px;
background: rgba(0, 21, 31, 0.2);
background: rgba(0, 21, 31, 0.5);
border-radius: 2px;
text-align: left;
}
@ -251,12 +383,15 @@ const updateRemark = (e) => {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.camera-view img {
width: 100%;
height: 100%;
object-fit: cover;
/* object-fit: cover; */
}
.info-section {
@ -268,15 +403,14 @@ const updateRemark = (e) => {
}
.info-group {
background: rgba(0, 21, 31, 0.5);
border-radius: 4px;
border:1px solid rgba(0,206,234,0.7);
border: 1px solid rgba(0, 206, 234, 0.7);
}
.info-group h3 {
color: #B9E8FF;
color: #b9e8ff;
font-size: 14px;
margin-bottom: 15px;
}
@ -293,8 +427,8 @@ const updateRemark = (e) => {
align-items: center;
justify-content: space-between;
/* background: rgba(0, 21, 31, 0.2); */
border-bottom: 1px solid #244C60;
padding:10px;
border-bottom: 1px solid #244c60;
padding: 10px;
/* padding: 15px 20px; */
}
@ -303,13 +437,13 @@ const updateRemark = (e) => {
}
.info-item .label {
color: #B9E8FF;
color: #b9e8ff;
font-size: 14px;
margin: 0;
}
.info-item .label:before{
content: '';
.info-item .label:before {
content: "";
display: inline-block;
width: 10px;
height: 10px;
@ -319,18 +453,18 @@ const updateRemark = (e) => {
}
.info-item .value {
color: #B9E8FF;
color: #b9e8ff;
font-size: 14px;
margin: 0;
text-align: right;
}
.info-item .value.未处理 {
color: #F33F3F;
color: #f33f3f;
}
.info-item .value.已处理 {
color: #00FF84;
color: #00ff84;
}
.remark-content {
@ -345,13 +479,12 @@ const updateRemark = (e) => {
min-height: 130px;
background: none;
border-radius: 4px;
color: #B9E8FF;
color: #b9e8ff;
padding: 10px;
outline: none;
}
.remark-box:empty:before {
content: attr(data-placeholder);
color: rgba(185, 232, 255, 0.4);
}
@ -373,20 +506,40 @@ const updateRemark = (e) => {
}
.btn.cancel {
background: url("../../assets/img/cancel.png") no-repeat ;
background: url("../../assets/img/cancel.png") no-repeat;
background-size: 100% 100%;
color: #C6F4FF;
color: #c6f4ff;
}
.btn.confirm {
background: url("../../assets/img/confirm.png") no-repeat ;
background: url("../../assets/img/confirm.png") no-repeat;
background-size: 100% 100%;
color: #C6F4FF;
color: #c6f4ff;
}
.btn.report {
background: url("../../assets/img/report.png") no-repeat ;
background: url("../../assets/img/report.png") no-repeat;
background-size: 100% 100%;
color: #C6F4FF;
color: #c6f4ff;
}
</style>
.video-empty-state {
padding-top: 30px;
background: #033347;
}
:deep(.empty-icon) {
/* width: 100px; */
height: 46px !important;
}
:deep(.empty-title) {
display: none;
}
:deep(.empty-subtitle) {
color: #fff;
font-size: 10px;
letter-spacing: 1px;
margin-top: 5px;
}
</style>

View File

@ -15,7 +15,21 @@
<div class="main-monitor">
<div class="monitor-title">主监控</div>
<div class="monitor-view">
<img :src="eventData.mainImage" alt="" />
<template v-if="eventData.mainImage">
<img :src="eventData.mainImage" alt="" @error="imgLoadError.main = true" v-show="!imgLoadError.main" />
<EmptyState
v-if="imgLoadError.main"
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</template>
<EmptyState
v-else
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</div>
</div>
@ -24,13 +38,41 @@
<div class="monitor-item">
<div class="monitor-title">云台监控</div>
<div class="monitor-view">
<img :src="eventData.subImages[0]" alt="" />
<template v-if="eventData.subImages && eventData.subImages.length > 0">
<img :src="eventData.subImages[0]" alt="" @error="imgLoadError.sub1 = true" v-show="!imgLoadError.sub1" />
<EmptyState
v-if="imgLoadError.sub1"
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</template>
<EmptyState
v-else
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</div>
</div>
<div class="monitor-item">
<div class="monitor-title">热成像</div>
<div class="monitor-view">
<img :src="eventData.subImages[1]" alt="" />
<template v-if="eventData.subImages && eventData.subImages.length > 1">
<img :src="eventData.subImages[1]" alt="" @error="imgLoadError.sub2 = true" v-show="!imgLoadError.sub2" />
<EmptyState
v-if="imgLoadError.sub2"
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</template>
<EmptyState
v-else
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</div>
</div>
</div>
@ -72,7 +114,21 @@
<div v-for="(monitor, index) in monitorList" :key="index" class="monitor-item">
<div class="monitor-title">{{ monitor.title }}</div>
<div class="monitor-view">
<img :src="monitor.image" alt="" />
<template v-if="monitor.image">
<img :src="monitor.image" alt="" @error="setMonitorError(index)" v-show="!monitorErrors[index]" />
<EmptyState
v-if="monitorErrors[index]"
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</template>
<EmptyState
v-else
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
/>
</div>
</div>
</div>
@ -90,8 +146,10 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted, watch } from 'vue';
import TitleBlock from '../common/TitleBlock.vue';
import EmptyState from '../common/EmptyState.vue';
import empty from '../../assets/img/empty.png';
const props = defineProps({
visible: {
@ -115,6 +173,55 @@ const props = defineProps({
}
});
//
const imgLoadError = ref({
main: false,
sub1: false,
sub2: false
});
//
const monitorErrors = ref([]);
//
const setMonitorError = (index) => {
if (monitorErrors.value.length <= index) {
//
for (let i = monitorErrors.value.length; i <= index; i++) {
monitorErrors.value.push(false);
}
}
monitorErrors.value[index] = true;
};
//
const resetImgLoadError = () => {
imgLoadError.value = {
main: false,
sub1: false,
sub2: false
};
monitorErrors.value = [];
};
//
watch(
() => props.visible,
(newValue) => {
if (newValue) {
//
resetImgLoadError();
}
}
);
//
onMounted(() => {
if (props.visible) {
resetImgLoadError();
}
});
const emit = defineEmits(['update:visible', 'confirm', 'report']);
const remarkText = ref('');
@ -216,6 +323,7 @@ const handleReport = () => {
border: 1px solid rgba(0,206,234,0.7);
border-radius: 4px;
overflow: hidden;
background: #033347; /* 设置底部矩形的背景色 */
}
.main-monitor {
@ -238,6 +346,9 @@ const handleReport = () => {
.monitor-view {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.monitor-view img {
@ -381,4 +492,25 @@ padding:10px;
background: url("../../assets/img/report.png") no-repeat;
background-size: 100% 100%;
}
.video-empty-state {
padding-top: 30px;
background: #033347;
}
:deep(.empty-icon) {
/* width: 100px; */
height: 74px;
}
:deep(.empty-title) {
display: none;
}
:deep(.empty-subtitle) {
color: #fff;
font-size: 10px;
letter-spacing: 1px;
margin-top: 5px;
}
</style>

View File

@ -48,16 +48,8 @@
<div class="bottom_content">
<TitleBlock title="厂区及机器人实时监控">厂区及机器人实时监控</TitleBlock>
<div class="monitor-content">
<div v-if="displayCameras.length === 0" class="empty-state">
<div class="empty-icon">
<!-- <svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30 10C31.3 10 32.4 11.1 32.4 12.4V15.2C35.5 15.9 38.4 17.4 40.6 19.6C43.5 22.5 45 26.4 45 30.5C45 38.9 38.4 45.5 30 45.5C21.6 45.5 15 38.9 15 30.5C15 22.7 20.6 16.4 28 15.3V12.4C28 11.1 29.1 10 30.4 10H30ZM30 19C24.5 19 20 23.5 20 29C20 34.5 24.5 39 30 39C35.5 39 40 34.5 40 29C40 23.5 35.5 19 30 19ZM25.5 25.5C26.3 25.5 27 26.2 27 27V31C27 31.8 26.3 32.5 25.5 32.5C24.7 32.5 24 31.8 24 31V27C24 26.2 24.7 25.5 25.5 25.5ZM34.5 25.5C35.3 25.5 36 26.2 36 27V31C36 31.8 35.3 32.5 34.5 32.5C33.7 32.5 33 31.8 33 31V27C33 26.2 33.7 25.5 34.5 25.5Z" fill="#B9E8FF" fill-opacity="0.5"/>
</svg> -->
</div>
<div class="empty-text">暂无信息</div>
</div>
<div v-else class="camera-grid">
<div v-for="(camera, index) in displayCameras" :key="index" class="camera-item">
<div class="camera-grid">
<div v-for="(camera, index) in cameras" :key="index" class="camera-item">
<div class="camera-header">
<div class="camera-title">
<img :src="camera.src" alt="摄像头" class="camera-icon" />
@ -67,12 +59,17 @@
v-model="selectedViews[camera.title]"
:options="viewOptions"
size="small"
variant="search"
font="small"
variant="search"
font="small"
@change="handleViewChange(camera.title)"
/>
</div>
<div class="camera-feed">
<img />
<VideoPlayer
:streamUrl="camera.streamUrl"
:title="camera.title"
fillMode="stretch-height"
/>
</div>
</div>
</div>
@ -85,7 +82,7 @@
</template>
<script setup>
import { onMounted, ref, computed } from "vue";
import { onMounted, ref, computed, watch } from "vue";
import TopHeader from '../components/common/TopHeader.vue'
import TitleBlock from '../components/common/TitleBlock.vue'
import StatisticCard from '../components/StatisticCard.vue'
@ -94,6 +91,7 @@ import AlarmStatistics from '../components/AlarmStatistics.vue'
import LatestAlarms from '../components/LatestAlarms.vue'
import RobotListModal from '../components/dialog/RobotListModal.vue'
import CustomSelect from '../components/common/CustomSelect.vue'
import VideoPlayer from '../components/common/VideoPlayer.vue'
import { homeApi } from '../api/index'
import Icon1 from '../assets/img/icon1.png'
import Icon2 from '../assets/img/icon2.png'
@ -164,20 +162,67 @@ const fetchRobotStatistics = async () => {
};
const cameras = ref([
{ title: '厂区大门', src: jkA },
{ title: '生产车间', src: jkA },
{ title: '仓库区域', src: jkRobot },
{ title: '室外区域', src: jkRobot }
{ title: 'A区厂区监控', src: jkA, streamUrl: '' },
{ title: 'B区厂区监控', src: jkA, streamUrl: '' },
{ title: '追随机器人监控', src: jkRobot, streamUrl: '' },
{ title: '室外机器人监控', src: jkRobot, streamUrl: '' }
]);
const selectedViews = ref({
'厂区大门': '视角1',
'生产车间': '视角1',
'仓库区域': '视角1',
'室外区域': '视角1'
'A区厂区监控': '视角1',
'B区厂区监控': '视角1',
'追随机器人监控': '视角1',
'室外机器人监控': '视角1'
});
const viewOptions = ['视角1', '视角2', '视角3'];
const viewOptions = ['视角1', '视角2'];
//
const monitorStreams = ref({});
//
const fetchMonitorStreams = async () => {
try {
const res = await homeApi.getFactoryRobotRealTime();
if (res.code === 200) {
monitorStreams.value = res.data || {};
console.log('获取监控视频流成功:', JSON.stringify(monitorStreams.value));
// URL
updateCameraStreams();
} else {
console.error('获取监控视频流失败:', res);
}
} catch (err) {
console.error('获取监控视频流错误:', err);
}
};
// URL
const updateCameraStreams = () => {
cameras.value.forEach(camera => {
const cameraData = monitorStreams.value[camera.title];
console.log(`处理摄像头 ${camera.title}:`, cameraData);
if (cameraData) {
const viewName = selectedViews.value[camera.title];
const newStreamUrl = cameraData[viewName] || '';
console.log(`设置 ${camera.title}${viewName} 流URL:`, newStreamUrl);
camera.streamUrl = newStreamUrl;
} else {
console.warn(`未找到 ${camera.title} 的数据`);
}
});
//
console.log('更新后的摄像头数据:', JSON.stringify(cameras.value));
};
//
watch(selectedViews, (newViews) => {
updateCameraStreams();
}, { deep: true });
const showRobotListModal = ref(false);
@ -187,17 +232,23 @@ const handleStatisticClick = (item) => {
}
};
//
const handleViewChange = (cameraTitle) => {
const camera = cameras.value.find(c => c.title === cameraTitle);
if (camera) {
const cameraData = monitorStreams.value[cameraTitle];
if (cameraData) {
const viewName = selectedViews.value[cameraTitle];
camera.streamUrl = cameraData[viewName] || '';
console.log(`切换 ${cameraTitle}${viewName}, URL: ${camera.streamUrl}`);
}
}
};
//
onMounted(() => {
fetchRobotStatistics();
});
// true false
const showEmptyState = ref(false);
//
const displayCameras = computed(() => {
return showEmptyState.value ? [] : cameras.value;
fetchMonitorStreams();
});
</script>
@ -361,6 +412,7 @@ const displayCameras = computed(() => {
border-radius: 4px;
overflow: hidden;
position: relative;
/* border: 1px solid rgba(0, 168, 255, 0.2); */
}
.camera-header {
@ -372,8 +424,8 @@ const displayCameras = computed(() => {
display: flex;
align-items: center;
justify-content: space-between;
padding:4px 8px;
background: rgba(0, 0, 0, 0.2);
padding: 6px 10px;
background: rgba(0, 0, 0, 0.7);
color: #B9E8FF;
font-size: 12px;
}
@ -382,15 +434,25 @@ const displayCameras = computed(() => {
display: flex;
align-items: center;
gap: 5px;
img {
width: 14px;
height: 14px;
}
}
.camera-title img {
width: 16px;
height: 16px;
}
.camera-title p {
margin: 0;
font-size: 13px;
letter-spacing: 0.5px;
}
.camera-feed {
height: 180px;
overflow: hidden;
/* background: rgba(0, 21, 31, 0.5); */
background: #033347;
border-radius: 4px;
}
.camera-feed img {
@ -398,30 +460,4 @@ const displayCameras = computed(() => {
height: 100%;
object-fit: cover;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
background: rgba(0, 21, 31, 0.5);
border-radius: 4px;
padding: 20px;
}
.empty-icon {
width: 100px;
height: 74px;
margin-bottom: 10px;
/* opacity: 0.8; */
background: url('../assets/img/empty.png') no-repeat;
background-size: 100% 100%;
}
.empty-text {
color: #fff;
font-size: 10px;
letter-spacing: 1px;
}
</style>

View File

@ -61,14 +61,22 @@
size="small"
variant="search"
font="small"
@change="handleViewChange(camera.title)"
/>
</div>
<div class="camera-feed">
<VideoPlayer
:streamUrl="camera.streamUrl"
:title="camera.title"
fillMode="stretch-height"
<CarouselVideoPlayer
v-if="!isWebRTCCamera(camera)"
:views="getViewsForCamera(camera)"
:title="camera.title"
:interval="10000"
v-model="selectedViews[camera.title]"
/>
<CarouselWebRTCPlayer
v-else
:views="getViewsForCamera(camera)"
:title="camera.title"
:interval="10000"
v-model="selectedViews[camera.title]"
/>
</div>
</div>
@ -91,7 +99,8 @@ import AlarmStatistics from '../components/AlarmStatistics.vue'
import LatestAlarms from '../components/LatestAlarms.vue'
import RobotListModal from '../components/dialog/RobotListModal.vue'
import CustomSelect from '../components/common/CustomSelect.vue'
import VideoPlayer from '../components/common/VideoPlayer.vue'
import CarouselVideoPlayer from '../components/common/CarouselVideoPlayer.vue'
import CarouselWebRTCPlayer from '../components/common/CarouselWebRTCPlayer.vue'
import { homeApi } from '../api/index'
import Icon1 from '../assets/img/icon1.png'
import Icon2 from '../assets/img/icon2.png'
@ -161,13 +170,15 @@ const fetchRobotStatistics = async () => {
}
};
//
const cameras = ref([
{ title: 'A区厂区监控', src: jkA, streamUrl: '' },
{ title: 'B区厂区监控', src: jkA, streamUrl: '' },
{ title: '追随机器人监控', src: jkRobot, streamUrl: '' },
{ title: '室外机器人监控', src: jkRobot, streamUrl: '' }
{ title: 'A区厂区监控', src: jkA },
{ title: 'B区厂区监控', src: jkA },
{ title: '追随机器人监控', src: jkRobot },
{ title: '室外机器人监控', src: jkRobot }
]);
//
const selectedViews = ref({
'A区厂区监控': '视角1',
'B区厂区监控': '视角1',
@ -175,6 +186,7 @@ const selectedViews = ref({
'室外机器人监控': '视角1'
});
//
const viewOptions = ['视角1', '视角2'];
//
@ -188,9 +200,6 @@ const fetchMonitorStreams = async () => {
if (res.code === 200) {
monitorStreams.value = res.data || {};
console.log('获取监控视频流成功:', JSON.stringify(monitorStreams.value));
// URL
updateCameraStreams();
} else {
console.error('获取监控视频流失败:', res);
}
@ -199,30 +208,33 @@ const fetchMonitorStreams = async () => {
}
};
// URL
const updateCameraStreams = () => {
cameras.value.forEach(camera => {
const cameraData = monitorStreams.value[camera.title];
console.log(`处理摄像头 ${camera.title}:`, cameraData);
if (cameraData) {
const viewName = selectedViews.value[camera.title];
const newStreamUrl = cameraData[viewName] || '';
console.log(`设置 ${camera.title}${viewName} 流URL:`, newStreamUrl);
camera.streamUrl = newStreamUrl;
} else {
console.warn(`未找到 ${camera.title} 的数据`);
}
});
//
console.log('更新后的摄像头数据:', JSON.stringify(cameras.value));
// WebRTC
const isWebRTCStream = (url) => {
if (!url) return false;
return url.startsWith('webrtc://') || url.includes('31011500991180041301');
};
//
watch(selectedViews, (newViews) => {
updateCameraStreams();
}, { deep: true });
//
const getViewsForCamera = (camera) => {
const cameraData = monitorStreams.value[camera.title];
if (!cameraData) return [];
return viewOptions.map(viewName => {
return {
name: viewName,
streamUrl: cameraData[viewName] || ''
};
}).filter(view => view.streamUrl); // URL
};
// 使WebRTC
const isWebRTCCamera = (camera) => {
const views = getViewsForCamera(camera);
if (views.length === 0) return false;
// WebRTC
return isWebRTCStream(views[0].streamUrl);
};
const showRobotListModal = ref(false);
@ -232,19 +244,6 @@ const handleStatisticClick = (item) => {
}
};
//
const handleViewChange = (cameraTitle) => {
const camera = cameras.value.find(c => c.title === cameraTitle);
if (camera) {
const cameraData = monitorStreams.value[cameraTitle];
if (cameraData) {
const viewName = selectedViews.value[cameraTitle];
camera.streamUrl = cameraData[viewName] || '';
console.log(`切换 ${cameraTitle}${viewName}, URL: ${camera.streamUrl}`);
}
}
};
//
onMounted(() => {
fetchRobotStatistics();

View File

@ -34,10 +34,9 @@
<div class="thumbnail-container">
<div class="thumbnail-title">主摄像头</div>
<div class="camera-feed">
<WebRTCPlayer
<CustomWebRTCPlayer
v-if="streamUrls.ptz"
:url="streamUrls.ptz"
:debug="true"
:streamUrl="streamUrls.ptz"
/>
</div>
</div>
@ -45,20 +44,18 @@
<div class="thumbnail-container">
<div class="thumbnail-title">左侧摄像头</div>
<div class="thumbnail">
<WebRTCPlayer
<CustomWebRTCPlayer
v-if="streamUrls.front"
:url="streamUrls.front"
:debug="true"
:streamUrl="streamUrls.front"
/>
</div>
</div>
<div class="thumbnail-container">
<div class="thumbnail-title">右前摄像头</div>
<div class="thumbnail">
<WebRTCPlayer
<CustomWebRTCPlayer
v-if="streamUrls.therm"
:url="streamUrls.therm"
:debug="true"
:streamUrl="streamUrls.therm"
/>
</div>
</div>
@ -76,6 +73,8 @@
@view-all="handleViewAll"
@process-all="handleProcessAll"
/>
<!-- 告警事件列表 -->
<TitleBlock>告警事件列表</TitleBlock>
@ -139,7 +138,7 @@
>
处理
</button>
<span v-else style="color: #00ffff; font-size: 12px"
<span v-else class="handle-btn"
>已处理</span
>
</div>
@ -159,7 +158,7 @@
v-if="filteredMeterAlarms.length === 0"
title="暂无仪表异常"
subtitle="暂无需要处理的仪表异常"
:iconSrc="require('../assets/img/empty_list.png')"
:iconSrc="emptyList"
/>
<div
v-else
@ -185,7 +184,7 @@
>
处理
</button>
<span v-else style="color: #00ffff; font-size: 12px"
<span v-else class="handle-btn"
>已处理</span
>
</div>
@ -247,7 +246,7 @@ import EventDetailModal from '@/components/dialog/EventDetailModal.vue'
import TimeDisplay from '@/components/common/TimeDisplay.vue';
import { robotApi } from '@/api/detail';
import WebSocketClient from '@/utils/websocket';
import WebRTCPlayer from '@/components/detail/WebRTCPlayer.vue';
import CustomWebRTCPlayer from '@/components/common/CustomWebRTCPlayer.vue';
import ConfirmDialog from '@/components/dialog/ConfirmDialog.vue';
import EmptyState from '@/components/common/EmptyState.vue';
import emptyList from '@/assets/img/empty_list.png';
@ -523,20 +522,34 @@ const meterAlarms = computed(() =>
);
//
const pendingCount = computed(
//
const pendingCountComputed = computed(
() => alarmList.value.filter((a) => a.status === "pending" || a.status === "timeout").length
);
const doneCount = computed(
const doneCountComputed = computed(
() => alarmList.value.filter((a) => a.status === "done").length
);
const unreadCount = computed(
const unreadCountComputed = computed(
() => alarmList.value.filter((a) => !a.isRead).length
);
// -
const pendingCount = ref(0);
const doneCount = ref(0);
const unreadCount = ref(0);
const alertCount = ref(0);
const processedCount = ref(0);
//
const currentPage = ref(1);
const pageSize = ref(999);
// Tab
const alarmTab = ref("pending"); // "pending" | "done"
const setAlarmTab = (tab) => {
const setAlarmTab = async (tab) => {
alarmTab.value = tab;
//
await getAlarmEventList();
await getAlarmEventCount();
};
//
@ -550,59 +563,446 @@ const handleProcessAll = async () => {
confirmDialogMessage.value = '是否确认处理所有未处理的告警事件?';
};
//
const handleConfirmProcess = async () => {
// OCR
const handleOcrAlerts = async () => {
try {
// ID
const pendingAlarms = alarmList.value
.filter(a => a.status === 'pending')
.map(a => a.id);
if (pendingAlarms.length === 0) {
return;
}
const res = await robotApi.handleAlarmEvent({
eventIds: pendingAlarms
const res = await robotApi.handleOcrAlerts({
number: robotId.value
});
if (res.code === 200) {
//
alarmList.value = alarmList.value.map(a =>
pendingAlarms.includes(a.id) ? { ...a, status: 'done', isRead: true } : a
);
console.log('一键处理OCR告警成功');
//
await getAlarmEventList();
if (alarmTab.value === 'pending') {
await getUnhandledAlarmMessages();
} else {
await getHandledAlarmMessages();
}
//
await getAlarmEventCount();
return true;
}
return false;
} catch (error) {
console.error('一键处理失败:', error);
console.error('一键处理OCR告警失败:', error);
return false;
}
};
//
const getAlarmEventList = async () => {
//
const handleConfirmProcess = async () => {
try {
const res = await robotApi.getAlarmEventList({
robotId: robotId.value,
status: alarmTab.value // 'pending' 'done'
console.log('开始一键处理所有告警');
//
const ocrAlarms = alarmList.value.filter(a =>
(a.status === 'pending' || a.status === 'timeout') &&
(a.group === "meter" || a.content === "日常巡检")
);
const eventAlarms = alarmList.value.filter(a =>
(a.status === 'pending' || a.status === 'timeout') &&
a.group === "event" && a.content !== "日常巡检"
);
console.log(`分类完成: OCR告警 ${ocrAlarms.length}个, 事件告警 ${eventAlarms.length}`);
// OCR
if (ocrAlarms.length > 0) {
const ocrSuccess = await handleOcrAlerts();
console.log(`OCR告警处理${ocrSuccess ? '成功' : '失败'}`);
}
//
if (eventAlarms.length > 0) {
const eventIds = eventAlarms.map(a => a.id);
const res = await robotApi.handleAlarmEvent({
eventIds: eventIds
});
if (res.code === 200) {
console.log('事件告警处理成功');
//
alarmList.value = alarmList.value.map(a =>
eventIds.includes(a.id) ? { ...a, status: 'done', isRead: true } : a
);
} else {
console.error('事件告警处理失败:', res);
}
}
//
if (alarmTab.value === 'pending') {
await getUnhandledAlarmMessages();
} else {
await getHandledAlarmMessages();
}
//
await getAlarmEventCount();
//
showConfirmDialog.value = false;
} catch (error) {
console.error('一键处理失败:', error);
//
showConfirmDialog.value = false;
}
};
//
const isLoading = ref(false);
const loadingError = ref('');
//
const getAlarmEventCount = async () => {
try {
isLoading.value = true;
loadingError.value = '';
console.log('开始获取告警数量机器人ID:', robotId.value);
const res = await robotApi.getAlarmEventCount({
number: robotId.value
});
if (res.code === 200) {
//
alarmList.value = res.data.map(item => ({
id: item.id,
group: 'event',
level: item.level === 1 ? 'I级' : 'II级',
status: item.status === 1 ? 'pending' : 'done',
isRead: item.isRead === 1,
type: item.type === 1 ? 'warning' : 'danger',
content: item.content,
time: item.createTime
}));
console.log('获取告警数量成功,原始数据:', res.data);
alertCount.value = res.data.alert_count || 0;
processedCount.value = res.data.processed_count || 0;
//
pendingCount.value = alertCount.value;
doneCount.value = processedCount.value;
console.log('告警数量更新:', {
未处理: alertCount.value,
已处理: processedCount.value,
当前标签: alarmTab.value,
pendingCount: pendingCount.value,
doneCount: doneCount.value
});
} else {
console.error('获取告警数量失败,错误码:', res.code, '错误信息:', res.msg || res.message);
loadingError.value = `获取告警数量失败: ${res.msg || res.message || '未知错误'}`;
}
} catch (error) {
console.error('获取告警事件列表失败:', error);
console.error('获取告警数量失败,异常:', error);
loadingError.value = `获取告警数量失败: ${error.message || '未知错误'}`;
} finally {
isLoading.value = false;
}
};
//
const getUnhandledAlarmMessages = async () => {
try {
isLoading.value = true;
loadingError.value = '';
console.log('获取未处理告警消息列表,参数:', {
skip: (currentPage.value - 1) * pageSize.value,
limit: pageSize.value,
number: robotId.value
});
const res = await robotApi.getUnhandledAlarmMessages({
skip: (currentPage.value - 1) * pageSize.value,
limit: pageSize.value,
number: robotId.value
});
if (res.code === 200) {
console.log('获取未处理告警消息列表成功:', res.data);
//
alarmList.value = [];
//
if (res.data.high_temperature_message && res.data.high_temperature_message.length > 0) {
const highTempAlarms = res.data.high_temperature_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "I级", // I
status: "pending",
isRead: false,
type: "warning",
content: "高温感知报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || []
}));
alarmList.value.push(...highTempAlarms);
}
//
if (res.data.smoke_message && res.data.smoke_message.length > 0) {
const smokeAlarms = res.data.smoke_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "I级",
status: "pending",
isRead: false,
type: "danger",
content: "吸烟报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || []
}));
alarmList.value.push(...smokeAlarms);
}
//
if (res.data.long_stay_message && res.data.long_stay_message.length > 0) {
const longStayAlarms = res.data.long_stay_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "II级",
status: "pending",
isRead: false,
type: "warning",
content: "长时间滞留报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || []
}));
alarmList.value.push(...longStayAlarms);
}
//
if (res.data.air_quality_message && res.data.air_quality_message.length > 0) {
const airQualityAlarms = res.data.air_quality_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "II级",
status: "pending",
isRead: false,
type: "warning",
content: "空气质量报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
temperature: item.temperature
}));
alarmList.value.push(...airQualityAlarms);
}
//
if (res.data.stop_emergency_message && res.data.stop_emergency_message.length > 0) {
const emergencyAlarms = res.data.stop_emergency_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "I级",
status: "pending",
isRead: false,
type: "danger",
content: "急停按下",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || []
}));
alarmList.value.push(...emergencyAlarms);
}
//
if (res.data.voice_connect_message && res.data.voice_connect_message.length > 0) {
const voiceAlarms = res.data.voice_connect_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "II级",
status: "pending",
isRead: false,
type: "warning",
content: "语音未接通",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || []
}));
alarmList.value.push(...voiceAlarms);
}
// ()
if (res.data.daily_inspect_message && res.data.daily_inspect_message.length > 0) {
const inspectAlarms = res.data.daily_inspect_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "meter", //
level: "II级",
status: "pending",
isRead: false,
type: "warning",
content: "日常巡检",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
temperature: item.temperature
}));
alarmList.value.push(...inspectAlarms);
}
} else {
console.error('获取未处理告警消息列表失败:', res);
loadingError.value = `获取未处理告警消息列表失败: ${res.msg || res.message || '未知错误'}`;
}
} catch (error) {
console.error('获取未处理告警消息列表失败:', error);
loadingError.value = `获取未处理告警消息列表失败: ${error.message || '未知错误'}`;
} finally {
isLoading.value = false;
}
};
//
const getHandledAlarmMessages = async () => {
try {
const res = await robotApi.getHandledAlarmMessages({
skip: (currentPage.value - 1) * pageSize.value,
limit: pageSize.value,
number: robotId.value
});
if (res.code === 200) {
//
alarmList.value = [];
//
if (res.data.high_temperature_message && res.data.high_temperature_message.length > 0) {
const highTempAlarms = res.data.high_temperature_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "I级", // I
status: "done",
isRead: true,
type: "warning",
content: "高温感知报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
remark: item.remark
}));
alarmList.value.push(...highTempAlarms);
}
//
if (res.data.smoke_message && res.data.smoke_message.length > 0) {
const smokeAlarms = res.data.smoke_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "I级",
status: "done",
isRead: true,
type: "danger",
content: "吸烟报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
remark: item.remark
}));
alarmList.value.push(...smokeAlarms);
}
//
if (res.data.long_stay_message && res.data.long_stay_message.length > 0) {
const longStayAlarms = res.data.long_stay_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "II级",
status: "done",
isRead: true,
type: "warning",
content: "长时间滞留报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
remark: item.remark
}));
alarmList.value.push(...longStayAlarms);
}
//
if (res.data.air_quality_message && res.data.air_quality_message.length > 0) {
const airQualityAlarms = res.data.air_quality_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "II级",
status: "done",
isRead: true,
type: "warning",
content: "空气质量报警",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
temperature: item.temperature,
remark: item.remark
}));
alarmList.value.push(...airQualityAlarms);
}
//
if (res.data.stop_emergency_message && res.data.stop_emergency_message.length > 0) {
const emergencyAlarms = res.data.stop_emergency_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "I级",
status: "done",
isRead: true,
type: "danger",
content: "急停按下",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
remark: item.remark
}));
alarmList.value.push(...emergencyAlarms);
}
//
if (res.data.voice_connect_message && res.data.voice_connect_message.length > 0) {
const voiceAlarms = res.data.voice_connect_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "event",
level: "II级",
status: "done",
isRead: true,
type: "warning",
content: "语音未接通",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
remark: item.remark
}));
alarmList.value.push(...voiceAlarms);
}
// ()
if (res.data.daily_inspect_message && res.data.daily_inspect_message.length > 0) {
const inspectAlarms = res.data.daily_inspect_message.map(item => ({
id: item.eventId,
messageId: item.messageId,
group: "meter", //
level: "II级",
status: "done",
isRead: true,
type: "warning",
content: "日常巡检",
time: formatEventTime(item.createTime),
imageUrl: item.imageUrl || [],
temperature: item.temperature,
remark: item.remark
}));
alarmList.value.push(...inspectAlarms);
}
}
} catch (error) {
console.error('获取已处理告警消息列表失败:', error);
}
};
// tabAPI
const getAlarmEventList = async () => {
if (alarmTab.value === 'pending') {
await getUnhandledAlarmMessages();
} else {
await getHandledAlarmMessages();
}
};
@ -617,6 +1017,7 @@ const filteredEventAlarms = computed(() => {
}
return eventAlarms.value;
});
const filteredMeterAlarms = computed(() => {
if (alarmTab.value === "pending") {
//
@ -682,14 +1083,15 @@ const handleMouseLeave = (listType) => {
};
onMounted(async () => {
login();
console.log('RobotDetail组件挂载机器人ID:', robotId.value);
// token
// const loginSuccess = await login();
// if (loginSuccess) {
// //
// await getRobotStatus();
// }
await login();
console.log('登录成功,开始初始化数据');
//
startAutoScroll();
// DOM
nextTick(() => {
const taskListElement = document.querySelector('.task-list');
@ -700,7 +1102,29 @@ onMounted(async () => {
taskListElement.addEventListener('mouseleave', () => handleMouseLeave('taskList'));
}
});
await getAlarmEventList();
try {
// -
console.log('开始初始化告警数据');
await getAlarmEventCount();
console.log('告警数量获取完成,开始获取告警列表');
//
await getAlarmEventList();
console.log('告警列表获取完成');
// -
await getAlarmEventCount();
console.log('数据初始化完成,当前计数:', {
pendingCount: pendingCount.value,
doneCount: doneCount.value,
alertCount: alertCount.value,
processedCount: processedCount.value
});
} catch (error) {
console.error('初始化告警数据失败:', error);
}
});
onUnmounted(() => {
@ -731,58 +1155,182 @@ const showMeterModal = ref(false);
const currentAlarmData = ref({});
const currentMeterData = ref({});
//
const fetchAlarmDetail = async (messageId) => {
try {
const res = await robotApi.getAlarmEventDetail(messageId);
if (res.code === 200) {
console.log('获取告警详情成功:', res.data);
return res.data;
}
return null;
} catch (error) {
console.error('获取告警详情失败:', error);
return null;
}
};
//
const showAlarmDetail = (alarm) => {
const showAlarmDetail = async (alarm) => {
// messageId
let detailData = null;
if (alarm.messageId) {
detailData = await fetchAlarmDetail(alarm.messageId);
}
//
currentAlarmData.value = {
title: '告警详情',
mainImage: '../assets/img/camera-thumb1.jpg', //
subImages: ['../assets/img/camera-thumb2.jpg', '../assets/img/camera-thumb3.jpg'], //
messageId: alarm.messageId,
mainImage: detailData?.imageUrl && detailData.imageUrl.length > 0
? detailData.imageUrl[0]
: (alarm.imageUrl && alarm.imageUrl.length > 0 ? alarm.imageUrl[0] : '../assets/img/camera-thumb1.jpg'),
subImages: [],
type: alarm.content,
time: alarm.time,
robotName: robotId.value,
status: alarm.status === 'pending' ? '未处理' : '已处理'
robotName: detailData?.name || robotId.value,
status: alarm.status === 'pending' ? '未处理' : '已处理',
temperature: detailData?.temperature || alarm.temperature || '',
remark: detailData?.remark || alarm.remark || ''
};
//
if (detailData) {
//
if (detailData.imageUrl && detailData.imageUrl.length > 1) {
currentAlarmData.value.subImages = detailData.imageUrl.slice(1);
} else if (alarm.imageUrl && alarm.imageUrl.length > 1) {
currentAlarmData.value.subImages = alarm.imageUrl.slice(1);
} else {
currentAlarmData.value.subImages = [];
}
//
currentAlarmData.value.flvPtz = detailData.flvPtz || '';
currentAlarmData.value.flvTherm = detailData.flvTherm || '';
currentAlarmData.value.flvThermLight = detailData.flvThermLight || '';
} else {
// 使
currentAlarmData.value.subImages = alarm.imageUrl && alarm.imageUrl.length > 1
? alarm.imageUrl.slice(1)
: [];
}
showAlarmModal.value = true;
};
//
const showMeterDetail = (alarm) => {
const showMeterDetail = async (alarm) => {
// messageId
let detailData = null;
if (alarm.messageId) {
detailData = await fetchAlarmDetail(alarm.messageId);
}
//
currentMeterData.value = {
title: '仪表异常详情',
mainImage: '../assets/img/camera-thumb1.jpg', //
subImages: ['../assets/img/camera-thumb2.jpg', '../assets/img/camera-thumb3.jpg'], //
messageId: alarm.messageId,
mainImage: detailData?.imageUrl && detailData.imageUrl.length > 0
? detailData.imageUrl[0]
: (alarm.imageUrl && alarm.imageUrl.length > 0 ? alarm.imageUrl[0] : '../assets/img/camera-thumb1.jpg'),
subImages: [],
type: alarm.content,
time: alarm.time,
robotName: robotId.value,
status: alarm.status === 'pending' ? '未处理' : '已处理'
robotName: detailData?.name || robotId.value,
status: alarm.status === 'pending' ? '未处理' : '已处理',
temperature: detailData?.temperature || alarm.temperature || '',
remark: detailData?.remark || alarm.remark || ''
};
//
if (detailData) {
//
if (detailData.imageUrl && detailData.imageUrl.length > 1) {
currentMeterData.value.subImages = detailData.imageUrl.slice(1);
} else if (alarm.imageUrl && alarm.imageUrl.length > 1) {
currentMeterData.value.subImages = alarm.imageUrl.slice(1);
} else {
currentMeterData.value.subImages = [];
}
//
currentMeterData.value.flvPtz = detailData.flvPtz || '';
currentMeterData.value.flvTherm = detailData.flvTherm || '';
currentMeterData.value.flvThermLight = detailData.flvThermLight || '';
} else {
// 使
currentMeterData.value.subImages = alarm.imageUrl && alarm.imageUrl.length > 1
? alarm.imageUrl.slice(1)
: [];
}
showMeterModal.value = true;
};
//
const handleAlarmConfirm = (data) => {
const handleAlarmConfirm = async (data) => {
console.log('确认处理告警:', data);
//
await handleSingleAlarm(data);
showAlarmModal.value = false;
};
//
const handleAlarmReport = (data) => {
const handleAlarmReport = async (data) => {
console.log('处理并上报告警:', data);
//
await handleSingleAlarm(data);
showAlarmModal.value = false;
};
//
const handleMeterConfirm = (data) => {
const handleMeterConfirm = async (data) => {
console.log('确认处理仪表异常:', data);
//
await handleSingleAlarm(data);
showMeterModal.value = false;
};
//
const handleMeterReport = (data) => {
const handleMeterReport = async (data) => {
console.log('处理并上报仪表异常:', data);
//
await handleSingleAlarm(data);
showMeterModal.value = false;
};
//
const handleSingleAlarm = async (data) => {
try {
// messageId
const messageId = data.messageId;
if (!messageId) {
console.error('缺少messageId无法处理告警');
return;
}
const res = await robotApi.handleSingleAlarmEvent({
messageId: messageId,
remark: data.remark || '',
number: robotId.value
});
if (res.code === 200) {
console.log('处理单个告警成功');
//
await getAlarmEventList();
//
await getAlarmEventCount();
}
} catch (error) {
console.error('处理单个告警失败:', error);
}
};
const showViewAllModal = ref(false);
const currentEvent = ref({
mainImage: '../assets/img/camera-thumb1.jpg',
@ -1242,25 +1790,24 @@ const showConfirmDialog = ref(false);
const confirmDialogMessage = ref('');
// alarmTab
watch(alarmTab, async () => {
watch(alarmTab, async (newValue) => {
console.log('标签切换为:', newValue);
//
await getAlarmEventList();
//
await getAlarmEventCount();
});
//
const handleAlarmEvent = async (alarm) => {
try {
// API
const res = await robotApi.handleAlarmEvent({
eventIds: [alarm.id]
});
if (res.code === 200) {
//
alarm.status = 'done';
alarm.isRead = true;
//
await getAlarmEventList();
//
if (alarm.group === "meter" || alarm.content === "日常巡检") {
await showMeterDetail(alarm);
} else {
//
await showAlarmDetail(alarm);
}
} catch (error) {
console.error('处理告警事件失败:', error);