第一版差

This commit is contained in:
renna 2025-06-09 15:11:47 +08:00
parent 0b23ae04bb
commit f615b15783
8 changed files with 675 additions and 161 deletions

View File

@ -120,6 +120,13 @@ export const robotApi = {
limit: params.limit
}
})
},
// 获取事件类型下拉框
getEventTypes: () => {
return service({
url: '/api/v1/events/getEtypeNameList',
method: 'get',
})
}
}

View File

@ -0,0 +1,403 @@
<template>
<div class="alarm-statistics">
<div class="time-filter">
<div
v-for="period in periods"
:key="period.value"
:class="['filter-item', { active: currentPeriod === period.value }]"
@click="handleManualSelect(period.value)"
>
{{ period.label }}
</div>
</div>
<div class="chart-container" ref="chartRef"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from "vue";
import * as echarts from "echarts";
import { homeApi } from '../api/index';
import emptyList from '../assets/img/empty_list.png';
const chartRef = ref(null);
let chart = null;
let autoSwitchTimer = null;
let checkInterval = null;
const isLoading = ref(false);
const isManualMode = ref(false); //
//
const eventTypeMap = {
'0': '读表告警',
'1': '高温感知报警',
'2': '吸烟报警',
'3': '长时间滞留报警',
'4': '空气质量报警',
'5': '急停按下',
'6': '语音未接通',
'7': '日常巡检'
};
// API
const apiData = ref({
'1': [], //
'7': [], //
'30': [], //
});
//
const hasData = computed(() => {
return apiData.value[currentPeriod.value] && apiData.value[currentPeriod.value].length > 0;
});
// API
const transformData = (data) => {
const xAxisData = [];
const seriesData = [];
if (!data || data.length === 0) {
return { xAxis: [], series: [] };
}
//
data.forEach(item => {
const eventTypeName = eventTypeMap[item.eventType] || `未知类型(${item.eventType})`;
xAxisData.push(eventTypeName);
seriesData.push(item.count);
});
return {
xAxis: xAxisData,
series: seriesData
};
};
const periods = [
{ label: "天", value: "1" },
{ label: "周", value: "7" },
{ label: "月", value: "30" },
];
const currentPeriod = ref("1");
//
const fetchAlarmStatistics = async (day) => {
console.log(`获取${day}天的数据`);
isLoading.value = true;
try {
const res = await homeApi.getAlarmStatistics(day);
if (res.code === 200) {
apiData.value[day] = res.data || [];
console.log(`获取${day}天告警统计数据成功:`, res.data);
} else {
console.error(`获取${day}天告警统计数据失败:`, res);
apiData.value[day] = [];
}
} catch (err) {
console.error(`获取${day}天告警统计数据错误:`, err);
apiData.value[day] = [];
} finally {
isLoading.value = false;
//
if (currentPeriod.value === day) {
updateChart();
}
}
};
//
const updateChart = () => {
if (!chartRef.value || !chart) return;
console.log(`更新图表数据,当前时间段: ${currentPeriod.value}`);
//
const chartData = transformData(apiData.value[currentPeriod.value]);
//
const option = {
grid: {
top: "10%",
left: "5%",
right: "4%",
bottom: "15%",
containLabel: true,
},
xAxis: {
type: "category",
// 使
data: chartData.xAxis.length > 0 ? chartData.xAxis : ['暂无数据'],
axisLine: {
show: true,
lineStyle: {
color: "rgba(185, 232, 255, 0.1)",
},
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B9E8FF",
fontSize: 12,
interval: 0,
rotate: 45,
},
},
yAxis: {
type: "value",
name: "数量: 次",
nameTextStyle: {
color: "#B9E8FF",
fontSize: 12,
padding: [0, 30, 0, 0],
},
splitLine: {
show: true,
lineStyle: {
color: "rgba(185, 232, 255, 0.1)",
type: "dashed",
},
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
color: "#B9E8FF",
fontSize: 12,
},
},
series: [
{
//
data: chartData.series.length > 0 ? chartData.series : [0],
type: "line",
smooth: true,
symbol: "circle",
symbolSize: 8,
itemStyle: {
color: "#FF8A00",
},
lineStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#FF8A00",
},
{
offset: 1,
color: "rgba(255, 138, 0, 0)",
},
],
},
width: 3,
},
areaStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "rgba(255, 138, 0, 0.3)",
},
{
offset: 1,
color: "rgba(255, 138, 0, 0)",
},
],
},
},
label: {
show: true,
position: 'top',
color: '#FF8A00',
fontSize: 14,
fontWeight: 'bold',
},
},
],
};
console.log('设置图表选项');
chart.setOption(option, true); // 使true
};
//
const initChart = () => {
if (!chartRef.value) return;
if (chart) {
chart.dispose();
}
chart = echarts.init(chartRef.value);
updateChart(); //
};
//
const startAutoSwitch = () => {
stopAutoSwitch(); //
autoSwitchTimer = setInterval(async () => {
//
if (isManualMode.value) return;
const currentIndex = periods.findIndex(p => p.value === currentPeriod.value);
const nextIndex = (currentIndex + 1) % periods.length;
const nextPeriod = periods[nextIndex].value;
console.log(`自动切换到: ${periods[nextIndex].label}(${nextPeriod})`);
//
currentPeriod.value = nextPeriod;
//
await fetchAlarmStatistics(nextPeriod);
}, 10000); // 10
};
//
const stopAutoSwitch = () => {
if (autoSwitchTimer) {
clearInterval(autoSwitchTimer);
autoSwitchTimer = null;
}
};
//
const handleManualSelect = async (period) => {
console.log(`手动选择时间段: ${period}`);
//
isManualMode.value = true;
//
if (currentPeriod.value === period) return;
//
currentPeriod.value = period;
//
await fetchAlarmStatistics(period);
// 5
setTimeout(() => {
console.log('恢复自动模式');
isManualMode.value = false;
}, 30000); // 30
};
//
const handleResize = () => {
chart && chart.resize();
};
//
watch(currentPeriod, (newPeriod) => {
console.log(`当前选中时间段变为: ${newPeriod}`);
});
onMounted(async () => {
console.log('组件挂载');
// 1
await fetchAlarmStatistics('1');
//
Promise.all([
fetchAlarmStatistics('7'),
fetchAlarmStatistics('30')
]);
initChart();
window.addEventListener("resize", handleResize);
//
console.log('开始自动轮播');
startAutoSwitch();
//
checkInterval = setInterval(() => {
if (!autoSwitchTimer) {
console.log('检测到轮播已停止,重新启动');
startAutoSwitch();
}
}, 20000); // 20
});
onUnmounted(() => {
//
stopAutoSwitch();
if (checkInterval) {
clearInterval(checkInterval);
checkInterval = null;
}
//
if (chart) {
chart.dispose();
chart = null;
}
window.removeEventListener("resize", handleResize);
console.log('组件卸载,停止自动轮播');
});
</script>
<style scoped>
.alarm-statistics {
width: 100%;
height: 100%;
position: relative;
}
.chart-container {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.time-filter {
display: flex;
justify-content: flex-end;
gap: 5px;
padding: 0 10px;
position: relative;
z-index: 3;
}
.filter-item {
color: #b9e8ff;
cursor: pointer;
padding: 2px 10px;
border-radius: 2px;
transition: all 0.3s;
}
.filter-item.active {
padding: 2px 10px;
background: url("../assets/img/alarm_tri.png") no-repeat;
background-size: 100% 100%;
color: #00ffff;
}
</style>

View File

@ -175,33 +175,18 @@ const updateChart = () => {
},
series: [
{
//
data: chartData.series.length > 0 ? chartData.series : [0],
type: "line",
smooth: true,
smooth: false,
symbol: "circle",
symbolSize: 8,
itemStyle: {
color: "#FF8A00",
color: "#fff", //
borderColor: '#FF8A00', //
borderWidth: 1,
},
lineStyle: {
color: {
type: "linear",
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "#FF8A00",
},
{
offset: 1,
color: "rgba(255, 138, 0, 0)",
},
],
},
color: "#FF8A00", // 线
width: 3,
},
areaStyle: {
@ -212,50 +197,18 @@ const updateChart = () => {
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: "rgba(255, 138, 0, 0.3)",
},
{
offset: 1,
color: "rgba(255, 138, 0, 0)",
},
{ offset: 0, color: "rgba(255, 138, 0, 0.3)" },
{ offset: 1, color: "rgba(255, 138, 0, 0)" },
],
},
},
markPoint: chartData.series.length > 0 ? {
symbol: "circle",
symbolSize: 50,
itemStyle: {
color: {
type: "radial",
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{
offset: 0,
color: "rgba(255, 138, 0, 0.2)",
},
{
offset: 0.8,
color: "rgba(255, 138, 0, 0.1)",
},
{
offset: 1,
color: "rgba(255, 138, 0, 0)",
},
],
},
},
data: [
{
type: "max",
name: "最高频率",
label: { show: true, color: "#FF8A00", fontSize: 12 },
},
],
} : null,
label: {
show: true,
position: 'top',
color: '#FF8A00',
fontSize: 14,
fontWeight: 'bold',
},
},
],
};

