仓库传感器详情页修改
首页修改
This commit is contained in:
17
src/api/worn/socket.js
Normal file
17
src/api/worn/socket.js
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user