实现大部分功能
This commit is contained in:
parent
93db9389a2
commit
36aef50419
@ -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修改逻辑 找不到流的时候 页面显示空状态 也进行轮播 而不是找不到匹配的视角或视角没有流,找第一个有效视角
|
||||
@ -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',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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 |
@ -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) => {
|
||||
|
||||
379
src/components/common/CarouselVideoPlayer.vue
Normal file
379
src/components/common/CarouselVideoPlayer.vue
Normal 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 });
|
||||
|
||||
// 当currentViewIndex变化时,更新modelValue
|
||||
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>
|
||||
372
src/components/common/CarouselWebRTCPlayer.vue
Normal file
372
src/components/common/CarouselWebRTCPlayer.vue
Normal 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 });
|
||||
|
||||
// 当currentViewIndex变化时,更新modelValue
|
||||
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>
|
||||
@ -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;
|
||||
|
||||
// 确保streamUrl是完整的WebRTC URL格式
|
||||
if (!streamUrlValue.startsWith('webrtc://')) {
|
||||
// 如果不是完整URL,尝试构建完整URL
|
||||
streamUrlValue = `webrtc://qvs-live.thirdmonitor.concoai.com:447/2xenzwqy2ppcl/${streamUrlValue}`;
|
||||
}
|
||||
|
||||
console.log('发送WebRTC请求:', {
|
||||
url: apiUrl,
|
||||
streamUrl: streamUrlValue
|
||||
});
|
||||
|
||||
// 发送offer到服务器并获取answer
|
||||
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>
|
||||
@ -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>
|
||||
@ -354,7 +354,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
:deep(.empty-icon) {
|
||||
width: 100px;
|
||||
/* width: 100px; */
|
||||
height: 74px;
|
||||
}
|
||||
|
||||
|
||||
@ -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("");
|
||||
|
||||
// 监听alarmData中的remark变化
|
||||
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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
// 修改原有的获取告警事件列表函数,根据当前tab调用对应的API
|
||||
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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user