793 lines
20 KiB
Vue
793 lines
20 KiB
Vue
<template>
|
||
<view class="container">
|
||
<!-- 顶部状态栏 -->
|
||
<!-- <view class="status-bar"></view> -->
|
||
|
||
<!-- 头部 -->
|
||
<view class="header">
|
||
<view class="header-left" @click="goBack">
|
||
<text class="back-icon">←</text>
|
||
<text class="title">报警历史</text>
|
||
</view>
|
||
<view class="header-right">
|
||
<text class="refresh-text" @click="loadAlarmHistory" :class="{loading: isLoading}">
|
||
{{isLoading ? '刷新中...' : '刷新'}}
|
||
</text>
|
||
<text class="clear-text" @click="clearHistory">清空</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 筛选选项 -->
|
||
<view class="filter-bar">
|
||
<scroll-view scroll-x="true" class="filter-scroll">
|
||
<view class="filter-list">
|
||
<text
|
||
:class="['filter-item', selectedFilter === 'all' ? 'active' : '']"
|
||
@click="setFilter('all')"
|
||
>全部</text>
|
||
<text
|
||
:class="['filter-item', selectedFilter === 'danger' ? 'active' : '']"
|
||
@click="setFilter('danger')"
|
||
>高温危险</text>
|
||
<text
|
||
:class="['filter-item', selectedFilter === 'warning' ? 'active' : '']"
|
||
@click="setFilter('warning')"
|
||
>异常升温</text>
|
||
<text
|
||
:class="['filter-item', selectedFilter === 'unattended' ? 'active' : '']"
|
||
@click="setFilter('unattended')"
|
||
>无人值守</text>
|
||
<text
|
||
:class="['filter-item', selectedFilter === 'expanding' ? 'active' : '']"
|
||
@click="setFilter('expanding')"
|
||
>高温扩散</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 统计信息 -->
|
||
<view class="stats-card">
|
||
<view class="stats-grid">
|
||
<view class="stats-item">
|
||
<text class="stats-num">{{totalAlarms}}</text>
|
||
<text class="stats-label">总报警</text>
|
||
</view>
|
||
<view class="stats-item">
|
||
<text class="stats-num danger">{{dangerAlarms}}</text>
|
||
<text class="stats-label">高危</text>
|
||
</view>
|
||
<view class="stats-item">
|
||
<text class="stats-num warning">{{warningAlarms}}</text>
|
||
<text class="stats-label">警告</text>
|
||
</view>
|
||
<view class="stats-item">
|
||
<text class="stats-num today">{{todayAlarms}}</text>
|
||
<text class="stats-label">今日</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 报警列表 -->
|
||
<view class="history-list">
|
||
<view class="date-group" v-for="group in filteredHistory" :key="group.date">
|
||
<view class="date-header">
|
||
<text class="date-text">{{group.date}}</text>
|
||
<text class="count-text">{{group.alarms.length}}条</text>
|
||
</view>
|
||
<view class="alarm-card" v-for="alarm in group.alarms" :key="alarm.id" @click="showAlarmDetail(alarm)">
|
||
<view class="alarm-header">
|
||
<view class="alarm-icon-wrapper" :class="alarm.level">
|
||
<text class="alarm-icon">{{alarm.icon}}</text>
|
||
</view>
|
||
<view class="alarm-info">
|
||
<text class="alarm-title">{{alarm.title}}</text>
|
||
<text class="alarm-time">{{alarm.time}}</text>
|
||
</view>
|
||
<view class="alarm-temp">
|
||
<text class="temp-value">{{alarm.temp}}°C</text>
|
||
</view>
|
||
</view>
|
||
<view class="alarm-content">
|
||
<text class="alarm-desc">{{alarm.desc}}</text>
|
||
<text class="alarm-location">热点区域: {{alarm.location}}</text>
|
||
</view>
|
||
<view class="alarm-actions">
|
||
<text class="action-btn" @click.stop="markAsRead(alarm)">标记已读</text>
|
||
<text class="action-btn" @click.stop="deleteAlarm(alarm)">删除</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view class="empty-state" v-if="filteredHistory.length === 0">
|
||
<text class="empty-icon">📋</text>
|
||
<text class="empty-text">暂无报警记录</text>
|
||
<text class="empty-desc">系统将自动记录所有报警事件</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { getAlarmHistoryUrl } from '@/utils/esp32Client.js'
|
||
import { requestEsp32Api } from '@/utils/networkHelper.js'
|
||
import { syncEsp32AlarmHistoryToCloud } from '@/utils/alarmSync.js'
|
||
import { listCloudAlarms, markCloudAlarmRead } from '@/utils/alarmCloudApi.js'
|
||
import { getAllDevices } from '@/utils/deviceStore.js'
|
||
import { getToken } from '@/utils/authStore.js'
|
||
export default {
|
||
data() {
|
||
return {
|
||
selectedFilter: 'all',
|
||
isLoading: false,
|
||
refreshTimer: null,
|
||
alarmHistory: []
|
||
}
|
||
},
|
||
computed: {
|
||
filteredHistory() {
|
||
if (this.selectedFilter === 'all') {
|
||
return this.alarmHistory
|
||
}
|
||
|
||
return this.alarmHistory.map(group => ({
|
||
...group,
|
||
alarms: group.alarms.filter(alarm => {
|
||
switch (this.selectedFilter) {
|
||
case 'danger':
|
||
return alarm.level === 'danger'
|
||
case 'warning':
|
||
return alarm.level === 'warning'
|
||
case 'unattended':
|
||
return alarm.title.includes('无人值守')
|
||
case 'expanding':
|
||
return alarm.title.includes('扩散')
|
||
default:
|
||
return true
|
||
}
|
||
})
|
||
})).filter(group => group.alarms.length > 0)
|
||
},
|
||
totalAlarms() {
|
||
return this.alarmHistory.reduce((total, group) => total + group.alarms.length, 0)
|
||
},
|
||
dangerAlarms() {
|
||
return this.alarmHistory.reduce((total, group) =>
|
||
total + group.alarms.filter(alarm => alarm.level === 'danger').length, 0)
|
||
},
|
||
warningAlarms() {
|
||
return this.alarmHistory.reduce((total, group) =>
|
||
total + group.alarms.filter(alarm => alarm.level === 'warning').length, 0)
|
||
},
|
||
todayAlarms() {
|
||
const todayGroup = this.alarmHistory.find(group => group.date === '今天')
|
||
return todayGroup ? todayGroup.alarms.length : 0
|
||
}
|
||
},
|
||
onLoad() {
|
||
this.loadAlarmHistory()
|
||
// 设置定时刷新,每30秒自动刷新一次
|
||
this.refreshTimer = setInterval(() => {
|
||
this.loadAlarmHistory()
|
||
}, 30000)
|
||
},
|
||
onUnload() {
|
||
// 页面卸载时清除定时器
|
||
if (this.refreshTimer) {
|
||
clearInterval(this.refreshTimer)
|
||
this.refreshTimer = null
|
||
}
|
||
},
|
||
methods: {
|
||
parseAlarmTimestamp(alarm) {
|
||
// 尝试多种时间格式解析
|
||
let timestamp = null
|
||
|
||
// 0. 优先使用timestampMs字段(设备端新增字段)
|
||
if (alarm.timestampMs && typeof alarm.timestampMs === 'number') {
|
||
timestamp = alarm.timestampMs
|
||
}
|
||
|
||
// 1. 尝试timestamp_ms字段
|
||
else if (alarm.timestamp_ms && typeof alarm.timestamp_ms === 'number') {
|
||
timestamp = alarm.timestamp_ms
|
||
}
|
||
// 2. 尝试timestamp字段
|
||
else if (alarm.timestamp) {
|
||
if (typeof alarm.timestamp === 'number') {
|
||
// 如果是秒级时间戳,转换为毫秒
|
||
timestamp = alarm.timestamp < 1000000000000 ? alarm.timestamp * 1000 : alarm.timestamp
|
||
} else if (typeof alarm.timestamp === 'string') {
|
||
// 尝试解析字符串时间
|
||
const parsed = Date.parse(alarm.timestamp)
|
||
if (!isNaN(parsed)) {
|
||
timestamp = parsed
|
||
}
|
||
}
|
||
}
|
||
// 3. 尝试ESP32的time+date字段组合 (如: time="15:44:57", date="01-13")
|
||
else if (alarm.time && alarm.date && typeof alarm.time === 'string' && typeof alarm.date === 'string') {
|
||
try {
|
||
// 解析日期和时间
|
||
const currentYear = new Date().getFullYear()
|
||
const [month, day] = alarm.date.split('-').map(Number)
|
||
const [hour, minute, second] = alarm.time.split(':').map(Number)
|
||
|
||
// 构造完整日期
|
||
const alarmDate = new Date(currentYear, month - 1, day, hour, minute, second)
|
||
timestamp = alarmDate.getTime()
|
||
} catch (error) {
|
||
console.error('解析ESP32时间失败:', error, alarm)
|
||
}
|
||
}
|
||
// 4. 尝试单独time字段
|
||
else if (alarm.time && typeof alarm.time === 'string') {
|
||
const parsed = Date.parse(alarm.time)
|
||
if (!isNaN(parsed)) {
|
||
timestamp = parsed
|
||
}
|
||
}
|
||
// 5. 尝试created_at或类似字段
|
||
else if (alarm.created_at) {
|
||
const parsed = Date.parse(alarm.created_at)
|
||
if (!isNaN(parsed)) {
|
||
timestamp = parsed
|
||
}
|
||
}
|
||
|
||
// 如果都解析失败,使用当前时间但记录警告
|
||
if (!timestamp) {
|
||
console.warn('⚠️ 报警历史: 无法解析报警时间,使用当前时间:', alarm)
|
||
timestamp = Date.now()
|
||
}
|
||
|
||
return timestamp
|
||
},
|
||
|
||
goBack() {
|
||
uni.navigateBack()
|
||
},
|
||
getCloudDeviceUid() {
|
||
// 你可以按自己的实际绑定逻辑改:这里先允许从多个地方取值
|
||
try {
|
||
const dcRaw = uni.getStorageSync('cloud_device_config')
|
||
const dc = dcRaw ? (typeof dcRaw === 'string' ? JSON.parse(dcRaw) : dcRaw) : null
|
||
if (dc && dc.deviceUid) return String(dc.deviceUid)
|
||
} catch (e) {}
|
||
try {
|
||
const v = uni.getStorageSync('cloud_device_uid')
|
||
if (v) return String(v)
|
||
} catch (e) {}
|
||
try {
|
||
const list = getAllDevices() || []
|
||
const kitchen = list.find(d => d && d.type === 'host' && d.id === 'dev_host_2')
|
||
if (kitchen && kitchen.id) return String(kitchen.id)
|
||
const host = list.find(d => d && d.type === 'host' && d.id)
|
||
if (host && host.id) return String(host.id)
|
||
} catch (e) {}
|
||
return ''
|
||
},
|
||
getEsp32AlarmHistoryUrlFallback() {
|
||
try {
|
||
const list = getAllDevices() || []
|
||
const host = list.find(d => d && d.type === 'host' && d.ip)
|
||
if (host && host.ip) return 'http://' + String(host.ip) + '/api/alarm/history'
|
||
} catch (e) {}
|
||
return ''
|
||
},
|
||
async loadAlarmHistory() {
|
||
if (this.isLoading) return
|
||
|
||
this.isLoading = true
|
||
try {
|
||
const token = getToken()
|
||
if (token) {
|
||
// 1) 同步ESP32->云端(失败不阻断)
|
||
try {
|
||
const deviceUid = this.getCloudDeviceUid()
|
||
if (deviceUid) {
|
||
await syncEsp32AlarmHistoryToCloud({ deviceUid })
|
||
}
|
||
} catch (e) {
|
||
console.warn('⚠️ 同步云端报警历史失败:', e)
|
||
}
|
||
|
||
// 2) 从云端加载(优先)
|
||
try {
|
||
const deviceUid = this.getCloudDeviceUid()
|
||
const cloudRes = await listCloudAlarms({ deviceUid, page: 0, size: 200 })
|
||
const ok = cloudRes && cloudRes.statusCode === 200 && cloudRes.data && cloudRes.data.code === 200
|
||
if (ok && Array.isArray(cloudRes.data.data)) {
|
||
const cloudAlarms = cloudRes.data.data.map(a => {
|
||
let occurredAtMs = 0
|
||
try {
|
||
occurredAtMs = a && a.occurredAt ? Date.parse(a.occurredAt) : 0
|
||
} catch (e) {
|
||
occurredAtMs = 0
|
||
}
|
||
return {
|
||
id: a.id,
|
||
timestampMs: occurredAtMs || 0,
|
||
title: a.title || (a.alarmType ? String(a.alarmType) : '报警'),
|
||
desc: a.message || '',
|
||
temp: typeof a.temp === 'number' ? a.temp : 0,
|
||
level: a.level || 'warning',
|
||
isRead: !!a.isRead,
|
||
location: '厨房区域'
|
||
}
|
||
})
|
||
this.parseAlarmHistory(cloudAlarms)
|
||
return
|
||
}
|
||
} catch (e) {
|
||
console.warn('⚠️ 云端报警历史加载失败,回退ESP32:', e)
|
||
}
|
||
}
|
||
|
||
// 3) 回退展示ESP32本地历史
|
||
console.log('🔄 获取ESP32报警历史数据...')
|
||
const url = getAlarmHistoryUrl() || this.getEsp32AlarmHistoryUrlFallback()
|
||
if (!url) {
|
||
throw new Error('ESP32 baseUrl not configured')
|
||
}
|
||
const response = await requestEsp32Api({ url, method: 'GET' })
|
||
if (response.statusCode === 200 && response.data && response.data.alarms) {
|
||
this.parseAlarmHistory(response.data.alarms)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 获取报警历史失败:', error)
|
||
uni.showToast({ title: '获取历史数据失败', icon: 'none' })
|
||
} finally {
|
||
this.isLoading = false
|
||
}
|
||
},
|
||
parseAlarmHistory(alarms) {
|
||
// 按日期分组报警记录
|
||
const groupedAlarms = {}
|
||
const today = new Date()
|
||
const yesterday = new Date(today)
|
||
yesterday.setDate(yesterday.getDate() - 1)
|
||
|
||
console.log('解析报警历史数据:', alarms.length, '条记录')
|
||
|
||
alarms.forEach(alarm => {
|
||
const timestamp = this.parseAlarmTimestamp(alarm)
|
||
const alarmTime = new Date(timestamp)
|
||
const alarmDate = this.formatDate(alarmTime)
|
||
let dateKey = alarmDate
|
||
|
||
if (this.formatDate(alarmTime) === this.formatDate(today)) {
|
||
dateKey = '今天'
|
||
} else if (this.formatDate(alarmTime) === this.formatDate(yesterday)) {
|
||
dateKey = '昨天'
|
||
}
|
||
|
||
if (!groupedAlarms[dateKey]) {
|
||
groupedAlarms[dateKey] = {
|
||
date: dateKey,
|
||
alarms: []
|
||
}
|
||
}
|
||
|
||
const iconMap = {
|
||
'高温危险': '🔥',
|
||
'异常升温': '⚠️',
|
||
'高温移动': '🏃',
|
||
'燃烧报警': '🔥',
|
||
'高温扩散': '🌡️',
|
||
'无人值守': '👤'
|
||
}
|
||
|
||
let level = alarm.level || 'warning'
|
||
if (alarm.title && (alarm.title.includes('高温危险') || alarm.title.includes('燃烧'))) {
|
||
level = 'danger'
|
||
}
|
||
|
||
const title = alarm.title || '报警'
|
||
const formattedAlarm = {
|
||
id: alarm.id,
|
||
icon: iconMap[String(title).replace('报警', '')] || '⚠️',
|
||
title: title,
|
||
time: alarmTime.getHours() + ':' + String(alarmTime.getMinutes()).padStart(2, '0'),
|
||
temp: alarm.temp || 0,
|
||
level: level,
|
||
desc: alarm.desc || alarm.message || '温度异常',
|
||
location: alarm.location || '厨房区域',
|
||
isRead: alarm.isRead || false
|
||
}
|
||
|
||
groupedAlarms[dateKey].alarms.push(formattedAlarm)
|
||
})
|
||
|
||
// 对每个日期组内的报警按时间倒序排列(最新的在前)
|
||
Object.values(groupedAlarms).forEach(group => {
|
||
group.alarms.sort((a, b) => {
|
||
// 根据时间字符串排序,格式为 "HH:MM"
|
||
const timeA = a.time.split(':').map(Number)
|
||
const timeB = b.time.split(':').map(Number)
|
||
const minutesA = timeA[0] * 60 + timeA[1]
|
||
const minutesB = timeB[0] * 60 + timeB[1]
|
||
return minutesB - minutesA // 倒序,最新的在前
|
||
})
|
||
})
|
||
|
||
// 按日期排序:今天 > 昨天 > 其他日期(按日期倒序)
|
||
this.alarmHistory = Object.values(groupedAlarms).sort((a, b) => {
|
||
const order = { '今天': 0, '昨天': 1 }
|
||
const aOrder = order[a.date]
|
||
const bOrder = order[b.date]
|
||
|
||
// 如果都是特殊日期(今天、昨天),按预定义顺序
|
||
if (aOrder !== undefined && bOrder !== undefined) {
|
||
return aOrder - bOrder
|
||
}
|
||
|
||
// 如果一个是特殊日期,一个不是,特殊日期优先
|
||
if (aOrder !== undefined) return -1
|
||
if (bOrder !== undefined) return 1
|
||
|
||
// 如果都不是特殊日期,按日期字符串倒序(最新的在前)
|
||
return b.date.localeCompare(a.date)
|
||
})
|
||
|
||
if (this.alarmHistory.length === 0) {
|
||
console.log('没有报警历史数据')
|
||
}
|
||
},
|
||
formatDate(date) {
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
return `${month}-${day}`
|
||
},
|
||
setFilter(filter) {
|
||
this.selectedFilter = filter
|
||
},
|
||
clearHistory() {
|
||
uni.showModal({
|
||
title: '确认清空',
|
||
content: '确定要清空所有报警历史记录吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.alarmHistory = []
|
||
uni.showToast({
|
||
title: '已清空历史记录',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
showAlarmDetail(alarm) {
|
||
// 显示报警详情
|
||
uni.showModal({
|
||
title: alarm.title,
|
||
content: `时间: ${alarm.time}\n温度: ${alarm.temp}°C\n位置: ${alarm.location}\n描述: ${alarm.desc}`,
|
||
showCancel: false
|
||
})
|
||
},
|
||
markAsRead(alarm) {
|
||
alarm.isRead = true
|
||
uni.showToast({
|
||
title: '已标记为已读',
|
||
icon: 'success'
|
||
})
|
||
},
|
||
deleteAlarm(alarm) {
|
||
uni.showModal({
|
||
title: '确认删除',
|
||
content: '确定要删除这条报警记录吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
// 找到并删除报警记录
|
||
this.alarmHistory.forEach(group => {
|
||
const index = group.alarms.findIndex(a => a.id === alarm.id)
|
||
if (index !== -1) {
|
||
group.alarms.splice(index, 1)
|
||
}
|
||
})
|
||
|
||
// 删除空的日期组
|
||
this.alarmHistory = this.alarmHistory.filter(group => group.alarms.length > 0)
|
||
|
||
uni.showToast({
|
||
title: '已删除',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style>
|
||
.container {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding-bottom: 40rpx;
|
||
}
|
||
|
||
.status-bar {
|
||
background: #3498DB;
|
||
height: 0;
|
||
padding-top: constant(safe-area-inset-top);
|
||
padding-top: env(safe-area-inset-top);
|
||
}
|
||
|
||
.header {
|
||
background: #3498DB;
|
||
padding: 20rpx 30rpx 40rpx;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.back-icon {
|
||
font-size: 36rpx;
|
||
color: #fff;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.refresh-text {
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
padding: 8rpx 16rpx;
|
||
border-radius: 16rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.refresh-text.loading {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.clear-text {
|
||
font-size: 28rpx;
|
||
color: #fff;
|
||
}
|
||
|
||
/* 筛选栏 */
|
||
.filter-bar {
|
||
background: #fff;
|
||
padding: 20rpx 0;
|
||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.filter-scroll {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-list {
|
||
display: flex;
|
||
padding: 0 20rpx;
|
||
}
|
||
|
||
.filter-item {
|
||
padding: 12rpx 24rpx;
|
||
margin-right: 16rpx;
|
||
background: #f5f5f5;
|
||
border-radius: 24rpx;
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.filter-item.active {
|
||
background: #9B59B6;
|
||
color: #fff;
|
||
}
|
||
|
||
/* 统计卡片 */
|
||
.stats-card {
|
||
background: #fff;
|
||
margin: 20rpx;
|
||
border-radius: 16rpx;
|
||
padding: 30rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.stats-grid {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
}
|
||
|
||
.stats-item {
|
||
text-align: center;
|
||
}
|
||
|
||
.stats-num {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.stats-num.danger {
|
||
color: #E74C3C;
|
||
}
|
||
|
||
.stats-num.warning {
|
||
color: #F39C12;
|
||
}
|
||
|
||
.stats-num.today {
|
||
color: #3498DB;
|
||
}
|
||
|
||
.stats-label {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
display: block;
|
||
}
|
||
|
||
/* 历史列表 */
|
||
.history-list {
|
||
margin: 20rpx;
|
||
}
|
||
|
||
.date-group {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.date-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16rpx 0;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.date-text {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.count-text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.alarm-card {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
margin-bottom: 16rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.alarm-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.alarm-icon-wrapper {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 20rpx;
|
||
}
|
||
|
||
.alarm-icon-wrapper.danger {
|
||
background: rgba(231, 76, 60, 0.1);
|
||
}
|
||
|
||
.alarm-icon-wrapper.warning {
|
||
background: rgba(243, 156, 18, 0.1);
|
||
}
|
||
|
||
.alarm-icon {
|
||
font-size: 36rpx;
|
||
}
|
||
|
||
.alarm-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.alarm-title {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
|
||
.alarm-time {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
display: block;
|
||
}
|
||
|
||
.alarm-temp {
|
||
text-align: right;
|
||
}
|
||
|
||
.temp-value {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #E74C3C;
|
||
}
|
||
|
||
.alarm-content {
|
||
margin-bottom: 16rpx;
|
||
padding: 16rpx;
|
||
background: #f8f9fa;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.alarm-desc {
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.alarm-location {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
display: block;
|
||
}
|
||
|
||
.alarm-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
padding: 8rpx 16rpx;
|
||
font-size: 24rpx;
|
||
color: #3498DB;
|
||
background: rgba(52, 152, 219, 0.1);
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 120rpx 40rpx;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 120rpx;
|
||
display: block;
|
||
margin-bottom: 32rpx;
|
||
opacity: 0.3;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 32rpx;
|
||
color: #999;
|
||
display: block;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.empty-desc {
|
||
font-size: 24rpx;
|
||
color: #ccc;
|
||
display: block;
|
||
}
|
||
</style>
|