1004 lines
23 KiB
Vue
1004 lines
23 KiB
Vue
<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> |