仓库传感器详情页修改

首页修改
This commit is contained in:
2026-04-15 17:07:40 +08:00
parent 8364c0e9a9
commit d9b9a76ac1
3 changed files with 423 additions and 27 deletions

17
src/api/worn/socket.js Normal file
View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
// 智能排风远程开关
export function controlSocket(data) {
const payload = {
devEui: data.devEui,
deviceId: data.deviceId,
status: data.status
}
return request({
url: '/worn/socket/control',
method: 'post',
params: payload,
data: payload
})
}

View File

@@ -51,8 +51,25 @@
<div class="donut-card">
<div class="donut">
<strong>{{ exceptionStats.total }}</strong>
<em class="donut-total-tag">累计告警数</em>
<span>累计告警</span>
</div>
<div class="donut-info">
<div class="donut-legend">
<div class="legend-item danger">
<i></i>
<span>红色告警</span>
</div>
<div class="legend-item warning">
<i></i>
<span>橙色预警</span>
</div>
<div class="legend-item normal">
<i></i>
<span>绿色正常</span>
</div>
</div>
</div>
</div>
</div>
@@ -1032,13 +1049,36 @@ onBeforeUnmount(() => {
}
.donut-card {
display: grid;
place-items: center;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
min-height: 184px;
padding: 20px 18px;
gap: 18px;
}
.donut-head {
display: flex;
flex-direction: column;
gap: 4px;
text-align: left;
strong {
font-size: 14px;
color: #0f172a;
}
span {
font-size: 12px;
color: #64748b;
}
}
.donut {
position: relative;
display: grid;
flex: 0 0 110px;
width: 110px;
height: 110px;
place-items: center;
@@ -1062,17 +1102,90 @@ onBeforeUnmount(() => {
}
strong {
font-size: 24px;
font-size: 28px;
font-weight: 900;
color: #0f172a;
}
.donut-total-tag {
position: relative;
z-index: 1;
margin-top: 2px;
font-size: 10px;
font-style: normal;
font-weight: 700;
color: #94a3b8;
}
span {
margin-top: 24px;
margin-top: 4px;
font-size: 11px;
font-weight: 700;
color: #64748b;
}
}
.donut-info {
display: flex;
flex: 1;
flex-direction: column;
justify-content: center;
gap: 12px;
min-width: 0;
}
.donut-legend {
display: grid;
gap: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
min-height: 34px;
padding: 8px 10px;
font-size: 12px;
color: #334155;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(226, 232, 240, 0.82);
border-radius: 12px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.86);
i {
width: 10px;
height: 10px;
border-radius: 50%;
}
&.danger i {
background: #ef4444;
}
&.warning i {
background: #f59e0b;
}
&.normal i {
background: #22c55e;
}
}
@media (max-width: 1200px) {
.donut-card {
flex-direction: column;
align-items: center;
}
.donut-head {
text-align: center;
}
.donut-info {
width: 100%;
}
}
.alarm-list {
flex: 1;
min-height: 118px;

View File

@@ -57,6 +57,24 @@
<p>{{ item.locationName }}</p>
<h2>{{ item.typeName }}</h2>
<strong>{{ item.summary }}</strong>
<div v-if="item.type === 'socket'" class="socket-actions">
<button
type="button"
class="control-btn control-on"
:disabled="!!controlLoadingMap[item.cardKey]"
@click="handleSocketControl(item, 'on')"
>
{{ controlLoadingMap[item.cardKey] ? '发送中...' : '开启排风' }}
</button>
<button
type="button"
class="control-btn control-off"
:disabled="!!controlLoadingMap[item.cardKey]"
@click="handleSocketControl(item, 'off')"
>
{{ controlLoadingMap[item.cardKey] ? '发送中...' : '关闭排风' }}
</button>
</div>
</div>
</div>
@@ -87,8 +105,11 @@
<script setup name="WarehouseDashboard">
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { listMqttDevice } from '@/api/worn/mqttDevice'
import { listMqttData } from '@/api/worn/mqttData'
import { listMqttEvent } from '@/api/worn/mqttEvent'
import { controlSocket } from '@/api/worn/socket'
import { closeWs, connectWs } from '@/utils/ws'
const route = useRoute()
@@ -97,6 +118,7 @@ const router = useRouter()
const loading = ref(false)
const devices = ref([])
const latestDataMap = reactive({})
const controlLoadingMap = reactive({})
const ENV_CARD_TYPES = ['tempHum', 'toxicGas', 'hydrogenSulfide']
const deptId = computed(() => route.query.deptId)
@@ -140,16 +162,42 @@ function loadLatestData() {
return Promise.resolve()
}
return listMqttData({
pageNum: 1,
pageSize: 1,
deviceId: device.id
}).then((res) => {
const row = (res.rows || [])[0]
if (row) {
latestDataMap[getDeviceKey(device)] = normalizePayload(row)
}
}).catch(() => {})
const requestList = [
listMqttData({
pageNum: 1,
pageSize: 1,
deviceId: device.id
}).then((res) => {
const row = (res.rows || [])[0]
if (row) {
latestDataMap[getDeviceKey(device)] = normalizePayload({
...(latestDataMap[getDeviceKey(device)] || {}),
...row
})
}
}).catch(() => {})
]
if (isStateDevice(device)) {
requestList.push(
listMqttEvent({
pageNum: 1,
pageSize: 1,
deviceId: device.id
}).then((res) => {
const row = (res.rows || [])[0]
if (row) {
latestDataMap[getDeviceKey(device)] = normalizePayload({
...(latestDataMap[getDeviceKey(device)] || {}),
...row,
...deriveStateFromEvent(row)
})
}
}).catch(() => {})
)
}
return Promise.all(requestList)
})
return Promise.all(tasks)
@@ -160,20 +208,80 @@ function handleWsMessage(data) {
return
}
const key = getDeviceKey(data)
latestDataMap[key] = normalizePayload(data)
const index = devices.value.findIndex((device) => {
return String(device.id || '') === String(data.deviceId || '') ||
String(device.devEui || '').toLowerCase() === String(data.devEUI || data.devEui || '').toLowerCase()
})
if (index > -1) {
const device = devices.value[index]
const key = getDeviceKey(device)
const patchedData = patchRealtimeState(latestDataMap[key] || {}, data)
latestDataMap[key] = normalizePayload({
...(latestDataMap[key] || {}),
...patchedData
})
devices.value[index] = {
...devices.value[index],
...device,
status: '0',
deviceName: data.deviceName || devices.value[index].deviceName
deviceName: data.deviceName || device.deviceName
}
return
}
const key = getDeviceKey(data)
const patchedData = patchRealtimeState(latestDataMap[key] || {}, data)
latestDataMap[key] = normalizePayload({
...(latestDataMap[key] || {}),
...patchedData
})
}
function patchRealtimeState(previous, incoming) {
const next = { ...incoming }
const waterStatus = deriveWaterStatus({ ...previous, ...incoming })
if (waterStatus !== undefined) {
next.waterStatus = waterStatus
next.water_status = waterStatus
}
const doorStatus = deriveDoorStatus({ ...previous, ...incoming })
if (doorStatus !== undefined) {
next.doorStatus = doorStatus
next.door_status = doorStatus
}
const socketStatus = deriveSocketStatus({ ...previous, ...incoming })
if (socketStatus !== undefined) {
next.switchStatus = socketStatus
next.socket_status = socketStatus
next.socketStatus = socketStatus
}
return next
}
async function handleSocketControl(item, action) {
const deviceId = item.id || item.deviceId
const devEui = item.devEui || item.devEUI || item.dev_eui
if (!devEui) {
ElMessage.error('未找到设备 DevEUI无法下发指令')
return
}
const loadingKey = item.cardKey || String(deviceId)
if (controlLoadingMap[loadingKey]) {
return
}
controlLoadingMap[loadingKey] = true
try {
const status = action === 'on' ? 1 : 0
await controlSocket({ devEui, deviceId, status })
ElMessage.success(action === 'on' ? '开启排风指令已发送' : '关闭排风指令已发送')
} finally {
controlLoadingMap[loadingKey] = false
}
}
@@ -217,11 +325,27 @@ function getDeviceKey(device) {
function normalizePayload(row) {
const json = parseJson(row.dataJson) || parseJson(row.payload) || {}
const nestedData = typeof row.data === 'object'
? row.data
: (typeof row.data === 'string' ? parseJson(row.data) : null) || {}
const inferredWaterStatus = deriveWaterStatus({
...json,
...nestedData,
...row
})
return {
...json,
...nestedData,
...row,
devEUI: row.devEUI || row.devEui || json.devEUI || json.devEui,
deviceName: row.deviceName || json.deviceName
deviceName: row.deviceName || json.deviceName || nestedData.deviceName,
waterStatus: row.waterStatus ?? row.water_status ?? nestedData.waterStatus ?? nestedData.water_status ?? json.waterStatus ?? json.water_status ?? inferredWaterStatus,
water_status: row.water_status ?? row.waterStatus ?? nestedData.water_status ?? nestedData.waterStatus ?? json.water_status ?? json.waterStatus ?? inferredWaterStatus,
socket_status: row.socket_status ?? row.socketStatus ?? nestedData.socket_status ?? nestedData.socketStatus ?? json.socket_status ?? json.socketStatus,
switchStatus: row.switchStatus ?? row.socket_status ?? row.socketStatus ?? nestedData.switchStatus ?? nestedData.socket_status ?? nestedData.socketStatus ?? json.switchStatus ?? json.socket_status ?? json.socketStatus,
doorStatus: row.doorStatus ?? row.door_status ?? nestedData.doorStatus ?? nestedData.door_status ?? json.doorStatus ?? json.door_status,
tamperStatus: row.tamperStatus ?? row.tamper_status ?? nestedData.tamperStatus ?? nestedData.tamper_status ?? json.tamperStatus ?? json.tamper_status
}
}
@@ -237,6 +361,98 @@ function parseJson(value) {
}
}
function deriveStateFromEvent(row) {
const eventType = String(row.eventType || row.event_type || row.event || '').trim().toLowerCase()
const eventDesc = String(row.eventDesc || row.event_desc || row.statusDesc || row.status_desc || '').trim().toLowerCase()
const normalized = {
eventType: row.eventType || row.event_type,
eventDesc: row.eventDesc || row.event_desc,
statusDesc: row.statusDesc || row.status_desc
}
if (eventType === 'door_open' || eventDesc.includes('门已打开')) {
normalized.doorStatus = 1
} else if (eventType === 'door_close' || eventDesc.includes('门已关闭')) {
normalized.doorStatus = 0
}
if (eventType === 'socket_on' || eventDesc.includes('插座通电')) {
normalized.socket_status = 1
normalized.switchStatus = 1
} else if (eventType === 'socket_off' || eventDesc.includes('插座关闭') || eventDesc.includes('插座断电')) {
normalized.socket_status = 0
normalized.switchStatus = 0
}
const inferredWaterStatus = deriveWaterStatus(row)
if (inferredWaterStatus !== undefined) {
normalized.waterStatus = inferredWaterStatus
normalized.water_status = inferredWaterStatus
}
return normalized
}
function deriveWaterStatus(data) {
const eventType = String(data.eventType || data.event_type || data.event || data.status || '').trim().toLowerCase()
const eventDesc = String(data.eventDesc || data.event_desc || data.statusDesc || data.status_desc || '').trim().toLowerCase()
if (eventType === 'alarm' || eventDesc.includes('浸水预警') || eventDesc.includes('水浸预警') || eventDesc.includes('浸水报警') || eventDesc.includes('水浸报警')) {
return 1
}
if (eventType === 'recovery' || eventType === 'normal' || eventDesc.includes('水浸正常') || eventDesc.includes('浸水正常')) {
return 0
}
const directValue = data.waterStatus ?? data.water_status ?? data.water
if (directValue !== undefined && directValue !== null && directValue !== '') {
return directValue
}
return undefined
}
function deriveDoorStatus(data) {
const directValue = data.doorStatus ?? data.door_status
if (directValue !== undefined && directValue !== null && directValue !== '') {
return directValue
}
const eventType = String(data.eventType || data.event_type || data.event || data.status || '').trim().toLowerCase()
const eventDesc = String(data.eventDesc || data.event_desc || data.statusDesc || data.status_desc || '').trim().toLowerCase()
if (eventType === 'door_open' || eventDesc.includes('门已打开')) {
return 1
}
if (eventType === 'door_close' || eventDesc.includes('门已关闭')) {
return 0
}
return undefined
}
function deriveSocketStatus(data) {
const directValue = data.switchStatus ?? data.socket_status ?? data.socketStatus
if (directValue !== undefined && directValue !== null && directValue !== '') {
return directValue
}
const eventType = String(data.eventType || data.event_type || data.event || data.status || '').trim().toLowerCase()
const eventDesc = String(data.eventDesc || data.event_desc || data.statusDesc || data.status_desc || '').trim().toLowerCase()
if (eventType === 'socket_on' || eventDesc.includes('插座通电')) {
return 1
}
if (eventType === 'socket_off' || eventDesc.includes('插座关闭') || eventDesc.includes('插座断电')) {
return 0
}
return undefined
}
function getSensorType(device, data) {
const raw = `${device.deviceType || ''} ${device.deviceName || ''} ${data.type || ''}`.toLowerCase()
if (raw.includes('smoke')) return 'smoke'
@@ -247,6 +463,11 @@ function getSensorType(device, data) {
return 'sensor'
}
function isStateDevice(device) {
const raw = `${device.deviceType || ''} ${device.deviceName || ''}`.toLowerCase()
return raw.includes('door') || raw.includes('socket')
}
function getSensorIcon(type) {
const iconMap = {
smoke: '烟',
@@ -295,11 +516,11 @@ function getSensorSummary(data, type) {
}
if (type === 'socket') {
return `插座状态:${translateSwitch(data.switchStatus ?? data.socket_status)}`
return `插座状态:${translateSwitch(data.switchStatus ?? data.socket_status ?? data.socketStatus ?? data.status)}`
}
if (type === 'door') {
return `门禁状态:${translateSwitch(data.doorStatus)}`
return `门禁状态:${translateSwitch(data.doorStatus ?? data.door_status ?? data.status)}`
}
if (type === 'env') {
@@ -368,8 +589,12 @@ function buildMetrics(data, type) {
metrics.unshift({ label: '水浸状态', value: translateNormalAlarm(data.waterStatus ?? data.water_status ?? data.water) })
}
if (type === 'socket' && (data.switchStatus !== undefined || data.socket_status !== undefined)) {
metrics.unshift({ label: '插座状态', value: translateSwitch(data.switchStatus ?? data.socket_status) })
if (type === 'socket' && (data.switchStatus !== undefined || data.socket_status !== undefined || data.socketStatus !== undefined || data.status !== undefined)) {
metrics.unshift({ label: '插座状态', value: translateSwitch(data.switchStatus ?? data.socket_status ?? data.socketStatus ?? data.status) })
}
if (type === 'door' && (data.doorStatus !== undefined || data.door_status !== undefined || data.status !== undefined)) {
metrics.unshift({ label: '门禁状态', value: translateSwitch(data.doorStatus ?? data.door_status ?? data.status) })
}
return metrics.slice(0, 6)
@@ -403,9 +628,9 @@ function translateSmokeStatus(value) {
}
function translateSwitch(value) {
const normalized = String(value).toLowerCase()
if (normalized === '0' || normalized === 'off' || normalized === 'false') return '关闭'
if (normalized === '1' || normalized === 'on' || normalized === 'true') return '开启'
const normalized = String(value ?? '').trim().toLowerCase()
if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'close' || normalized === 'closed') return '关闭'
if (normalized === '1' || normalized === 'on' || normalized === 'true' || normalized === 'open' || normalized === 'opened') return '开启'
return value || '--'
}
@@ -777,6 +1002,47 @@ onBeforeUnmount(() => {
}
}
.socket-actions {
display: flex;
gap: 10px;
margin-top: 14px;
}
.control-btn {
min-width: 94px;
padding: 9px 14px;
font-size: 13px;
font-weight: 800;
line-height: 1;
cursor: pointer;
border: 1px solid transparent;
border-radius: 999px;
transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
&:hover:not(:disabled) {
transform: translateY(-1px);
}
&:disabled {
cursor: not-allowed;
opacity: 0.68;
}
}
.control-on {
color: #065f46;
background: linear-gradient(135deg, #dcfce7, #f0fdf4);
border-color: #86efac;
box-shadow: 0 10px 18px rgba(34, 197, 94, 0.12);
}
.control-off {
color: #9a3412;
background: linear-gradient(135deg, #ffedd5, #fff7ed);
border-color: #fdba74;
box-shadow: 0 10px 18px rgba(249, 115, 22, 0.12);
}
.metric-panel {
display: flex;
flex-direction: column;