首页完善

This commit is contained in:
renna 2025-06-09 12:05:24 +08:00
parent 36aef50419
commit 0b23ae04bb
10 changed files with 365 additions and 99 deletions

View File

@ -102,6 +102,24 @@ export const robotApi = {
url: `/api/v1/events/alertFront/${messageId}`,
method: 'get',
})
},
// 查看全部告警事件接口
getAlarmEventList: (number) => {
return service({
url: `/api/v1/events/getAllAlertMessage/${number}`,
method: 'get',
})
},
// 查看全部告警事件接口(支持分页)
getAlarmDetailList: (params) => {
return service({
url: `/api/v1/events/getAllAlertMessage/${params.number}`,
method: 'get',
params: {
offset: params.offset,
limit: params.limit
}
})
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -7,9 +7,9 @@
type="text"
placeholder="请输入机器人搜索"
v-model="searchText"
@input="handleSearchInput"
@keyup.enter="handleSearchInput"
>
<img src="../assets/img/fdj.png" alt="搜索" class="search-icon" />
<img src="../assets/img/fdj.png" alt="搜索" class="search-icon" @click="handleSearchInput"/>
</div>
<CustomSelect
v-model="selectedFilter"
@ -32,9 +32,11 @@
<button class="retry-button" @click="fetchRobotList">重试</button>
</div>
<div v-else-if="groupedRobots.length === 0" class="empty-state">
<div class="empty-icon">🔍</div>
<div class="empty-text">没有找到机器人</div>
<div v-else-if="groupedRobots.length === 0" class="empty-state-container">
<EmptyState
subtitle="暂无机器人信息"
:iconSrc="emptyRobot"
/>
</div>
<template v-else>
@ -68,10 +70,23 @@ import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useRouter } from 'vue-router'
import BatteryIndicator from './common/BatteryIndicator.vue'
import CustomSelect from './common/CustomSelect.vue'
import EmptyState from './common/EmptyState.vue'
import { homeApi } from '../api/index'
import emptyRobot from '../assets/img/empty_robot.png'
const router = useRouter()
//
const debounce = (fn, delay) => {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
//
const searchText = ref('')
const filterOptions = ['全部', '在线', '离线', '故障']
@ -83,14 +98,14 @@ const loading = ref(false)
const error = ref(null)
//
const fetchRobotList = async () => {
const fetchRobotList = async (searchNumber = null) => {
loading.value = true
error.value = null
try {
const res = await homeApi.getRobotList({
tenantInfoId: '4fff5d4bcc4b4239941ff077a0da8958', // id
number: null, //
number: searchNumber, //
status: null, //
onlineStatus: null // 线
})
@ -210,8 +225,9 @@ const groupedRobots = computed(() => {
return grouped.filter(group => group.robots.length > 0)
})
const handleSearchInput = (e) => {
searchText.value = e.target.value
const handleSearchInput = () => {
console.log('点击搜索:', searchText.value)
searchRobots()
}
const handleWheel = (e) => {
@ -258,7 +274,14 @@ onUnmounted(() => {
//
watch(selectedFilter, () => {
console.log('筛选条件变化:', selectedFilter.value)
searchRobots() //
})
//
const searchRobots = () => {
console.log('搜索机器人:', searchText.value)
fetchRobotList(searchText.value)
}
</script>
<style scoped>
@ -558,4 +581,20 @@ watch(selectedFilter, () => {
.robot-status.待机中 {
color: #00ffff;
}
.empty-state-container {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.empty-icon) {
width: 213px !important;
height: 150px !important;
}
:deep(.empty-subtitle) {
color: #fff!important;
font-size: 16px!important;
}
</style>

View File

@ -53,6 +53,10 @@ const props = defineProps({
variant: {
type: String,
default: 'default' // 'default', 'search'
},
font: {
type: String,
default: 'default' // 'default', 'small'
}
})
@ -108,6 +112,7 @@ const selectOption = (option) => {
.select-text {
color: #B9E8FF;
margin-right: 25px;
font-size: v-bind("font === 'small' ? '12px' : '14px'");
}
.arrow-icon {
@ -137,7 +142,7 @@ const selectOption = (option) => {
color: #B9E8FF;
cursor: pointer;
white-space: nowrap;
font-size: v-bind("size === 'small' ? '12px' : '12px'");
font-size: v-bind("font === 'small' ? '12px' : '12px'");
line-height: v-bind("size === 'small' ? '18px' : '20px'");
}

View File

@ -14,7 +14,7 @@ import small_title from '../../assets/img/small_title.png';
const props = defineProps({
title: String
});
console.log(props.title,"--------------");
const titleBgSrc = computed(() => {
if (props.title === '最新告警') {
return title2;

View File

@ -164,8 +164,8 @@
<!-- 底部按钮 -->
<div class="modal-footer">
<button class="btn cancel" @click="handleClose">取消</button>
<button class="btn confirm" @click="handleConfirm">确认处理</button>
<button class="btn report" @click="handleReport">处理并上报</button>
<button class="btn confirm" @click="handleConfirm" v-if="alarmData.status !== '已处理'">确认处理</button>
<button class="btn report" @click="handleReport" v-if="alarmData.status !== '已处理'">处理并上报</button>
</div>
</div>
</div>
@ -271,15 +271,17 @@ const updateRemark = (e) => {
<style scoped>
.alarm-modal {
position: fixed;
top: 0;
left: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100vw;
height: 100vh;
/* background: rgba(0, 0, 0, 0.7); */
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {

View File

@ -49,15 +49,17 @@ const handleCancel = () => {
<style scoped>
.confirm-dialog-overlay {
position: fixed;
top: 0;
left: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.5);
}
.confirm-dialog {

View File

@ -13,10 +13,10 @@
<div class="left-content">
<!-- 主监控 -->
<div class="main-monitor">
<div class="monitor-title">主监控</div>
<div class="monitor-title">{{ selectedEvent ? selectedEvent.etypeName : '主监控' }}</div>
<div class="monitor-view">
<template v-if="eventData.mainImage">
<img :src="eventData.mainImage" alt="" @error="imgLoadError.main = true" v-show="!imgLoadError.main" />
<template v-if="selectedImage">
<img :src="selectedImage" alt="" @error="imgLoadError.main = true" v-show="!imgLoadError.main" />
<EmptyState
v-if="imgLoadError.main"
subtitle="暂无图片"
@ -35,33 +35,18 @@
<!-- 云台监控和热成像 -->
<div class="sub-monitors">
<div class="monitor-item">
<div class="monitor-title">云台监控</div>
<div class="monitor-item" v-for="index in 2" :key="index-1">
<div class="monitor-title">{{ index === 1 ? '云台监控' : '热成像' }}</div>
<div class="monitor-view">
<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 v-if="selectedEvent && selectedEvent.imageList && selectedEvent.imageList.length >= index">
<img
:src="selectedEvent.imageList[index-1]"
alt=""
@error="imgLoadError['sub'+(index-1)] = true"
v-show="!imgLoadError['sub'+(index-1)]"
/>
</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">
<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"
v-if="imgLoadError['sub'+(index-1)]"
subtitle="暂无图片"
:iconSrc="empty"
class="video-empty-state"
@ -89,11 +74,12 @@
<th>状态</th>
</tr>
<tr class="table-content">
<td>{{ eventData.type }}</td>
<td>{{ eventData.time }}</td>
<td>{{ eventData.robotName }}</td>
<td :class="eventData.status">{{ eventData.status }}</td>
<td>{{ selectedEvent ? selectedEvent.etypeName : '' }}</td>
<td>{{ rawCreateTime }}</td>
<td>{{ selectedEvent ? selectedEvent.name : '' }}</td>
<td :class="handleStatus">{{ handleStatus }}</td>
</tr>
</table>
</div>
</div>
@ -102,20 +88,32 @@
<div class="remark-section">
<TitleBlock>备注</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-text="selectedEvent && selectedEvent.remark ? selectedEvent.remark : ''"
></div>
</div>
</div>
</div>
<!-- 右侧监控视图列表 -->
<div class="right-content">
<TitleBlock>监控视图</TitleBlock>
<TitleBlock>告警列表</TitleBlock>
<div class="monitor-list">
<div v-for="(monitor, index) in monitorList" :key="index" class="monitor-item">
<div class="monitor-title">{{ monitor.title }}</div>
<div
v-for="(event, index) in eventList"
:key="event.messageId"
class="monitor-item"
:class="{ 'active': selectedEventIndex === index }"
@click="selectEvent(event, index)"
>
<div class="monitor-title">{{ event.etypeName }}</div>
<div class="monitor-view">
<template v-if="monitor.image">
<img :src="monitor.image" alt="" @error="setMonitorError(index)" v-show="!monitorErrors[index]" />
<template v-if="event.imagePreview">
<img :src="event.imagePreview" alt="" @error="setMonitorError(index)" v-show="!monitorErrors[index]" />
<EmptyState
v-if="monitorErrors[index]"
subtitle="暂无图片"
@ -138,15 +136,23 @@
<!-- 底部按钮 -->
<div class="modal-footer">
<button class="btn cancel" @click="handleClose">取消</button>
<button class="btn confirm" @click="handleConfirm">确认处理</button>
<button class="btn report" @click="handleReport">处理并上报</button>
<button
class="btn confirm"
@click="handleConfirm"
v-if="selectedEvent && selectedEvent.handle !== '1'"
>确认处理</button>
<button
class="btn report"
@click="handleReport"
v-if="selectedEvent && selectedEvent.handle !== '1'"
>处理并上报</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { ref, onMounted, watch, computed } from 'vue';
import TitleBlock from '../common/TitleBlock.vue';
import EmptyState from '../common/EmptyState.vue';
import empty from '../../assets/img/empty.png';
@ -170,14 +176,79 @@ const props = defineProps({
monitorList: {
type: Array,
default: () => []
},
eventList: {
type: Array,
default: () => []
}
});
//
const selectedEventIndex = ref(0);
const selectedEvent = ref(null);
const selectedImage = ref('');
//
const formatTime = computed(() => {
if (!selectedEvent.value || !selectedEvent.value.createTime) {
return '';
}
//
if (typeof selectedEvent.value.createTime === 'string' && selectedEvent.value.createTime.includes('-')) {
return selectedEvent.value.createTime;
}
// T" "
if (selectedEvent.value.createTime.includes('T')) {
selectedEvent.value.createTime = selectedEvent.value.createTime.replace('T', ' ');
}
//
try {
const date = new Date(Number(selectedEvent.value.createTime));
return date.toLocaleString();
} catch (e) {
return selectedEvent.value.createTime || '';
}
});
// createTimeT
const rawCreateTime = computed(() => {
if (!selectedEvent.value || !selectedEvent.value.createTime) return '-';
return selectedEvent.value.createTime.replace('T', ' ');
});
//
const handleStatus = computed(() => {
if (!selectedEvent.value) {
return '未处理';
}
return selectedEvent.value.handle === '1' ? '已处理' : '未处理';
});
//
const selectEvent = (event, index) => {
selectedEventIndex.value = index;
selectedEvent.value = event;
selectedImage.value = event.imagePreview || (event.imageList && event.imageList.length > 0 ? event.imageList[0] : '');
//
resetImgLoadError();
};
//
watch(() => props.eventList, (newList) => {
if (newList && newList.length > 0) {
selectEvent(newList[0], 0);
} else {
selectedEvent.value = null;
selectedImage.value = '';
}
}, { immediate: true });
//
const imgLoadError = ref({
main: false,
sub1: false,
sub2: false
sub0: false,
sub1: false
});
//
@ -198,8 +269,8 @@ const setMonitorError = (index) => {
const resetImgLoadError = () => {
imgLoadError.value = {
main: false,
sub1: false,
sub2: false
sub0: false,
sub1: false
};
monitorErrors.value = [];
};
@ -236,14 +307,14 @@ const handleClose = () => {
const handleConfirm = () => {
emit('confirm', {
...props.eventData,
...selectedEvent.value,
remark: remarkText.value
});
};
const handleReport = () => {
emit('report', {
...props.eventData,
...selectedEvent.value,
remark: remarkText.value
});
};
@ -252,20 +323,22 @@ const handleReport = () => {
<style scoped>
.event-detail-modal {
position: fixed;
top: 0;
left: 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100vw;
height: 100vh;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
width: 890px;
height: 900px;
height: 800px;
background: url("../../assets/img/alert2.png") no-repeat;
background-size: 100% 100%;
border-radius: 4px;
@ -287,6 +360,7 @@ const handleReport = () => {
font-size: 28px;
letter-spacing: 4px;
padding-left: 20px;
line-height: 20px;
}
.close-icon {
@ -327,8 +401,8 @@ const handleReport = () => {
}
.main-monitor {
min-height: 300px;
height: 300px;
min-height: 215px;
height: 215px;
}
.monitor-title {
@ -337,7 +411,7 @@ const handleReport = () => {
left: 0;
width: 100%;
padding: 5px 10px;
/* background: rgba(0, 21, 31, 0.2); */
background: rgba(0, 21, 31, 0.5);
color: #B9E8FF;
font-size: 12px;
z-index: 1;
@ -410,7 +484,7 @@ const handleReport = () => {
.remark-section{
border:1px solid rgba(0,206,234,0.7);
border-radius: 4px;
height: 135px;
height: 116px;
}
.remark-content {
padding:10px;
@ -434,7 +508,7 @@ const handleReport = () => {
}
.monitor-list {
padding:10px;
padding:10px;
flex: 1;
overflow-y: auto;
}
@ -455,6 +529,16 @@ padding:10px;
.monitor-list .monitor-item {
margin-bottom: 10px;
height: 150px;
cursor: pointer;
transition: all 0.3s;
}
.monitor-list .monitor-item:hover {
transform: scale(1.02);
}
.monitor-list .monitor-item.active {
border: 2px solid #00FFFF;
}
.modal-footer {

View File

@ -57,7 +57,7 @@
</div>
<CustomSelect
v-model="selectedViews[camera.title]"
:options="viewOptions"
:options="getViewOptionsForCamera(camera)"
size="small"
variant="search"
font="small"
@ -108,6 +108,10 @@ import Icon3 from '../assets/img/icon3.png'
import Icon4 from '../assets/img/icon4.png'
import jkA from '../assets/img/jkA.png'
import jkRobot from '../assets/img/jkRobot.png'
import VideoPlayer from '../components/common/VideoPlayer.vue'
import EmptyState from '../components/common/EmptyState.vue'
import CustomWebRTCPlayer from '../components/common/CustomWebRTCPlayer.vue'
import empty from '../assets/img/empty.png'
//
const statistics = ref([
@ -180,14 +184,20 @@ const cameras = ref([
//
const selectedViews = ref({
'A区厂区监控': '视角1',
'B区厂区监控': '视角1',
'追随机器人监控': '视角1',
'室外机器人监控': '视角1'
'A区厂区监控': '',
'B区厂区监控': '',
'追随机器人监控': '',
'室外机器人监控': ''
});
//
const viewOptions = ['视角1', '视角2'];
//
const getViewOptionsForCamera = (camera) => {
const cameraData = monitorStreams.value[camera.title];
if (!cameraData) return [];
// URL
return Object.keys(cameraData);
};
//
const monitorStreams = ref({});
@ -200,6 +210,15 @@ const fetchMonitorStreams = async () => {
if (res.code === 200) {
monitorStreams.value = res.data || {};
console.log('获取监控视频流成功:', JSON.stringify(monitorStreams.value));
//
cameras.value.forEach(camera => {
const viewOptions = getViewOptionsForCamera(camera);
if (viewOptions.length > 0) {
//
selectedViews.value[camera.title] = viewOptions[0];
}
});
} else {
console.error('获取监控视频流失败:', res);
}
@ -211,7 +230,7 @@ const fetchMonitorStreams = async () => {
// WebRTC
const isWebRTCStream = (url) => {
if (!url) return false;
return url.startsWith('webrtc://') || url.includes('31011500991180041301');
return url.startsWith('webrtc://') || url.includes('31011500991180041301') || url.includes('34020000001320000');
};
//
@ -219,12 +238,13 @@ const getViewsForCamera = (camera) => {
const cameraData = monitorStreams.value[camera.title];
if (!cameraData) return [];
return viewOptions.map(viewName => {
//
return Object.entries(cameraData).map(([viewName, streamUrl]) => {
return {
name: viewName,
streamUrl: cameraData[viewName] || ''
streamUrl: streamUrl || '' // URL"empty"
};
}).filter(view => view.streamUrl); // URL
}); // URL
};
// 使WebRTC
@ -232,8 +252,8 @@ const isWebRTCCamera = (camera) => {
const views = getViewsForCamera(camera);
if (views.length === 0) return false;
// WebRTC
return isWebRTCStream(views[0].streamUrl);
// WebRTC
return views.some(view => isWebRTCStream(view.streamUrl));
};
const showRobotListModal = ref(false);

View File

@ -69,7 +69,7 @@
v-model="alarmTab"
:pending-count="pendingCount"
:done-count="doneCount"
:unread-count="unreadCount"
:unread-count="allCount"
@view-all="handleViewAll"
@process-all="handleProcessAll"
/>
@ -215,6 +215,7 @@
v-model:visible="showViewAllModal"
:event-data="currentEvent"
:monitor-list="monitorList"
:event-list="eventListData"
@confirm="handleConfirm"
@report="handleReport"
/>
@ -537,6 +538,7 @@ const unreadCountComputed = computed(
const pendingCount = ref(0);
const doneCount = ref(0);
const unreadCount = ref(0);
const allCount = ref(0); // allCount
const alertCount = ref(0);
const processedCount = ref(0);
//
@ -553,8 +555,51 @@ const setAlarmTab = async (tab) => {
};
//
const handleViewAll = () => {
showViewAllModal.value = true;
const handleViewAll = async () => {
try {
isLoading.value = true;
// 使API10
const res = await robotApi.getAlarmDetailList({
number: robotId.value,
offset: 0,
limit: 10
});
if (res.code === 200 && res.data && res.data.length > 0) {
console.log('获取告警事件列表成功:', res.data);
// APIEventDetailModal
showViewAllModal.value = true;
//
currentEvent.value = {
mainImage: res.data[0].imagePreview || '',
subImages: res.data[0].imageList || [],
type: res.data[0].etypeName || '',
time: res.data[0].createTime || '',
robotName: res.data[0].name || robotId.value,
status: res.data[0].handle === '1' ? '已处理' : '未处理'
};
// monitorList
monitorList.value = res.data.map(item => ({
title: item.etypeName || '未知告警',
image: item.imagePreview || ''
}));
//
eventListData.value = res.data;
} else {
console.error('获取告警事件列表失败或列表为空');
window.$message && window.$message.error('获取告警事件列表失败或列表为空');
}
} catch (error) {
console.error('获取告警事件列表异常:', error);
window.$message && window.$message.error('获取告警事件列表失败');
} finally {
isLoading.value = false;
}
};
//
@ -672,6 +717,7 @@ const getAlarmEventCount = async () => {
console.log('获取告警数量成功,原始数据:', res.data);
alertCount.value = res.data.alert_count || 0;
processedCount.value = res.data.processed_count || 0;
allCount.value = res.data.all_count || 0; // all_count
//
pendingCount.value = alertCount.value;
@ -680,6 +726,7 @@ const getAlarmEventCount = async () => {
console.log('告警数量更新:', {
未处理: alertCount.value,
已处理: processedCount.value,
全部: allCount.value, //
当前标签: alarmTab.value,
pendingCount: pendingCount.value,
doneCount: doneCount.value
@ -1350,14 +1397,46 @@ const monitorList = ref([
{ title: '监控视图6', image: '../assets/img/camera-thumb3.jpg' }
]);
const handleConfirm = () => {
console.log('确认处理');
showViewAllModal.value = false;
const handleConfirm = async (data) => {
console.log('确认处理告警事件:', data);
// messageId
if (data.messageId) {
try {
const res = await robotApi.handleSingleAlarmEvent({
messageId: data.messageId,
remark: data.remark || '',
number: robotId.value
});
if (res.code === 200) {
console.log('处理单个告警成功');
//
await getAlarmEventList();
//
await getAlarmEventCount();
//
showViewAllModal.value = false;
} else {
console.error('处理单个告警失败:', res);
}
} catch (error) {
console.error('处理单个告警异常:', error);
}
} else {
console.log('缺少messageId无法处理告警');
showViewAllModal.value = false;
}
};
const handleReport = () => {
console.log('处理并上报');
showViewAllModal.value = false;
const handleReport = async (data) => {
console.log('处理并上报告警事件:', data);
// handleConfirm
await handleConfirm(data);
};
//
@ -1802,17 +1881,34 @@ watch(alarmTab, async (newValue) => {
//
const handleAlarmEvent = async (alarm) => {
try {
//
//
if (alarm.group === "meter" || alarm.content === "日常巡检") {
await showMeterDetail(alarm);
// messageId
if (!alarm.messageId) {
window.$message && window.$message.error('缺少 messageId无法处理该告警');
return;
}
// loading
isLoading.value = true;
try {
await handleSingleAlarm({ messageId: alarm.messageId });
} catch (err) {
window.$message && window.$message.error('处理仪表识别告警失败');
} finally {
isLoading.value = false;
}
} else {
//
//
await showAlarmDetail(alarm);
}
} catch (error) {
console.error('处理告警事件失败:', error);
}
};
// RobotDetail.vueeventListData
// currentEventmonitorList
const eventListData = ref([]);
</script>
<style scoped>