Files
hazardousWaste_app/pages/components/EquipmentInfo.vue
2026-04-23 16:01:13 +08:00

902 lines
28 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>
<navigation :title="title" back-url="pages/intelligent/index"> </navigation>
<view class="warehouse-page contentBox">
<view class="warehouse-hero">
<view>
<text class="hero-sub">{{ projectName || '项目仓库' }}</text>
<view class="hero-title">{{ warehouseName || '仓库传感器看板' }}</view>
</view>
<view class="hero-meta ">
<view class="warehouse-hero-box">
<div class="summary-card mr-16 total">
<span class="label">设备总数</span>
<strong class="value">{{ sensorCards.length }}</strong>
</div>
<div class="summary-card mr-16 online">
<span class="label">在线设备</span>
<strong class="value">{{ onlineCount }}</strong>
</div>
<div class="summary-card offline">
<span class="label">离线设备</span>
<strong class="value">{{ offlineCount }}</strong>
</div>
</view>
<view class="report"> 最近上报:{{ latestReportTime || '--' }} </view>
</view>
</view>
<view class="sensor-grid">
<view v-if="loading" class="loading-view">加载中...</view>
<view v-else-if="sensorCards.length === 0" class="empty-state">
当前仓库暂无传感器设备
</view>
<view v-for="item in sensorCards" :key="item.cardKey" :class="['sensor-card', item.statusClass]">
<view class="sensor-content">
<view class="sensor-main">
<view class="sensor-top flex">
<view :class="['sensor-icon', item.type]">
<text>{{ item.icon }}</text>
</view>
<view v-if="isSocketControlType(item.type)" class="socket-actions">
<uv-switch v-model="controlLoadingMap[item.cardKey]" active-color="#165D46"
@change="(e) => handleSocketControl(e, item)" />
</view>
<!-- <view class="sensor-state">
<text class="dot"></text>
{{ item.onlineText }}
</view> -->
</view>
<view class="sensor-body">
<view class="flex justify-between align-center">
<view class="type-name mb-4">{{ item.typeName }} </view>
</view>
<view>
<text class="mr-4"> {{ item.summary }}</text>|
<text :class="['sensor-state', item.statusClass, 'ml-4']"> {{ item.onlineText }}</text>
</view>
<view v-if="isSocketControlType(item.type)" class="socket-actions">
<!-- <text class="control-btn on" :disabled="!!controlLoadingMap[item.cardKey]"
@tap="handleSocketControl(item, 'on')">
{{ getSocketActionText(item.type, 'on', !!controlLoadingMap[item.cardKey]) }}
</text>
<text class="control-btn off" :disabled="!!controlLoadingMap[item.cardKey]"
@tap="handleSocketControl(item, 'off')">
{{ getSocketActionText(item.type, 'off', !!controlLoadingMap[item.cardKey]) }}
</text> -->
</view>
<view v-else-if="item.type === 'switch'" class="switch-actions">
<view v-for="channel in [1, 2, 3]" :key="channel" class="switch-row">
<text class="ch-label">开关{{ channel }}</text>
<button class="control-btn on"
:disabled="!!controlLoadingMap[`${item.cardKey}-ch-${channel}`]"
@tap="handleLightSwitchControl(item, channel, 'on')">
{{ !!controlLoadingMap[`${item.cardKey}-ch-${channel}`] ? '发送中...' : '' }}
</button>
<button class="control-btn off"
:disabled="!!controlLoadingMap[`${item.cardKey}-ch-${channel}`]"
@tap="handleLightSwitchControl(item, channel, 'off')">
{{ !!controlLoadingMap[`${item.cardKey}-ch-${channel}`] ? '发送中...' : '' }}
</button>
</view>
</view>
</view>
</view>
<!-- <view class="metric-panel">
<text class="panel-title">实时指标</text>
<view class="metric-list">
<view v-for="metric in item.metrics" :key="metric.label" class="metric-item">
<text class="label">{{ metric.label }}</text>
<text class="value">{{ metric.value }}</text>
</view>
<view v-if="item.metrics.length === 0" class="metric-item muted">
<text class="label">状态</text>
<text class="value">等待数据</text>
</view>
</view>
</view> -->
</view>
<!-- <view class="sensor-footer">
<text>DevEUI{{ item.devEui || '--' }}</text>
<text>{{ item.reportTime || '暂无上报' }}</text>
</view> -->
</view>
</view>
</view>
</template>
<script setup>
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { controlLightSwitch, controlSocket, listMqttDevice, listMqttData, listMqttEvent } from '@/api/home'
import { closeWs, connectWs } from '../utils/ws'
import { onLoad } from "@dcloudio/uni-app";
import Navigation from './Navigation.vue';
const loading = ref(false)
const devices = ref([])
const title = ref('')
const latestDataMap = reactive({})
const controlLoadingMap = reactive({})
const ENV_CARD_TYPES = ['tempHum', 'toxicGas', 'hydrogenSulfide']
const deptId = ref('')
const projectName = ref('')
const warehouseName = ref('')
onLoad((options) => {
deptId.value = options?.deptId
projectName.value = options?.projectName
warehouseName.value = options?.warehouseName
title.value = options?.warehouseName
})
const sensorCards = computed(() => devices.value.flatMap((device) => expandDeviceCards(device)))
const onlineCount = computed(() => sensorCards.value.filter(i => i.isOnline).length)
const offlineCount = computed(() => Math.max(sensorCards.value.length - onlineCount.value, 0))
const latestReportTime = computed(() => {
const times = sensorCards.value.map(i => i.reportTime).filter(Boolean)
return times[0] || ''
})
function goBack() {
uni.navigateBack()
}
function loadDevices() {
if (!deptId.value) {
devices.value = []
return
}
loading.value = true
listMqttDevice({
pageNum: 1,
pageSize: 100,
deptId: deptId.value
}).then(res => {
devices.value = res.rows || []
return loadLatestData()
}).finally(() => {
loading.value = false
})
}
function loadLatestData() {
const tasks = devices.value.map(device => {
if (!device.id) return Promise.resolve()
const reqs = [
listMqttData({ pageNum: 1, pageSize: 1, deviceId: device.id }).then(res => {
const row = (res.rows || [])[0]
if (row) {
const key = getDeviceKey(device)
latestDataMap[key] = normalizePayload({ ...latestDataMap[key] || {}, ...row })
}
}).catch(() => { })
]
if (isStateDevice(device)) {
reqs.push(listMqttEvent({ pageNum: 1, pageSize: 1, deviceId: device.id }).then(res => {
const row = (res.rows || [])[0]
if (row) {
const key = getDeviceKey(device)
latestDataMap[key] = normalizePayload({ ...latestDataMap[key] || {}, ...row, ...deriveStateFromEvent(row) })
}
}).catch(() => { }))
}
return Promise.all(reqs)
})
return Promise.all(tasks)
}
function handleWsMessage(data) {
if (!data || String(data.deptId || '') !== String(deptId.value || '')) return
const runtimeStatus = data.deviceStatus != null ? String(data.deviceStatus) : null
const index = devices.value.findIndex(device =>
String(device.id || '') === String(data.deviceId || '') ||
String(device.devEui || '').toLowerCase() === String(data.devEui || '').toLowerCase()
)
if (index > -1) {
const device = devices.value[index]
const key = getDeviceKey(device)
const patched = patchRealtimeState(latestDataMap[key] || {}, data)
latestDataMap[key] = normalizePayload({ ...latestDataMap[key] || {}, ...patched })
devices.value[index] = { ...device, status: runtimeStatus ?? '0' }
return
}
const key = getDeviceKey(data)
const patched = patchRealtimeState(latestDataMap[key] || {}, data)
latestDataMap[key] = normalizePayload({ ...latestDataMap[key] || {}, ...patched })
}
function patchRealtimeState(prev, next) {
const res = { ...next }
const s1 = next.switch_1 ?? next.switch1
const s2 = next.switch_2 ?? next.switch2
const s3 = next.switch_3 ?? next.switch3
if (s1 != null) res.switch_1 = res.switch1 = s1
if (s2 != null) res.switch_2 = res.switch2 = s2
if (s3 != null) res.switch_3 = res.switch3 = s3
const water = deriveWaterStatus({ ...prev, ...next })
if (water != null) res.waterStatus = res.water_status = water
const door = deriveDoorStatus({ ...prev, ...next })
if (door != null) res.doorStatus = res.door_status = door
const socket = deriveSocketStatus({ ...prev, ...next })
if (socket != null) res.switchStatus = res.socket_status = res.socketStatus = socket
return res
}
async function handleSocketControl(e, item) {
console.log(e, item, '111');
const action = e ? 'on' : 'off'
console.log(action, '111');
if (action) {
const deviceId = item.id || item.deviceId
const devEui = item.devEui || item.devEUI || item.dev_eui
console.log('devEui', devEui);
if (!devEui) return uni.showToast({ title: '未找到设备', icon: 'none' })
const k = item.cardKey || String(deviceId)
if (controlLoadingMap[k]) return
controlLoadingMap[k] = true
try {
const status = action === 'on' ? 1 : 0
console.log(status, action, 'status');
await controlSocket({ devEui, deviceId, status })
syncSocketLocalState(item, status)
uni.showToast({ title: '指令已发送', icon: 'success' })
} finally {
controlLoadingMap[k] = false
}
}
}
async function handleLightSwitchControl(item, ch, action) {
const devEui = item.devEui || item.devEUI || item.dev_eui
if (!devEui) return uni.showToast({ title: '未找到设备', icon: 'none' })
const k = `${item.cardKey}-ch-${ch}`
if (controlLoadingMap[k]) return
controlLoadingMap[k] = true
try {
const status = action === 'on' ? 1 : 0
await controlLightSwitch({ devEui, channel: ch, status })
syncLightSwitchLocalState(item, ch, status)
uni.showToast({ title: '指令已发送', icon: 'success' })
} finally {
controlLoadingMap[k] = false
}
}
function syncSocketLocalState(item, status) {
const key = getDeviceKey(item)
if (!key) return
const now = new Date().toISOString()
latestDataMap[key] = normalizePayload({
...latestDataMap[key] || {},
socket_status: status, socketStatus: status, switchStatus: status,
gatewayTime: now, createTime: now
})
}
function syncLightSwitchLocalState(item, ch, status) {
const key = getDeviceKey(item)
if (!key) return
const now = new Date().toISOString()
latestDataMap[key] = normalizePayload({
...latestDataMap[key] || {},
deviceId: item.deviceId || item.id,
devEui: item.devEui || item.devEUI || item.dev_eui,
deviceName: item.deviceName,
[`switch_${ch}`]: status, [`switch${ch}`]: status,
gatewayTime: now, createTime: now
})
}
function expandDeviceCards(device) {
const data = latestDataMap[getDeviceKey(device)] || {}
const type = getSensorType(device, data)
if (type === 'env') return ENV_CARD_TYPES.map(t => buildSensorCard(device, t))
return [buildSensorCard(device)]
}
function buildSensorCard(device, displayType) {
const data = latestDataMap[getDeviceKey(device)] || {}
const type = displayType || getSensorType(device, data)
const metrics = buildDisplayMetrics(data, type)
const reportTime = formatTime(data.createTime || data.gatewayTime)
const isOnline = String(device.status) === '0'
return {
...device,
cardKey: `${getDeviceKey(device) || 's'}-${type}`,
type,
icon: getDisplayIcon(type),
typeName: getDisplayTypeName(type),
locationName: getLocationName(device, data),
summary: getDisplaySummary(data, type),
metrics,
reportTime,
isOnline,
onlineText: isOnline ? '在线' : '离线',
statusClass: isOnline ? 'online' : 'offline'
}
}
function isSocketControlType(type) {
return type === 'socket' || type === 'acSocket'
}
function getSocketActionText(type, action, loading) {
if (loading) return '发送中...'
if (type === 'acSocket') return action === 'on' ? '开启空调' : '关闭空调'
return action === 'on' ? '开启排风' : '关闭排风'
}
function getDisplayIcon(type) {
return type === 'acSocket' ? '空' : getSensorIcon(type)
}
function getDisplayTypeName(type) {
return type === 'acSocket' ? '智能空调插座' : getSensorTypeName(type)
}
function getDisplaySummary(data, type) {
if (type === 'acSocket') return `空调状态:${translateSwitch(data.switchStatus)}`
return getSensorSummary(data, type)
}
function buildDisplayMetrics(data, type) {
const m = buildMetrics(data, type === 'acSocket' ? 'socket' : type)
if (type === 'acSocket') {
return m.map(i => i.label === '插座状态' ? { ...i, label: '空调状态' } : i)
}
return m
}
function getDeviceKey(d) {
return d.deviceId || d.id || d.devEui || d.devEUI || d.dev_eui
}
function normalizePayload(row) {
const json = parseJson(row.dataJson) || parseJson(row.payload) || {}
const nested = parseJson(row.data) || {}
const water = deriveWaterStatus({ ...json, ...nested, ...row })
return {
...json, ...nested, ...row,
devEui: row.devEui || row.devEUI || json.devEui,
waterStatus: row.waterStatus ?? row.water_status ?? water,
water_status: row.water_status ?? row.waterStatus ?? water,
socket_status: row.socket_status ?? row.socketStatus,
switchStatus: row.switchStatus ?? row.socket_status,
switch_1: row.switch_1 ?? row.switch1, switch1: row.switch1 ?? row.switch_1,
switch_2: row.switch_2 ?? row.switch2, switch2: row.switch2 ?? row.switch_2,
switch_3: row.switch_3 ?? row.switch3, switch3: row.switch3 ?? row.switch_3,
doorStatus: row.doorStatus ?? row.door_status
}
}
function parseJson(v) {
if (typeof v !== 'string') return null
try { return JSON.parse(v) } catch { return null }
}
function deriveStateFromEvent(row) {
const evt = (row.eventType || row.event || '').toLowerCase()
const desc = (row.eventDesc || row.statusDesc || '').toLowerCase()
const res = {}
if (evt.includes('open') || desc.includes('打开')) res.doorStatus = 1
if (evt.includes('close') || desc.includes('关闭')) res.doorStatus = 0
if (evt.includes('on') || desc.includes('通电')) res.socket_status = res.switchStatus = 1
if (evt.includes('off') || desc.includes('断电')) res.socket_status = res.switchStatus = 0
const water = deriveWaterStatus(row)
if (water != null) res.waterStatus = res.water_status = water
return res
}
function deriveWaterStatus(data) {
const evt = (data.eventType || data.status || '').toLowerCase()
const desc = (data.eventDesc || data.statusDesc || '').toLowerCase()
if (evt === 'alarm' || desc.includes('浸水') || desc.includes('水浸')) return 1
if (evt === 'normal' || desc.includes('正常')) return 0
return data.waterStatus ?? data.water_status
}
function deriveDoorStatus(data) {
return data.doorStatus ?? data.door_status
}
function deriveSocketStatus(data) {
return data.switchStatus ?? data.socket_status ?? data.socketStatus
}
function getSensorType(device, data) {
const t = (device.deviceType || '').toLowerCase()
const name = (device.deviceName || '').toLowerCase()
if (t === 'smoke') return 'smoke'
if (t === 'water') return 'water'
if (t === 'door') return 'door'
if (t === 'env') return 'env'
if (t === 'switch') return 'switch'
if (t === 'socket') return name.includes('空调') ? 'acSocket' : 'socket'
if (name.includes('smoke')) return 'smoke'
if (name.includes('water')) return 'water'
if (name.includes('空调')) return 'acSocket'
if (name.includes('socket')) return 'socket'
if (name.includes('switch')) return 'switch'
if (name.includes('door')) return 'door'
if (name.includes('temp') || name.includes('hum')) return 'env'
return 'sensor'
}
function isStateDevice(device) {
const s = `${device.deviceType} ${device.deviceName}`.toLowerCase()
return s.includes('door') || s.includes('socket') || s.includes('switch')
}
function getSensorIcon(type) {
const map = { smoke: '烟', water: '水', socket: '电', switch: '灯', door: '门', env: '温', tempHum: '温', toxicGas: '氨', hydrogenSulfide: '硫' }
return map[type] || '测'
}
function getSensorTypeName(type) {
const map = { smoke: '烟雾传感器', water: '水浸传感器', socket: '智能排风', switch: '智慧照明', door: '门禁状态', env: '环境传感器', tempHum: '温湿度', toxicGas: '有毒气体', hydrogenSulfide: '硫化氢' }
return map[type] || '传感器'
}
function getLocationName(device) {
return device.deptName || warehouseName.value || '仓库'
}
function getSensorSummary(data, type) {
if (!Object.keys(data).length) return '等待上报'
if (type === 'water') return `水浸:${translateNormalAlarm(data.waterStatus)}`
if (type === 'smoke') return `烟雾:${translateSmokeStatus(data.status)}`
if (type === 'socket') return `插座:${translateSwitch(data.switchStatus)}`
if (type === 'switch') {
const arr = []
if (data.switch_1 != null) arr.push(`1${translateSwitchShort(data.switch_1)}`)
if (data.switch_2 != null) arr.push(`2${translateSwitchShort(data.switch_2)}`)
if (data.switch_3 != null) arr.push(`3${translateSwitchShort(data.switch_3)}`)
return arr.length ? `开关:${arr.join(' ')}` : '开关状态'
}
if (type === 'door') return `门禁:${translateSwitch(data.doorStatus)}`
if (type === 'tempHum') return `${data.temperature || '--'}℃/${data.humidity || '--'}%`
if (type === 'toxicGas') return `氨气:${data.nh3 || '--'}ppm`
if (type === 'hydrogenSulfide') return `硫化氢:${data.h2s || '--'}ppm`
if (type === 'env') return `${data.temperature || '--'}℃/${data.humidity || '--'}%`
return '已上报'
}
function buildMetrics(data, type) {
if (type === 'tempHum') return [{ label: '温度', value: data.temperature + '℃' }, { label: '湿度', value: data.humidity + '%' }, { label: '电量', value: data.battery + '%' }].filter(i => i.value)
if (type === 'toxicGas') return [{ label: '氨气', value: data.nh3 + 'ppm' }, { label: '电量', value: data.battery + '%' }].filter(i => i.value)
if (type === 'hydrogenSulfide') return [{ label: '硫化氢', value: data.h2s + 'ppm' }, { label: '电量', value: data.battery + '%' }].filter(i => i.value)
const list = []
if (data.temperature != null) list.push({ label: '温度', value: data.temperature + '℃' })
if (data.humidity != null) list.push({ label: '湿度', value: data.humidity + '%' })
if (data.nh3 != null) list.push({ label: '氨气', value: data.nh3 + 'ppm' })
if (data.h2s != null) list.push({ label: '硫化氢', value: data.h2s + 'ppm' })
if (data.battery != null) list.push({ label: '电量', value: data.battery + '%' })
if (type === 'water' && data.waterStatus != null) list.unshift({ label: '水浸', value: translateNormalAlarm(data.waterStatus) })
if (type === 'socket' && data.switchStatus != null) list.unshift({ label: '插座', value: translateSwitch(data.switchStatus) })
if (type === 'door' && data.doorStatus != null) list.unshift({ label: '门禁', value: translateSwitch(data.doorStatus) })
if (type === 'switch') {
const sw = []
if (data.switch_1 != null) sw.push({ label: '开关1', value: translateSwitchShort(data.switch_1) })
if (data.switch_2 != null) sw.push({ label: '开关2', value: translateSwitchShort(data.switch_2) })
if (data.switch_3 != null) sw.push({ label: '开关3', value: translateSwitchShort(data.switch_3) })
return [...sw, ...list].slice(0, 6)
}
return list.slice(0, 6)
}
function translateNormalAlarm(v) {
const s = String(v)
return s === '0' ? '正常' : s === '1' ? '告警' : '未知'
}
function translateSmokeStatus(v) {
const s = String(v).toLowerCase()
return s === '0' || s === 'normal' ? '正常' : s === '1' || s === 'alarm' ? '告警' : '正常'
}
function translateSwitch(v) {
const s = String(v).toLowerCase()
return s === '0' || s === 'off' ? '关闭' : s === '1' || s === 'on' ? '开启' : '--'
}
function translateSwitchShort(v) {
const s = String(v).toLowerCase()
return s === '0' || s === 'off' ? '关' : s === '1' || s === 'on' ? '开' : '-'
}
function formatTime(t) {
if (!t) return ''
return t.replace('T', ' ').substring(5, 16)
}
onMounted(() => {
loadDevices()
connectWs(handleWsMessage)
})
onBeforeUnmount(() => {
closeWs(handleWsMessage)
})
</script>
<style scoped lang="scss">
.warehouse-page {
padding: 20rpx;
background: #f5f7fa;
min-height: 100vh;
}
.warehouse-hero {
background: linear-gradient(135deg, #085f52, #0a7c70);
color: #fff;
padding: 30rpx;
border-radius: 24rpx;
margin-bottom: 20rpx;
position: relative;
}
.warehouse-hero-box {
display: flex;
justify-content: space-between;
}
.back-btn {
background: rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 100rpx;
font-size: 24rpx;
padding: 10rpx 20rpx;
border: none;
margin-bottom: 16rpx;
}
.hero-sub {
font-size: 24rpx;
opacity: 0.8;
}
.hero-title {
font-size: 40rpx;
font-weight: bold;
margin: 8rpx 0;
}
.hero-desc {
font-size: 24rpx;
opacity: 0.8;
}
.meta-num {
font-size: 40rpx;
font-weight: bold;
display: block;
}
.meta-label {
font-size: 24rpx;
opacity: 0.9;
}
.num {
font-size: 36rpx;
font-weight: bold;
color: #111;
margin-top: 8rpx;
display: block;
}
.online .num {
color: #07a186;
}
.offline .num {
color: #999;
}
/* 父容器:网格布局 → 2个一行 */
.sensor-grid {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
/* 两个卡片之间的间距 */
padding: 10rpx;
}
/* loading / 空状态 占满整行 */
.loading-view,
.empty-state {
width: 100%;
text-align: center;
padding: 40rpx 0;
font-size: 28rpx;
color: #666;
}
.sensor-card {
// background: #fff;
// border-radius: 24rpx;
// padding: 24rpx;
// margin-bottom: 20rpx;
// position: relative;
// overflow: hidden;
width: calc(50% - 10rpx);
box-sizing: border-box;
border-radius: 16rpx;
background: #fff;
overflow: hidden;
}
// .sensor-card::before {
// content: '';
// position: absolute;
// left: 0;
// top: 0;
// bottom: 0;
// width: 8rpx;
// background: #08a089;
// }
.sensor-card.offline {
opacity: 0.6;
filter: grayscale(0.5);
}
.sensor-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.sensor-main {
background: #fff;
border-radius: 20rpx;
padding: 20rpx;
}
.sensor-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.sensor-icon {
width: 70rpx;
height: 70rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: bold;
background: #e6f7f5;
color: #088a77;
}
.sensor-state.offline {
opacity: 0.6;
color: grey;
filter: grayscale(0.5);
}
.sensor-state.online {
color: #088a77;
}
.sensor-state {
font-size: 14px;
// padding: 8rpx 16rpx;
// background: #e6f7f5;
// color: #088a77;
// border-radius: 100rpx;
// display: flex;
// align-items: center;
}
.dot {
width: 12rpx;
height: 12rpx;
background: #09a089;
border-radius: 50%;
margin-right: 8rpx;
}
.sensor-body {
margin-top: 20rpx;
}
.location {
font-size: 16px;
color: #666;
}
.type-name {
font-size: 16px;
font-weight: 600;
// margin: 8rpx 0;
}
.summary {
font-size: 14px;
background: #f0f0f0;
padding: 8rpx 16rpx;
border-radius: 100rpx;
display: inline-block;
margin-top: 8rpx;
}
.socket-actions {
display: flex;
justify-content: space-between;
gap: 16rpx;
margin-top: 10px;
}
.switch-actions {
margin-top: 20rpx;
}
.switch-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.ch-label {
width: 80rpx;
font-size: 12px;
}
.control-btn {
padding: 12rpx 24rpx;
border-radius: 100rpx;
font-size: 12px;
font-weight: bold;
border: none;
}
.control-btn.on {
background: #d1fae5;
color: #065f46;
}
.control-btn.off {
background: #ffedd5;
color: #9a3412;
}
.metric-panel {
background: #f9f9f9;
border-radius: 20rpx;
padding: 10rpx;
}
.panel-title {
font-size: 14px;
color: #666;
margin-bottom: 16rpx;
display: block;
}
.metric-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.metric-item {
flex: 1;
min-width: 200rpx;
background: #fff;
padding: 16rpx;
border-radius: 16rpx;
}
.label {
font-size: 14px;
color: #666;
display: block;
}
.value {
font-size: 14px;
font-weight: bold;
color: #088a77;
margin-top: 4rpx;
}
.muted {
background: #f1f1f1;
color: #999;
}
.sensor-footer {
margin-top: 20rpx;
padding-top: 16rpx;
border-top: 1rpx dashed #eee;
font-size: 22rpx;
color: #999;
display: flex;
justify-content: space-between;
}
.empty-state {
text-align: center;
padding: 60rpx;
color: #999;
font-size: 28rpx;
}
.summary-card {
padding: 18px 20px;
border-radius: 18px;
position: relative;
overflow: hidden;
width: 30%;
.label {
display: block;
font-size: 13px;
font-weight: 500;
color: #334155;
margin-bottom: 6px;
}
.value {
font-size: 28px;
font-weight: 700;
line-height: 1.2;
}
}
// 设备总数
.summary-card.total {
background: #eef2ff;
color: #6366f1;
}
// 在线设备
.summary-card.online {
background: #ecfdf5;
color: #059669;
}
// 离线设备
.summary-card.offline {
background: #F5F5F5;
color: #64748b;
}
// 最近上报
.summary-card.report {
background: #f0f9ff;
color: #0ea5e9;
}
.loading-view {
text-align: center;
padding: 40rpx;
font-size: 28rpx;
color: #666;
}
.hero-meta {
// background-color: #F8FAFC;
border-color: #CBD5E1;
border-radius: 16rpx;
margin-top: 16rpx;
// padding: 24rpx;
}
.report {
margin-top: 8rpx;
font-size: 14px;
color: #fff;
}
</style>