View File

@ -26,7 +26,7 @@
</div>
<!-- 视角指示器 -->
<div class="view-indicators" v-if="hasValidViews">
<!-- <div class="view-indicators" v-if="hasValidViews">
<div
v-for="(view, index) in validViews"
:key="index"
@ -34,7 +34,7 @@
:class="{ active: currentViewIndex === validViewsIndices[index] }"
@click="setView(validViewsIndices[index])"
></div>
</div>
</div> -->
</div>
</div>
</template>

View File

@ -24,7 +24,7 @@
</div>
<!-- 视角指示器 -->
<div class="view-indicators" v-if="hasValidViews">
<!-- <div class="view-indicators" v-if="hasValidViews">
<div
v-for="(view, index) in validViews"
:key="index"
@ -32,7 +32,7 @@
:class="{ active: currentViewIndex === validViewsIndices[index] }"
@click="setView(validViewsIndices[index])"
></div>
</div>
</div> -->
</div>
</div>
</template>

View File

@ -19,6 +19,24 @@
<div class="loading-text">加载中...</div>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
<!-- 控制按钮 -->
<div class="control-buttons">
<img
src="../../assets/img/fullscreen-icon.png"
alt="全屏"
class="control-icon fullscreen-icon"
@click="toggleFullscreen"
v-if="!isFullscreen"
/>
<img
src="../../assets/img/snapshot-icon.png"
alt="退出全屏"
class="control-icon snapshot-icon"
@click="exitFullscreen"
v-if="isFullscreen"
/>
</div>
</div>
</div>
</template>
@ -50,6 +68,57 @@ const videoRef = ref(null);
const isLoading = ref(false);
const error = ref('');
const pc = ref(null); // WebRTC
const isFullscreen = ref(false); //
//
const toggleFullscreen = () => {
const container = videoRef.value?.parentElement || videoRef.value;
if (!container) return;
if (!document.fullscreenElement) {
//
if (container.requestFullscreen) {
container.requestFullscreen().then(() => {
isFullscreen.value = true;
}).catch(err => {
console.error('全屏模式出错:', err);
});
} else if (container.webkitRequestFullscreen) { // Safari
container.webkitRequestFullscreen();
isFullscreen.value = true;
} else if (container.msRequestFullscreen) { // IE11
container.msRequestFullscreen();
isFullscreen.value = true;
}
} else {
exitFullscreen();
}
};
// 退
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen().then(() => {
isFullscreen.value = false;
}).catch(err => {
console.error('退出全屏模式出错:', err);
});
} else if (document.webkitExitFullscreen) { // Safari
document.webkitExitFullscreen();
isFullscreen.value = false;
} else if (document.msExitFullscreen) { // IE11
document.msExitFullscreen();
isFullscreen.value = false;
}
};
//
const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement ||
!!document.webkitFullscreenElement ||
!!document.msFullscreenElement;
};
// WebRTC
const initWebRTC = async () => {
@ -249,80 +318,63 @@ const cleanupWebRTC = () => {
error.value = '';
};
// URL
// streamUrl
watch(() => props.streamUrl, (newUrl) => {
if (newUrl) {
console.log('流URL变化重新初始化WebRTC:', newUrl);
console.log('streamUrl变化重新初始化WebRTC:', newUrl);
initWebRTC();
} else {
console.log('流URL为空清理WebRTC连接');
console.log('streamUrl为空清理WebRTC连接');
cleanupWebRTC();
}
}, { immediate: true });
onMounted(() => {
console.log('CustomWebRTCPlayer组件挂载');
if (props.streamUrl) {
initWebRTC();
}
});
//
onMounted(() => {
if (props.streamUrl) {
console.log('组件挂载初始化WebRTC:', props.streamUrl);
initWebRTC();
}
//
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
});
//
onUnmounted(() => {
console.log('CustomWebRTCPlayer组件卸载');
console.log('组件卸载清理WebRTC连接');
cleanupWebRTC();
//
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
document.removeEventListener('msfullscreenchange', handleFullscreenChange);
// 退
if (isFullscreen.value) {
exitFullscreen();
}
});
</script>
<style scoped>
.webrtc-player {
width: 100%;
height: 100%;
background: #033347;
position: relative;
overflow: hidden;
display: flex;
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;
overflow: hidden;
background-color: #033347;
}
.player-container {
position: relative;
width: 100%;
height: 100%;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.video-element {
@ -337,44 +389,88 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid #00a8ff;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 10px;
}
.loading-text {
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); }
}
</style>
.loading-text {
color: #ffffff;
margin-top: 10px;
font-size: 14px;
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ff4d4f;
background-color: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 4px;
font-size: 14px;
max-width: 80%;
text-align: center;
z-index: 2;
}
.empty-state {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.video-empty-state {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 控制按钮样式 */
.control-buttons {
position: absolute;
right: 10px;
bottom: 10px;
display: flex;
gap: 10px;
z-index: 3;
}
.control-icon {
width: 32px;
height: 32px;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.3s;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px;
border-radius: 4px;
}
.control-icon:hover {
opacity: 1;
}
</style>

View File

@ -472,6 +472,7 @@ onMounted(() => {
/* background: rgba(0, 21, 31, 0.5); */
background: #033347;
border-radius: 4px;
position: relative;
}
.camera-feed img {

View File

@ -31,7 +31,7 @@
</div>
<div class="center-panel">
<div class="thumbnail-container">
<div class="thumbnail-container1">
<div class="thumbnail-title">主摄像头</div>
<div class="camera-feed">
<CustomWebRTCPlayer
@ -96,9 +96,7 @@
class="search-icon arrow-icon"
/>
<ul v-if="selectOpen" class="alarm-select-dropdown">
<li @click.stop="selectType('全部告警')">全部告警</li>
<li @click.stop="selectType('事件告警')">事件告警</li>
<li @click.stop="selectType('仪表识别')">仪表识别</li>
<li v-for="type in eventTypeOptions" :key="type" @click.stop="selectType(type)">{{ type }}</li>
</ul>
</div>
</div>
@ -559,12 +557,19 @@ const handleViewAll = async () => {
try {
isLoading.value = true;
// 使API10
const res = await robotApi.getAlarmDetailList({
//
const params = {
number: robotId.value,
offset: 0,
limit: 10
});
offset: (currentPage.value - 1) * pageSize.value,
limit: pageSize.value
};
// etypeName
if (selectedType.value && selectedType.value !== '全部告警') {
params.etypeName = selectedType.value;
}
// 使API
const res = await robotApi.getAlarmDetailList(params);
if (res.code === 200 && res.data && res.data.length > 0) {
console.log('获取告警事件列表成功:', res.data);
@ -1055,14 +1060,18 @@ const getAlarmEventList = async () => {
// Tab
const filteredEventAlarms = computed(() => {
let list = eventAlarms.value;
if (selectedType.value && selectedType.value !== '全部告警') {
// etypeNamecontent
list = list.filter(a => a.etypeName === selectedType.value || a.content === selectedType.value);
}
if (alarmTab.value === "pending") {
//
return eventAlarms.value.filter((a) => a.status === "pending" || a.status === "timeout");
return list.filter((a) => a.status === "pending" || a.status === "timeout");
}
if (alarmTab.value === "done") {
return eventAlarms.value.filter((a) => a.status === "done");
return list.filter((a) => a.status === "done");
}
return eventAlarms.value;
return list;
});
const filteredMeterAlarms = computed(() => {
@ -1172,6 +1181,17 @@ onMounted(async () => {
} catch (error) {
console.error('初始化告警数据失败:', error);
}
//
try {
//
const res = await robotApi.getEventTypes();
if (res.code === 200 && Array.isArray(res.data)) {
eventTypeOptions.value = ['全部告警', ...res.data];
}
} catch (e) {
console.error('获取告警类型失败', e);
}
});
onUnmounted(() => {
@ -1194,6 +1214,10 @@ const closeSelect = () => {
const selectType = (type) => {
selectedType.value = type;
selectOpen.value = false;
// ""
if (showViewAllModal.value) {
handleViewAll();
}
};
//
@ -1909,6 +1933,24 @@ const handleAlarmEvent = async (alarm) => {
// RobotDetail.vueeventListData
// currentEventmonitorList
const eventListData = ref([]);
//
const eventTypeOptions = ref(['全部告警']);
//
onMounted(async () => {
// ... existing code ...
try {
//
const res = await robotApi.getEventTypes();
if (res.code === 200 && Array.isArray(res.data)) {
eventTypeOptions.value = ['全部告警', ...res.data];
}
} catch (e) {
console.error('获取告警类型失败', e);
}
// ... existing code ...
});
</script>
<style scoped>
@ -1962,6 +2004,7 @@ const eventListData = ref([]);
display: flex;
flex-direction: column;
gap: 15px;
height: 100%; /* 确保占满整个高度 */
}
/* 右侧面板 */
@ -2264,7 +2307,14 @@ const eventListData = ref([]);
flex-direction: column;
gap: 15px;
}
.thumbnail-container1{
height: 100%;
flex: 1;
display: flex;
flex-direction: column;
gap: 15px;
position: relative;
}
.camera-title {
font-size: 14px;
color: #b9e8ff;
@ -2275,7 +2325,7 @@ const eventListData = ref([]);
}
.camera-feed {
height: 60vh;
height: 55vh; /* 从60vh改为70vh */
background: rgba(0, 21, 31, 0.3);
border-radius: 4px;
border: 1px solid rgba(185, 232, 255, 0.3);
@ -2295,23 +2345,25 @@ const eventListData = ref([]);
right: 10px;
display: flex;
gap: 10px;
z-index: 10;
}
.camera-thumbnails {
/* height: 150px; */
display: flex;
flex: 1;
gap: 10px;
/* 移除固定高度使用flex布局 */
}
.thumbnail-container {
width: 100%;
background: rgba(0, 21, 31, 0.3);
border-radius: 4px;
overflow: hidden;
/* overflow: hidden; */
position: relative;
display: flex; /* 添加flex布局 */
flex-direction: column; /* 垂直方向flex */
flex: 1; /* 添加flex:1使其自适应高度 */
}
.thumbnail-container.active {
@ -2331,9 +2383,11 @@ const eventListData = ref([]);
.thumbnail {
width: 100%;
height: 100%;
min-height: 150px;
flex: 1; /* 使用flex:1代替固定高度 */
background: rgba(0, 21, 31, 0.3);
display: flex; /* 添加flex布局 */
flex-direction: column; /* 垂直方向flex */
}
.thumbnail img {