Files
movecheck/src/components/AlarmRecord.vue
2025-11-14 11:06:48 +08:00

1004 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-container alarm-record-container">
<!-- 日期选择区域 -->
<view class="date-picker-container">
<picker
mode="date"
:value="selectedDate"
@change="onDateChange"
class="date-picker"
>
<view class="picker-input">
<text class="picker-text">{{ selectedDate || '请选择日期' }}</text>
<text class="picker-icon"></text>
</view>
</picker>
</view>
<!-- 报警表格 -->
<view class="page-content">
<view class="alarm-content">
<view class="alarm-table">
<!-- 表格头部 -->
<view class="table-header">
<view class="table-cell header-cell content-column">内容</view>
<view class="table-cell header-cell type-column">种类</view>
<view class="table-cell header-cell time-column">时间</view>
</view>
<!-- 表格内容 -->
<scroll-view
class="table-body"
scroll-y="true"
:scroll-with-animation="true"
:scroll-top="scrollTop"
@scrolltolower="onScrollToLower"
@scroll="onScroll"
>
<!-- 加载状态 -->
<view class="table-loading-container" v-if="isLoading">
<view class="table-loading-spinner"></view>
<text class="table-loading-text">正在加载报警记录...</text>
</view>
<!-- 表格数据 -->
<template v-else>
<view
v-for="(alarm, index) in alarmList"
:key="index"
class="table-row"
:class="{ 'even-row': index % 2 === 0 }"
>
<view class="table-cell content-column">{{ alarm.content }}</view>
<view class="table-cell type-column">{{ alarm.type }}</view>
<view class="table-cell time-column">{{ alarm.time }}</view>
</view>
</template>
<!-- 空数据提示 -->
<view class="table-empty-container" v-if="!isLoading && alarmList.length === 0 && hasInitialized">
<text class="table-empty-text">暂无报警记录</text>
</view>
<!-- 初始状态提示 -->
<view class="table-empty-container" v-if="!isLoading && alarmList.length === 0 && !hasInitialized">
<text class="table-empty-text">暂无数据</text>
</view>
<!-- 加载更多提示 -->
<view class="load-more-container" v-if="isLoadingMore">
<view class="load-more-spinner"></view>
<text class="load-more-text">正在加载更多...</text>
</view>
<!-- 底部间距确保最后一条记录完全显示 -->
<view class="table-bottom-spacing"></view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import { alertApi, eventApi } from '@/utils/api.js';
// 报警数据
const alarmList = ref([]);
const isLoading = ref(false);
const isConnected = ref(false);
const hasInitialized = ref(false);
// 分页相关
const currentPage = ref(0);
const pageSize = ref(20);
const hasMoreData = ref(true);
const isLoadingMore = ref(false);
// 滚动相关
const scrollTop = ref(0);
const isScrolling = ref(false);
// 日期选择相关
const selectedDate = ref('');
const startTime = ref('');
const endTime = ref('');
// 移除空行占位逻辑,没有数据时只显示"暂无数据"
// MQTT报警服务接口预留
const mqttAlarmService = {
// 获取历史报警记录(分页)
getHistoryAlarms: async (page = 0, size = 20, isLoadMore = false) => {
try {
if (!isLoadMore) {
isLoading.value = true;
hasInitialized.value = true;
} else {
isLoadingMore.value = true;
}
// 如果没有选择日期,使用默认日期(当天)
let queryStartTime = startTime.value;
let queryEndTime = endTime.value;
if (!queryStartTime || !queryEndTime) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
const todayStr = `${year}-${month}-${day}`;
queryStartTime = `${todayStr} 00:00:00`;
queryEndTime = `${todayStr} 23:59:59`;
// 更新选择的日期
if (!selectedDate.value) {
selectedDate.value = todayStr;
startTime.value = queryStartTime;
endTime.value = queryEndTime;
}
}
// 调用分页获取告警接口
const response = await alertApi.getListByCreateTime({
page: page,
size: size,
startTime: queryStartTime,
endTime: queryEndTime
});
// 处理响应数据
if (response && response.data) {
const newAlarms = response.data.map(item => ({
content: item.content || '未知报警',
type: item.category || '未知类型',
time: formatDateTime(item.alertTime) || '未知时间',
level: mapLevelToDisplay(item.level) || 'C',
action: item.action || '待处理',
actionTime: formatDateTime(item.actionTime) || '未知时间'
}));
if (isLoadMore) {
// 加载更多时追加数据
alarmList.value = [...alarmList.value, ...newAlarms];
} else {
// 首次加载时替换数据
alarmList.value = newAlarms;
}
// 判断是否还有更多数据
hasMoreData.value = newAlarms.length === size;
currentPage.value = page;
// 保存查询事件(只在首次加载时保存)
if (!isLoadMore) {
await createQueryEvent('success', newAlarms.length);
}
} else {
if (!isLoadMore) {
alarmList.value = [];
}
hasMoreData.value = false;
// 保存查询事件(无数据)
if (!isLoadMore) {
await createQueryEvent('empty', 0);
}
}
isLoading.value = false;
isLoadingMore.value = false;
return Promise.resolve(response);
} catch (error) {
console.error('❌ 获取历史报警记录失败:', error);
isLoading.value = false;
isLoadingMore.value = false;
// 保存查询事件(错误)
if (!isLoadMore) {
await createQueryEvent('error', 0);
}
// 显示错误提示
uni.showToast({
title: '获取报警记录失败',
icon: 'error',
duration: 2000
});
return Promise.reject(error);
}
},
// 获取实时报警
getRealtimeAlarms: async () => {
try {
// 模拟实时报警数据
const contents = ['温度超标', '湿度异常', '压力偏高', '洁净度超标', '设备故障', '通讯中断'];
const types = ['参数超标', '环境异常', '设备故障', '电气故障', '通讯故障'];
const levels = ['A', 'B', 'C'];
const actions = ['处理中', '已恢复', '待处理', '检查中'];
const newAlarm = {
content: contents[Math.floor(Math.random() * contents.length)],
type: types[Math.floor(Math.random() * types.length)],
time: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '-').replace(', ', '-'),
level: levels[Math.floor(Math.random() * levels.length)],
action: actions[Math.floor(Math.random() * actions.length)],
actionTime: new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).replace(/\//g, '-').replace(', ', '-')
};
// 添加到报警列表顶部
alarmList.value.unshift(newAlarm);
// 限制报警数量保持最新的50条
if (alarmList.value.length > 50) {
alarmList.value = alarmList.value.slice(0, 50);
}
// 自动滚动到顶部显示最新报警
setTimeout(() => {
scrollToTop();
}, 100);
return Promise.resolve(newAlarm);
} catch (error) {
console.error('获取实时报警失败:', error);
return Promise.reject(error);
}
},
// 确认报警
confirmAlarm: async (alarmId) => {
try {
// 模拟确认操作
await new Promise(resolve => setTimeout(resolve, 300));
uni.showToast({
title: '报警已确认',
icon: 'success'
});
return Promise.resolve();
} catch (error) {
console.error('确认报警失败:', error);
uni.showToast({
title: '确认失败',
icon: 'error'
});
return Promise.reject(error);
}
},
// 清空报警记录
clearAlarms: async () => {
try {
// 模拟清空操作
await new Promise(resolve => setTimeout(resolve, 300));
alarmList.value = [];
uni.showToast({
title: '报警记录已清空',
icon: 'success'
});
return Promise.resolve();
} catch (error) {
console.error('清空报警记录失败:', error);
uni.showToast({
title: '清空失败',
icon: 'error'
});
return Promise.reject(error);
}
},
// 断开连接
disconnect: () => {
console.log('MQTT报警服务断开连接');
isConnected.value = false;
// 这里后期会实现真实的MQTT断开
}
};
// 映射报警级别到显示格式
const mapLevelToDisplay = (level) => {
const levelMap = {
'高危': 'A',
'中危': 'B',
'低危': 'C',
'high': 'A',
'medium': 'B',
'low': 'C'
};
return levelMap[level] || 'C';
};
// 格式化时间
const formatDateTime = (dateString) => {
if (!dateString) return '未知时间';
try {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} catch (error) {
console.error('时间格式化失败:', error);
return dateString; // 如果格式化失败,返回原始字符串
}
};
// 滚动事件处理
let scrollTimer = null;
const onScroll = (e) => {
isScrolling.value = true;
// 防抖处理,避免频繁触发
if (scrollTimer) {
clearTimeout(scrollTimer);
}
scrollTimer = setTimeout(() => {
isScrolling.value = false;
}, 150);
// 可以在这里添加滚动时的逻辑
};
const onScrollToLower = () => {
// 如果正在加载或没有更多数据,则不处理
if (isLoadingMore.value || !hasMoreData.value) {
console.log('正在加载中或无更多数据,跳过');
return;
}
// 加载下一页数据
const nextPage = currentPage.value + 1;
console.log(`📄 加载第${nextPage}页数据`);
mqttAlarmService.getHistoryAlarms(nextPage, pageSize.value, true);
};
// 滚动到顶部
const scrollToTop = () => {
scrollTop.value = scrollTop.value === 0 ? 1 : 0;
};
// 日期选择处理,选择后直接查询
const onDateChange = async (e) => {
selectedDate.value = e.detail.value;
// 自动构建开始和结束时间
if (selectedDate.value) {
startTime.value = `${selectedDate.value} 00:00:00`;
endTime.value = `${selectedDate.value} 23:59:59`;
try {
// 重置分页状态
currentPage.value = 0;
hasMoreData.value = true;
alarmList.value = [];
// 使用选择的日期范围查询
await mqttAlarmService.getHistoryAlarms(0, pageSize.value, false);
} catch (error) {
console.error('❌ 查询失败:', error);
}
}
};
// 滚动到底部
const scrollToBottom = () => {
// 使用nextTick确保DOM更新完成
nextTick(() => {
// 计算滚动到底部的位置
const scrollHeight = alarmList.value.length * 80; // 假设每行80rpx
scrollTop.value = scrollHeight + 100; // 额外100rpx确保完全滚动到底部
});
};
// 定时获取实时报警
let realtimeTimer = null;
const startRealtimeAlarm = () => {
if (realtimeTimer) return;
realtimeTimer = setInterval(() => {
if (isConnected.value && !isLoading.value) {
// 20%概率生成新报警
if (Math.random() < 0.2) {
mqttAlarmService.getRealtimeAlarms();
}
}
}, 8000); // 每8秒检查一次
};
const stopRealtimeAlarm = () => {
if (realtimeTimer) {
clearInterval(realtimeTimer);
realtimeTimer = null;
}
};
// 创建查询事件
const createQueryEvent = async (status, dataCount) => {
// try {
// const currentTime = formatDateTime(new Date().toISOString())
// const queryEvent = {
// eventType: "报警记录查询",
// eventTime: currentTime,
// status: getEventStatus(status),
// description: getEventDescription(status, dataCount),
// deviceId: "ALARM_QUERY_001"
// }
// console.log('📤 提交报警查询事件:', queryEvent)
// const response = await eventApi.create(queryEvent)
// console.log('✅ 报警查询事件创建成功:', response)
// } catch (error) {
// console.error('❌ 报警查询事件创建失败:', error)
// }
};
// 获取事件状态
const getEventStatus = (status) => {
const statusMap = {
'success': '已完成',
'empty': '已完成',
'error': '失败'
};
return statusMap[status] || '未知';
};
// 获取事件描述
const getEventDescription = (status, dataCount) => {
const descriptions = {
'success': `成功查询到${dataCount}条报警记录数据`,
'empty': '查询报警记录,但未找到数据',
'error': '查询报警记录时发生错误'
};
return descriptions[status] || '未知查询状态';
};
// 刷新数据方法
const refreshData = async () => {
try {
// 重置分页状态
currentPage.value = 0;
hasMoreData.value = true;
alarmList.value = [];
// 重新获取第一页数据
await mqttAlarmService.getHistoryAlarms(0, pageSize.value, false);
} catch (error) {
console.error('❌ 刷新数据失败:', error);
}
};
// 暴露方法给父组件
defineExpose({
refreshData
});
// 组件生命周期
onMounted(async () => {
try {
// 连接MQTT并初始化
// await mqttAlarmService.connect();
// await mqttAlarmService.subscribeAlarmData();
await mqttAlarmService.getHistoryAlarms(0, pageSize.value, false);
// 开始实时报警获取
// startRealtimeAlarm();
} catch (error) {
console.error('报警系统初始化失败:', error);
uni.showToast({
title: '连接失败',
icon: 'error'
});
}
});
onUnmounted(() => {
stopRealtimeAlarm();
mqttAlarmService.disconnect();
// 清理滚动定时器
if (scrollTimer) {
clearTimeout(scrollTimer);
scrollTimer = null;
}
});
</script>
<style lang="scss">
@import '@/styles/common.scss';
.alarm-record-container {
// 继承通用页面容器样式
display: flex;
flex-direction: column;
height: 100%;
}
// 日期选择器容器样式
.date-picker-container {
display: flex;
align-items: center;
// justify-content: center;
padding: 20rpx 30rpx;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-bottom: 2rpx solid #e8eaed;
}
.date-picker {
width: 100%;
// max-width: 500rpx;
}
.picker-input {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 24rpx;
background: #ffffff;
border: 2rpx solid #dadce0;
border-radius: 8rpx;
transition: all 0.3s ease;
}
.picker-input:active {
border-color: #1a73e8;
background: #f1f5ff;
}
.picker-text {
font-size: 26rpx;
color: #3c4043;
flex: 1;
}
.picker-icon {
font-size: 20rpx;
color: #5f6368;
margin-left: 12rpx;
}
.alarm-content {
// 继承通用内容样式
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.alarm-table {
// 继承通用表格样式
@extend .common-table;
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.table-header {
display: flex;
background: #f8f9fa;
color: #3c4043;
border-bottom: 1rpx solid #e8eaed;
}
.header-cell {
font-weight: 500;
font-size: 26rpx;
padding: 20rpx 16rpx;
text-align: center;
letter-spacing: 0.2rpx;
}
.table-body {
flex: 1;
height: 0; /* 重要配合flex: 1使用确保正确计算高度 */
overflow-y: auto;
overflow-x: hidden;
/* 滚动条样式 */
scrollbar-width: thin;
scrollbar-color: #dadce0 #f1f3f4;
/* 平滑滚动 */
scroll-behavior: smooth;
}
/* Webkit浏览器滚动条样式 */
.table-body::-webkit-scrollbar {
width: 8rpx;
}
.table-body::-webkit-scrollbar-track {
background: rgba(241, 243, 244, 0.5);
border-radius: 4rpx;
margin: 4rpx 0;
}
.table-body::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #dadce0 0%, #bdc1c6 100%);
border-radius: 4rpx;
border: 1rpx solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.table-body::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #bdc1c6 0%, #9aa0a6 100%);
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.table-body::-webkit-scrollbar-thumb:active {
background: linear-gradient(180deg, #9aa0a6 0%, #5f6368 100%);
}
.table-row {
display: flex;
border-bottom: 1rpx solid #e8eaed;
transition: all 0.2s ease;
min-height: 80rpx;
}
.table-row.even-row {
background-color: rgba(248, 249, 250, 0.5);
}
.table-row:hover {
background-color: rgba(241, 243, 244, 0.8);
transform: translateX(2rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.table-row:last-child {
border-bottom: none;
}
.table-cell {
padding: 16rpx 16rpx;
font-size: 24rpx;
color: #3c4043;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
min-height: 80rpx;
line-height: 1.4;
word-wrap: break-word;
word-break: break-all;
transition: color 0.2s ease;
}
.content-column {
flex: 3;
justify-content: flex-start;
text-align: left;
padding-left: 30rpx;
}
.type-column {
flex: 2;
}
.time-column {
flex: 2.5;
}
.level-column {
flex: 1;
font-weight: bold;
}
.action-column {
flex: 1.5;
}
.action-time-column {
flex: 2.5;
}
.level-a {
color: #ea4335;
font-weight: 700;
position: relative;
background: linear-gradient(135deg, rgba(234, 67, 53, 0.1) 0%, rgba(234, 67, 53, 0.05) 100%);
border-radius: 8rpx;
padding: 8rpx 12rpx;
border: 1rpx solid rgba(234, 67, 53, 0.2);
}
.level-a::before {
content: '🔴';
margin-right: 6rpx;
font-size: 18rpx;
}
.level-b {
color: #fbbc04;
font-weight: 700;
position: relative;
background: linear-gradient(135deg, rgba(251, 188, 4, 0.1) 0%, rgba(251, 188, 4, 0.05) 100%);
border-radius: 8rpx;
padding: 8rpx 12rpx;
border: 1rpx solid rgba(251, 188, 4, 0.2);
}
.level-b::before {
content: '🟡';
margin-right: 6rpx;
font-size: 18rpx;
}
.level-c {
color: #34a853;
font-weight: 700;
position: relative;
background: linear-gradient(135deg, rgba(52, 168, 83, 0.1) 0%, rgba(52, 168, 83, 0.05) 100%);
border-radius: 8rpx;
padding: 8rpx 12rpx;
border: 1rpx solid rgba(52, 168, 83, 0.2);
}
.level-c::before {
content: '🟢';
margin-right: 6rpx;
font-size: 18rpx;
}
/* 移除空行样式,不再需要 */
.table-bottom-spacing {
height: 40rpx;
background-color: transparent;
}
.table-loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx 20rpx;
gap: 20rpx;
background-color: #ffffff;
}
.table-loading-spinner {
width: 48rpx;
height: 48rpx;
border: 3rpx solid #e8eaed;
border-top: 3rpx solid #5f6368;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.table-loading-text {
font-size: 24rpx;
color: #5f6368;
font-weight: 400;
}
.table-empty-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 80rpx;
gap: 16rpx;
}
.table-empty-text {
font-size: 26rpx;
color: #9aa0a6;
font-weight: 400;
}
.table-loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #e9ecef;
border-top: 4rpx solid #6c757d;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.table-loading-text {
font-size: 28rpx;
color: #6c757d;
font-weight: 500;
}
.table-empty-container {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx 20rpx;
background-color: #ffffff;
}
.table-empty-text {
font-size: 32rpx;
color: #999;
font-weight: 500;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60rpx;
gap: 20rpx;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 152, 0, 0.3);
border-top: 4rpx solid #ff9800;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
font-size: 28rpx;
color: #666;
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx;
}
.empty-text {
font-size: 32rpx;
color: #adb5bd;
}
// 响应式设计 - 平板设备适配
@media (min-width: 768px) and (max-width: 1024px) {
.header-cell {
font-size: 28rpx;
padding: 24rpx 20rpx;
}
.table-cell {
font-size: 26rpx;
padding: 20rpx 16rpx;
}
.empty-text {
font-size: 28rpx;
}
.date-picker-container {
padding: 24rpx 32rpx;
}
.date-picker {
max-width: 600rpx;
}
.picker-text {
font-size: 28rpx;
}
}
// 响应式设计 - 手机设备适配
@media (max-width: 750rpx) {
.header-cell {
font-size: 22rpx;
padding: 16rpx 12rpx;
}
.table-cell {
font-size: 20rpx;
padding: 12rpx 8rpx;
}
.empty-text {
font-size: 28rpx;
}
.date-picker-container {
padding: 16rpx 20rpx;
}
.date-picker {
max-width: 100%;
}
.picker-text {
font-size: 24rpx;
}
}
// 响应式设计 - 大屏设备适配
@media (min-width: 1200px) {
.header-cell {
font-size: 30rpx;
padding: 28rpx 24rpx;
}
.table-cell {
font-size: 28rpx;
padding: 24rpx 20rpx;
}
.empty-text {
font-size: 30rpx;
}
}
// 加载更多样式
.load-more-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30rpx 20rpx;
gap: 15rpx;
background-color: #ffffff;
}
.load-more-spinner {
width: 40rpx;
height: 40rpx;
border: 3rpx solid #e8eaed;
border-top: 3rpx solid #5f6368;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.load-more-text {
font-size: 24rpx;
color: #5f6368;
font-weight: 400;
}
.no-more-container {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx 20rpx;
background-color: #ffffff;
}
.no-more-text {
font-size: 24rpx;
color: #9aa0a6;
font-weight: 400;
}
</style>