仓库传感器详情页修改
首页修改
This commit is contained in:
@@ -15,3 +15,19 @@ export function controlSocket(data) {
|
|||||||
data: payload
|
data: payload
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 智慧照明开关远程控制
|
||||||
|
export function controlLightSwitch(data) {
|
||||||
|
const payload = {
|
||||||
|
devEui: data.devEui,
|
||||||
|
channel: data.channel,
|
||||||
|
status: data.status
|
||||||
|
}
|
||||||
|
|
||||||
|
return request({
|
||||||
|
url: '/worn/socket/switch',
|
||||||
|
method: 'post',
|
||||||
|
params: payload,
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { getToken } from '@/utils/auth'
|
|||||||
let ws = null
|
let ws = null
|
||||||
let reconnectTimer = null
|
let reconnectTimer = null
|
||||||
let manualClose = false
|
let manualClose = false
|
||||||
let currentOnMessage = null
|
|
||||||
let retryCount = 0
|
let retryCount = 0
|
||||||
|
const subscribers = new Set()
|
||||||
|
|
||||||
const WS_PATH = '/ws'
|
const WS_PATH = '/ws'
|
||||||
const MAX_RECONNECT_DELAY = 30000
|
const MAX_RECONNECT_DELAY = 30000
|
||||||
@@ -37,7 +37,10 @@ export function connectWs(onMessage) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
currentOnMessage = onMessage
|
if (typeof onMessage === 'function') {
|
||||||
|
subscribers.add(onMessage)
|
||||||
|
}
|
||||||
|
|
||||||
manualClose = false
|
manualClose = false
|
||||||
clearReconnectTimer()
|
clearReconnectTimer()
|
||||||
|
|
||||||
@@ -61,17 +64,23 @@ export function connectWs(onMessage) {
|
|||||||
// Keep plain text messages as-is.
|
// Keep plain text messages as-is.
|
||||||
}
|
}
|
||||||
|
|
||||||
currentOnMessage && currentOnMessage(data, event)
|
subscribers.forEach((handler) => {
|
||||||
|
try {
|
||||||
|
handler(data, event)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[WebSocket] subscriber error', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
console.log('[WebSocket] closed')
|
console.log('[WebSocket] closed')
|
||||||
ws = null
|
ws = null
|
||||||
|
|
||||||
if (!manualClose) {
|
if (!manualClose && subscribers.size > 0) {
|
||||||
reconnectTimer = setTimeout(() => {
|
reconnectTimer = setTimeout(() => {
|
||||||
console.log('[WebSocket] reconnecting')
|
console.log('[WebSocket] reconnecting')
|
||||||
connectWs(currentOnMessage)
|
connectWs()
|
||||||
}, getReconnectDelay())
|
}, getReconnectDelay())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,21 +90,42 @@ export function connectWs(onMessage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeWs() {
|
export function closeWs(onMessage) {
|
||||||
|
if (typeof onMessage === 'function') {
|
||||||
|
subscribers.delete(onMessage)
|
||||||
|
} else {
|
||||||
|
subscribers.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscribers.size > 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
manualClose = true
|
manualClose = true
|
||||||
retryCount = 0
|
retryCount = 0
|
||||||
clearReconnectTimer()
|
clearReconnectTimer()
|
||||||
|
if (ws) {
|
||||||
|
ws.close()
|
||||||
|
ws = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reconnectWs(onMessage) {
|
||||||
|
if (typeof onMessage === 'function') {
|
||||||
|
subscribers.add(onMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
manualClose = false
|
||||||
|
retryCount = 0
|
||||||
|
clearReconnectTimer()
|
||||||
|
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close()
|
ws.close()
|
||||||
ws = null
|
ws = null
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export function reconnectWs(onMessage = currentOnMessage) {
|
|
||||||
closeWs()
|
|
||||||
manualClose = false
|
manualClose = false
|
||||||
connectWs(onMessage)
|
connectWs()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendWs(data) {
|
export function sendWs(data) {
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ function normalizeEvent(data) {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const type = data?.type || 'unknown'
|
const type = data?.type || 'unknown'
|
||||||
const level = getEventLevel(data)
|
const level = getEventLevel(data)
|
||||||
const deviceName = data?.deviceName || `设备 ${data?.deviceId || ''}`.trim()
|
const deviceName = getRealtimeDeviceLabel(data)
|
||||||
const deptName = data?.deptName || `部门 ${data?.deptId || ''}`.trim()
|
const deptName = data?.deptName || `部门 ${data?.deptId || ''}`.trim()
|
||||||
const desc = translateEventValue(data?.eventDesc || data?.statusDesc || data?.event || getTypeName(type))
|
const desc = translateEventValue(data?.eventDesc || data?.statusDesc || data?.event || getTypeName(type))
|
||||||
|
|
||||||
@@ -405,6 +405,57 @@ function normalizeEvent(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRealtimeDeviceLabel(data) {
|
||||||
|
const rawDeviceName = String(data?.deviceName || '').trim()
|
||||||
|
const remark = String(data?.remark || data?.deviceRemark || data?.deviceAlias || '').trim()
|
||||||
|
const typeLabel = getRealtimeDeviceTypeName(data)
|
||||||
|
|
||||||
|
if (remark) {
|
||||||
|
if (rawDeviceName && rawDeviceName !== remark) {
|
||||||
|
return `${remark}(${rawDeviceName})`
|
||||||
|
}
|
||||||
|
return remark
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeLabel) {
|
||||||
|
if (rawDeviceName) {
|
||||||
|
return `${typeLabel}(${rawDeviceName})`
|
||||||
|
}
|
||||||
|
return typeLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawDeviceName || `设备 ${data?.deviceId || ''}`.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRealtimeDeviceTypeName(data) {
|
||||||
|
const rawType = String(data?.deviceType || data?.type || '').trim().toLowerCase()
|
||||||
|
const rawName = String(data?.deviceName || '').trim().toLowerCase()
|
||||||
|
|
||||||
|
if (rawType === 'switch' || rawName.includes('light-switch') || rawName.includes('light switch')) {
|
||||||
|
return '智慧照明开关'
|
||||||
|
}
|
||||||
|
if (rawName.includes('ac-socket') || rawName.includes('ac socket') || rawName.includes('空调')) {
|
||||||
|
return '智能空调插座'
|
||||||
|
}
|
||||||
|
if (rawType === 'socket' || rawName.includes('socket')) {
|
||||||
|
return '智能排风'
|
||||||
|
}
|
||||||
|
if (rawType === 'smoke' || rawName.includes('smoke')) {
|
||||||
|
return '烟雾传感器'
|
||||||
|
}
|
||||||
|
if (rawType === 'water' || rawName.includes('water')) {
|
||||||
|
return '水浸传感器'
|
||||||
|
}
|
||||||
|
if (rawType === 'door' || rawName.includes('door')) {
|
||||||
|
return '门禁传感器'
|
||||||
|
}
|
||||||
|
if (rawType === 'env' || rawName.includes('env')) {
|
||||||
|
return '环境传感器'
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
function isDuplicateRealtimeEvent(event) {
|
function isDuplicateRealtimeEvent(event) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const lastTime = recentEventKeys.get(event.fingerprint)
|
const lastTime = recentEventKeys.get(event.fingerprint)
|
||||||
@@ -436,7 +487,10 @@ function buildEventFingerprint(data) {
|
|||||||
concentration: data?.concentration,
|
concentration: data?.concentration,
|
||||||
water: data?.water,
|
water: data?.water,
|
||||||
doorStatus: data?.doorStatus,
|
doorStatus: data?.doorStatus,
|
||||||
tamperStatus: data?.tamperStatus
|
tamperStatus: data?.tamperStatus,
|
||||||
|
switch1: data?.switch1 ?? data?.switch_1,
|
||||||
|
switch2: data?.switch2 ?? data?.switch_2,
|
||||||
|
switch3: data?.switch3 ?? data?.switch_3
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +594,13 @@ function buildExtraMetrics(data, knownKeys) {
|
|||||||
'deptName',
|
'deptName',
|
||||||
'eventDesc',
|
'eventDesc',
|
||||||
'statusDesc',
|
'statusDesc',
|
||||||
'time'
|
'time',
|
||||||
|
'switch_1_change',
|
||||||
|
'switch1_change',
|
||||||
|
'switch_2_change',
|
||||||
|
'switch2_change',
|
||||||
|
'switch_3_change',
|
||||||
|
'switch3_change'
|
||||||
])
|
])
|
||||||
|
|
||||||
return Object.keys(data || {})
|
return Object.keys(data || {})
|
||||||
@@ -559,6 +619,9 @@ function formatExtraMetricValue(key, value) {
|
|||||||
if (key === 'level') {
|
if (key === 'level') {
|
||||||
return translateLevelValue(value)
|
return translateLevelValue(value)
|
||||||
}
|
}
|
||||||
|
if (['switch_1', 'switch_2', 'switch_3', 'switch1', 'switch2', 'switch3'].includes(key)) {
|
||||||
|
return translateSwitchShort(value)
|
||||||
|
}
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,11 +670,28 @@ function getExtraMetricLabel(key) {
|
|||||||
const labelMap = {
|
const labelMap = {
|
||||||
event: '事件',
|
event: '事件',
|
||||||
eventType: '事件类型',
|
eventType: '事件类型',
|
||||||
level: '等级'
|
level: '等级',
|
||||||
|
switch_1: '开关1',
|
||||||
|
switch_2: '开关2',
|
||||||
|
switch_3: '开关3',
|
||||||
|
switch1: '开关1',
|
||||||
|
switch2: '开关2',
|
||||||
|
switch3: '开关3'
|
||||||
}
|
}
|
||||||
return labelMap[key] || key
|
return labelMap[key] || key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function translateSwitchShort(value) {
|
||||||
|
const normalized = String(value ?? '').trim().toLowerCase()
|
||||||
|
if (normalized === '1' || normalized === 'on' || normalized === 'true' || normalized === 'open' || normalized === 'opened') {
|
||||||
|
return '开'
|
||||||
|
}
|
||||||
|
if (normalized === '0' || normalized === 'off' || normalized === 'false' || normalized === 'close' || normalized === 'closed') {
|
||||||
|
return '关'
|
||||||
|
}
|
||||||
|
return String(value ?? '--')
|
||||||
|
}
|
||||||
|
|
||||||
function getEventLevel(data) {
|
function getEventLevel(data) {
|
||||||
if (data?.type === 'alarm' || data?.eventType === 'alarm' || data?.event === 'alarm') {
|
if (data?.type === 'alarm' || data?.eventType === 'alarm' || data?.event === 'alarm') {
|
||||||
return 'alarm'
|
return 'alarm'
|
||||||
@@ -680,7 +760,7 @@ onBeforeUnmount(() => {
|
|||||||
clearTimeout(tickerTimer)
|
clearTimeout(tickerTimer)
|
||||||
tickerTimer = null
|
tickerTimer = null
|
||||||
}
|
}
|
||||||
closeWs()
|
closeWs(handleWsMessage)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<el-table-column label="设备唯一标识" align="center" prop="devEui" />
|
<el-table-column label="设备唯一标识" align="center" prop="devEui" />
|
||||||
<el-table-column label="设备名称" align="center" prop="deviceName" />
|
<el-table-column label="设备名称" align="center" prop="deviceName" />
|
||||||
<el-table-column label="设备类型" align="center" prop="deviceType" />
|
<el-table-column label="设备类型" align="center" prop="deviceType" />
|
||||||
|
<el-table-column label="上报周期(分钟)" align="center" prop="reportIntervalMinute" />
|
||||||
<el-table-column label="所属部门" align="center" prop="deptId">
|
<el-table-column label="所属部门" align="center" prop="deptId">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
{{ getDictLabel(scope.row.deptId, departmentList, { name: 'deptName', value: 'deptId' }) }}
|
{{ getDictLabel(scope.row.deptId, departmentList, { name: 'deptName', value: 'deptId' }) }}
|
||||||
@@ -109,6 +110,21 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="上报周期(分钟)" prop="reportIntervalMinute">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.reportIntervalMinute"
|
||||||
|
:min="1"
|
||||||
|
:max="1440"
|
||||||
|
controls-position="right"
|
||||||
|
placeholder="请输入上报周期"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12"></el-col>
|
||||||
|
</el-row>
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="24">
|
<el-col :span="24">
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
@@ -157,6 +173,7 @@ const data = reactive({
|
|||||||
devEui: null,
|
devEui: null,
|
||||||
deviceName: null,
|
deviceName: null,
|
||||||
deviceType: null,
|
deviceType: null,
|
||||||
|
reportIntervalMinute: null,
|
||||||
deptId: null,
|
deptId: null,
|
||||||
status: null
|
status: null
|
||||||
},
|
},
|
||||||
@@ -202,6 +219,7 @@ function reset() {
|
|||||||
devEui: null,
|
devEui: null,
|
||||||
deviceName: null,
|
deviceName: null,
|
||||||
deviceType: null,
|
deviceType: null,
|
||||||
|
reportIntervalMinute: null,
|
||||||
deptId: null,
|
deptId: null,
|
||||||
status: null,
|
status: null,
|
||||||
remark: null,
|
remark: null,
|
||||||
|
|||||||
@@ -57,14 +57,14 @@
|
|||||||
<p>{{ item.locationName }}</p>
|
<p>{{ item.locationName }}</p>
|
||||||
<h2>{{ item.typeName }}</h2>
|
<h2>{{ item.typeName }}</h2>
|
||||||
<strong>{{ item.summary }}</strong>
|
<strong>{{ item.summary }}</strong>
|
||||||
<div v-if="item.type === 'socket'" class="socket-actions">
|
<div v-if="isSocketControlType(item.type)" class="socket-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="control-btn control-on"
|
class="control-btn control-on"
|
||||||
:disabled="!!controlLoadingMap[item.cardKey]"
|
:disabled="!!controlLoadingMap[item.cardKey]"
|
||||||
@click="handleSocketControl(item, 'on')"
|
@click="handleSocketControl(item, 'on')"
|
||||||
>
|
>
|
||||||
{{ controlLoadingMap[item.cardKey] ? '发送中...' : '开启排风' }}
|
{{ getSocketActionText(item.type, 'on', !!controlLoadingMap[item.cardKey]) }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -72,9 +72,34 @@
|
|||||||
:disabled="!!controlLoadingMap[item.cardKey]"
|
:disabled="!!controlLoadingMap[item.cardKey]"
|
||||||
@click="handleSocketControl(item, 'off')"
|
@click="handleSocketControl(item, 'off')"
|
||||||
>
|
>
|
||||||
{{ controlLoadingMap[item.cardKey] ? '发送中...' : '关闭排风' }}
|
{{ getSocketActionText(item.type, 'off', !!controlLoadingMap[item.cardKey]) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="item.type === 'switch'" class="switch-actions">
|
||||||
|
<div
|
||||||
|
v-for="channel in [1, 2, 3]"
|
||||||
|
:key="`${item.cardKey}-channel-${channel}`"
|
||||||
|
class="switch-action-row"
|
||||||
|
>
|
||||||
|
<span class="switch-channel-label">开关{{ channel }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="control-btn control-on"
|
||||||
|
:disabled="!!controlLoadingMap[`${item.cardKey}-channel-${channel}`]"
|
||||||
|
@click="handleLightSwitchControl(item, channel, 'on')"
|
||||||
|
>
|
||||||
|
{{ getLightSwitchActionText(channel, 'on', !!controlLoadingMap[`${item.cardKey}-channel-${channel}`]) }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="control-btn control-off"
|
||||||
|
:disabled="!!controlLoadingMap[`${item.cardKey}-channel-${channel}`]"
|
||||||
|
@click="handleLightSwitchControl(item, channel, 'off')"
|
||||||
|
>
|
||||||
|
{{ getLightSwitchActionText(channel, 'off', !!controlLoadingMap[`${item.cardKey}-channel-${channel}`]) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,7 +134,7 @@ import { ElMessage } from 'element-plus'
|
|||||||
import { listMqttDevice } from '@/api/worn/mqttDevice'
|
import { listMqttDevice } from '@/api/worn/mqttDevice'
|
||||||
import { listMqttData } from '@/api/worn/mqttData'
|
import { listMqttData } from '@/api/worn/mqttData'
|
||||||
import { listMqttEvent } from '@/api/worn/mqttEvent'
|
import { listMqttEvent } from '@/api/worn/mqttEvent'
|
||||||
import { controlSocket } from '@/api/worn/socket'
|
import { controlLightSwitch, controlSocket } from '@/api/worn/socket'
|
||||||
import { closeWs, connectWs } from '@/utils/ws'
|
import { closeWs, connectWs } from '@/utils/ws'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -208,6 +233,10 @@ function handleWsMessage(data) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const runtimeStatus = data.deviceStatus !== undefined && data.deviceStatus !== null && data.deviceStatus !== ''
|
||||||
|
? String(data.deviceStatus)
|
||||||
|
: null
|
||||||
|
|
||||||
const index = devices.value.findIndex((device) => {
|
const index = devices.value.findIndex((device) => {
|
||||||
return String(device.id || '') === String(data.deviceId || '') ||
|
return String(device.id || '') === String(data.deviceId || '') ||
|
||||||
String(device.devEui || '').toLowerCase() === String(data.devEUI || data.devEui || '').toLowerCase()
|
String(device.devEui || '').toLowerCase() === String(data.devEUI || data.devEui || '').toLowerCase()
|
||||||
@@ -224,7 +253,7 @@ function handleWsMessage(data) {
|
|||||||
|
|
||||||
devices.value[index] = {
|
devices.value[index] = {
|
||||||
...device,
|
...device,
|
||||||
status: '0',
|
status: runtimeStatus ?? '0',
|
||||||
deviceName: data.deviceName || device.deviceName
|
deviceName: data.deviceName || device.deviceName
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -240,6 +269,24 @@ function handleWsMessage(data) {
|
|||||||
|
|
||||||
function patchRealtimeState(previous, incoming) {
|
function patchRealtimeState(previous, incoming) {
|
||||||
const next = { ...incoming }
|
const next = { ...incoming }
|
||||||
|
|
||||||
|
const switch1 = incoming.switch_1 ?? incoming.switch1
|
||||||
|
const switch2 = incoming.switch_2 ?? incoming.switch2
|
||||||
|
const switch3 = incoming.switch_3 ?? incoming.switch3
|
||||||
|
|
||||||
|
if (switch1 !== undefined && switch1 !== null && switch1 !== '') {
|
||||||
|
next.switch_1 = switch1
|
||||||
|
next.switch1 = switch1
|
||||||
|
}
|
||||||
|
if (switch2 !== undefined && switch2 !== null && switch2 !== '') {
|
||||||
|
next.switch_2 = switch2
|
||||||
|
next.switch2 = switch2
|
||||||
|
}
|
||||||
|
if (switch3 !== undefined && switch3 !== null && switch3 !== '') {
|
||||||
|
next.switch_3 = switch3
|
||||||
|
next.switch3 = switch3
|
||||||
|
}
|
||||||
|
|
||||||
const waterStatus = deriveWaterStatus({ ...previous, ...incoming })
|
const waterStatus = deriveWaterStatus({ ...previous, ...incoming })
|
||||||
if (waterStatus !== undefined) {
|
if (waterStatus !== undefined) {
|
||||||
next.waterStatus = waterStatus
|
next.waterStatus = waterStatus
|
||||||
@@ -279,12 +326,78 @@ async function handleSocketControl(item, action) {
|
|||||||
try {
|
try {
|
||||||
const status = action === 'on' ? 1 : 0
|
const status = action === 'on' ? 1 : 0
|
||||||
await controlSocket({ devEui, deviceId, status })
|
await controlSocket({ devEui, deviceId, status })
|
||||||
ElMessage.success(action === 'on' ? '开启排风指令已发送' : '关闭排风指令已发送')
|
syncSocketLocalState(item, status)
|
||||||
|
ElMessage.success(getSocketSuccessMessage(item.type, action))
|
||||||
} finally {
|
} finally {
|
||||||
controlLoadingMap[loadingKey] = false
|
controlLoadingMap[loadingKey] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleLightSwitchControl(item, channel, action) {
|
||||||
|
const devEui = item.devEui || item.devEUI || item.dev_eui
|
||||||
|
if (!devEui) {
|
||||||
|
ElMessage.error('未找到设备 DevEUI,无法下发指令')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingKey = `${item.cardKey}-channel-${channel}`
|
||||||
|
if (controlLoadingMap[loadingKey]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
controlLoadingMap[loadingKey] = true
|
||||||
|
try {
|
||||||
|
const status = action === 'on' ? 1 : 0
|
||||||
|
await controlLightSwitch({ devEui, channel, status })
|
||||||
|
syncLightSwitchLocalState(item, channel, status)
|
||||||
|
ElMessage.success(`${action === 'on' ? '开启' : '关闭'}${channel}路指令已发送`)
|
||||||
|
} finally {
|
||||||
|
controlLoadingMap[loadingKey] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSocketLocalState(item, status) {
|
||||||
|
const key = getDeviceKey(item)
|
||||||
|
if (!key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = latestDataMap[key] || {}
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
latestDataMap[key] = normalizePayload({
|
||||||
|
...current,
|
||||||
|
deviceId: item.deviceId || item.id,
|
||||||
|
devEui: item.devEui || item.devEUI || item.dev_eui,
|
||||||
|
deviceName: item.deviceName,
|
||||||
|
socket_status: status,
|
||||||
|
socketStatus: status,
|
||||||
|
switchStatus: status,
|
||||||
|
status,
|
||||||
|
gatewayTime: current.gatewayTime || now,
|
||||||
|
createTime: now
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLightSwitchLocalState(item, channel, status) {
|
||||||
|
const key = getDeviceKey(item)
|
||||||
|
if (!key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = latestDataMap[key] || {}
|
||||||
|
const now = new Date().toISOString()
|
||||||
|
latestDataMap[key] = normalizePayload({
|
||||||
|
...current,
|
||||||
|
deviceId: item.deviceId || item.id,
|
||||||
|
devEui: item.devEui || item.devEUI || item.dev_eui,
|
||||||
|
deviceName: item.deviceName,
|
||||||
|
[`switch_${channel}`]: status,
|
||||||
|
[`switch${channel}`]: status,
|
||||||
|
gatewayTime: current.gatewayTime || now,
|
||||||
|
createTime: now
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function expandDeviceCards(device) {
|
function expandDeviceCards(device) {
|
||||||
const data = latestDataMap[getDeviceKey(device)] || {}
|
const data = latestDataMap[getDeviceKey(device)] || {}
|
||||||
const type = getSensorType(device, data)
|
const type = getSensorType(device, data)
|
||||||
@@ -298,7 +411,7 @@ function buildSensorCard(device, displayType) {
|
|||||||
const data = latestDataMap[getDeviceKey(device)] || {}
|
const data = latestDataMap[getDeviceKey(device)] || {}
|
||||||
const sourceType = getSensorType(device, data)
|
const sourceType = getSensorType(device, data)
|
||||||
const type = displayType || sourceType
|
const type = displayType || sourceType
|
||||||
const metrics = buildMetrics(data, type)
|
const metrics = buildDisplayMetrics(data, type)
|
||||||
const reportTime = formatTime(data.createTime || data.gatewayTime || data.time)
|
const reportTime = formatTime(data.createTime || data.gatewayTime || data.time)
|
||||||
const isOnline = String(device.status) === '0'
|
const isOnline = String(device.status) === '0'
|
||||||
|
|
||||||
@@ -307,10 +420,10 @@ function buildSensorCard(device, displayType) {
|
|||||||
cardKey: `${getDeviceKey(device) || device.id || device.devEui || 'sensor'}-${type}`,
|
cardKey: `${getDeviceKey(device) || device.id || device.devEui || 'sensor'}-${type}`,
|
||||||
sourceType,
|
sourceType,
|
||||||
type,
|
type,
|
||||||
icon: getSensorIcon(type),
|
icon: getDisplayIcon(type),
|
||||||
typeName: getSensorTypeName(type),
|
typeName: getDisplayTypeName(type),
|
||||||
locationName: getLocationName(device, data),
|
locationName: getLocationName(device, data),
|
||||||
summary: getSensorSummary(data, type),
|
summary: getDisplaySummary(data, type),
|
||||||
metrics,
|
metrics,
|
||||||
reportTime,
|
reportTime,
|
||||||
isOnline,
|
isOnline,
|
||||||
@@ -319,6 +432,72 @@ function buildSensorCard(device, displayType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSocketControlType(type) {
|
||||||
|
return type === 'socket' || type === 'acSocket'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSocketSuccessMessage(type, action) {
|
||||||
|
if (type === 'acSocket') {
|
||||||
|
return action === 'on' ? '开启空调指令已发送' : '关闭空调指令已发送'
|
||||||
|
}
|
||||||
|
return action === 'on' ? '开启排风指令已发送' : '关闭排风指令已发送'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSocketActionText(type, action, loading) {
|
||||||
|
if (loading) {
|
||||||
|
return '发送中...'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'acSocket') {
|
||||||
|
return action === 'on' ? '开启空调' : '关闭空调'
|
||||||
|
}
|
||||||
|
|
||||||
|
return action === 'on' ? '开启排风' : '关闭排风'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLightSwitchActionText(channel, action, loading) {
|
||||||
|
if (loading) {
|
||||||
|
return '发送中...'
|
||||||
|
}
|
||||||
|
|
||||||
|
return action === 'on' ? `开${channel}路` : `关${channel}路`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayIcon(type) {
|
||||||
|
if (type === 'acSocket') {
|
||||||
|
return '空'
|
||||||
|
}
|
||||||
|
return getSensorIcon(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplayTypeName(type) {
|
||||||
|
if (type === 'acSocket') {
|
||||||
|
return '智能空调插座'
|
||||||
|
}
|
||||||
|
return getSensorTypeName(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDisplaySummary(data, type) {
|
||||||
|
if (type === 'acSocket') {
|
||||||
|
return `空调状态:${translateSwitch(data.switchStatus ?? data.socket_status ?? data.socketStatus ?? data.status)}`
|
||||||
|
}
|
||||||
|
return getSensorSummary(data, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDisplayMetrics(data, type) {
|
||||||
|
const metrics = buildMetrics(data, type === 'acSocket' ? 'socket' : type)
|
||||||
|
if (type !== 'acSocket') {
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics.map((metric, index) => {
|
||||||
|
if (index === 0 && String(metric.label).includes('插座')) {
|
||||||
|
return { ...metric, label: '空调状态' }
|
||||||
|
}
|
||||||
|
return metric
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function getDeviceKey(device) {
|
function getDeviceKey(device) {
|
||||||
return device.deviceId || device.id || device.devEUI || device.devEui || device.dev_eui
|
return device.deviceId || device.id || device.devEUI || device.devEui || device.dev_eui
|
||||||
}
|
}
|
||||||
@@ -344,6 +523,12 @@ function normalizePayload(row) {
|
|||||||
water_status: row.water_status ?? row.waterStatus ?? nestedData.water_status ?? nestedData.waterStatus ?? json.water_status ?? json.waterStatus ?? 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,
|
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,
|
switchStatus: row.switchStatus ?? row.socket_status ?? row.socketStatus ?? nestedData.switchStatus ?? nestedData.socket_status ?? nestedData.socketStatus ?? json.switchStatus ?? json.socket_status ?? json.socketStatus,
|
||||||
|
switch_1: row.switch_1 ?? row.switch1 ?? nestedData.switch_1 ?? nestedData.switch1 ?? json.switch_1 ?? json.switch1,
|
||||||
|
switch1: row.switch1 ?? row.switch_1 ?? nestedData.switch1 ?? nestedData.switch_1 ?? json.switch1 ?? json.switch_1,
|
||||||
|
switch_2: row.switch_2 ?? row.switch2 ?? nestedData.switch_2 ?? nestedData.switch2 ?? json.switch_2 ?? json.switch2,
|
||||||
|
switch2: row.switch2 ?? row.switch_2 ?? nestedData.switch2 ?? nestedData.switch_2 ?? json.switch2 ?? json.switch_2,
|
||||||
|
switch_3: row.switch_3 ?? row.switch3 ?? nestedData.switch_3 ?? nestedData.switch3 ?? json.switch_3 ?? json.switch3,
|
||||||
|
switch3: row.switch3 ?? row.switch_3 ?? nestedData.switch3 ?? nestedData.switch_3 ?? json.switch3 ?? json.switch_3,
|
||||||
doorStatus: row.doorStatus ?? row.door_status ?? nestedData.doorStatus ?? nestedData.door_status ?? json.doorStatus ?? json.door_status,
|
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
|
tamperStatus: row.tamperStatus ?? row.tamper_status ?? nestedData.tamperStatus ?? nestedData.tamper_status ?? json.tamperStatus ?? json.tamper_status
|
||||||
}
|
}
|
||||||
@@ -454,10 +639,27 @@ function deriveSocketStatus(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSensorType(device, data) {
|
function getSensorType(device, data) {
|
||||||
const raw = `${device.deviceType || ''} ${device.deviceName || ''} ${data.type || ''}`.toLowerCase()
|
const deviceType = String(device.deviceType || '').trim().toLowerCase()
|
||||||
|
const deviceName = String(device.deviceName || '').trim().toLowerCase()
|
||||||
|
const raw = `${deviceType} ${deviceName} ${data.type || ''}`.toLowerCase()
|
||||||
|
|
||||||
|
if (deviceType === 'smoke') return 'smoke'
|
||||||
|
if (deviceType === 'water') return 'water'
|
||||||
|
if (deviceType === 'door') return 'door'
|
||||||
|
if (deviceType === 'env') return 'env'
|
||||||
|
if (deviceType === 'switch') return 'switch'
|
||||||
|
if (deviceType === 'socket') {
|
||||||
|
if (deviceName.includes('ac-socket') || deviceName.includes('ac socket') || deviceName.includes('空调')) {
|
||||||
|
return 'acSocket'
|
||||||
|
}
|
||||||
|
return 'socket'
|
||||||
|
}
|
||||||
|
|
||||||
if (raw.includes('smoke')) return 'smoke'
|
if (raw.includes('smoke')) return 'smoke'
|
||||||
if (raw.includes('water')) return 'water'
|
if (raw.includes('water')) return 'water'
|
||||||
|
if (raw.includes('ac-socket') || raw.includes('ac socket') || raw.includes('空调')) return 'acSocket'
|
||||||
if (raw.includes('socket')) return 'socket'
|
if (raw.includes('socket')) return 'socket'
|
||||||
|
if (raw.includes('switch')) return 'switch'
|
||||||
if (raw.includes('door')) return 'door'
|
if (raw.includes('door')) return 'door'
|
||||||
if (raw.includes('env') || raw.includes('temperature') || raw.includes('humidity')) return 'env'
|
if (raw.includes('env') || raw.includes('temperature') || raw.includes('humidity')) return 'env'
|
||||||
return 'sensor'
|
return 'sensor'
|
||||||
@@ -465,7 +667,7 @@ function getSensorType(device, data) {
|
|||||||
|
|
||||||
function isStateDevice(device) {
|
function isStateDevice(device) {
|
||||||
const raw = `${device.deviceType || ''} ${device.deviceName || ''}`.toLowerCase()
|
const raw = `${device.deviceType || ''} ${device.deviceName || ''}`.toLowerCase()
|
||||||
return raw.includes('door') || raw.includes('socket')
|
return raw.includes('door') || raw.includes('socket') || raw.includes('switch')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSensorIcon(type) {
|
function getSensorIcon(type) {
|
||||||
@@ -473,6 +675,7 @@ function getSensorIcon(type) {
|
|||||||
smoke: '烟',
|
smoke: '烟',
|
||||||
water: '水',
|
water: '水',
|
||||||
socket: '电',
|
socket: '电',
|
||||||
|
switch: '灯',
|
||||||
door: '门',
|
door: '门',
|
||||||
env: '温',
|
env: '温',
|
||||||
tempHum: '温',
|
tempHum: '温',
|
||||||
@@ -488,6 +691,7 @@ function getSensorTypeName(type) {
|
|||||||
smoke: '烟雾传感器',
|
smoke: '烟雾传感器',
|
||||||
water: '水浸传感器',
|
water: '水浸传感器',
|
||||||
socket: '智能排风',
|
socket: '智能排风',
|
||||||
|
switch: '智慧照明开关',
|
||||||
door: '门禁状态',
|
door: '门禁状态',
|
||||||
env: '环境传感器',
|
env: '环境传感器',
|
||||||
tempHum: '温湿度传感器',
|
tempHum: '温湿度传感器',
|
||||||
@@ -519,6 +723,20 @@ function getSensorSummary(data, type) {
|
|||||||
return `插座状态:${translateSwitch(data.switchStatus ?? data.socket_status ?? data.socketStatus ?? data.status)}`
|
return `插座状态:${translateSwitch(data.switchStatus ?? data.socket_status ?? data.socketStatus ?? data.status)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'switch') {
|
||||||
|
const channelParts = [
|
||||||
|
data.switch_1 !== undefined && data.switch_1 !== null && data.switch_1 !== '' ? `1路${translateSwitchShort(data.switch_1)}` : null,
|
||||||
|
data.switch_2 !== undefined && data.switch_2 !== null && data.switch_2 !== '' ? `2路${translateSwitchShort(data.switch_2)}` : null,
|
||||||
|
data.switch_3 !== undefined && data.switch_3 !== null && data.switch_3 !== '' ? `3路${translateSwitchShort(data.switch_3)}` : null
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
if (channelParts.length) {
|
||||||
|
return `开关状态:${channelParts.join(' / ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `开关状态:${translateSwitch(data.switchStatus ?? data.switch_status ?? data.socket_status ?? data.socketStatus ?? data.status)}`
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'door') {
|
if (type === 'door') {
|
||||||
return `门禁状态:${translateSwitch(data.doorStatus ?? data.door_status ?? data.status)}`
|
return `门禁状态:${translateSwitch(data.doorStatus ?? data.door_status ?? data.status)}`
|
||||||
}
|
}
|
||||||
@@ -593,6 +811,16 @@ function buildMetrics(data, type) {
|
|||||||
metrics.unshift({ label: '插座状态', value: translateSwitch(data.switchStatus ?? data.socket_status ?? data.socketStatus ?? data.status) })
|
metrics.unshift({ label: '插座状态', value: translateSwitch(data.switchStatus ?? data.socket_status ?? data.socketStatus ?? data.status) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === 'switch') {
|
||||||
|
const switchMetrics = [
|
||||||
|
data.switch_1 !== undefined && data.switch_1 !== null && data.switch_1 !== '' ? { label: '开关1', value: translateSwitchShort(data.switch_1) } : null,
|
||||||
|
data.switch_2 !== undefined && data.switch_2 !== null && data.switch_2 !== '' ? { label: '开关2', value: translateSwitchShort(data.switch_2) } : null,
|
||||||
|
data.switch_3 !== undefined && data.switch_3 !== null && data.switch_3 !== '' ? { label: '开关3', value: translateSwitchShort(data.switch_3) } : null
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
return [...switchMetrics, ...metrics].slice(0, 6)
|
||||||
|
}
|
||||||
|
|
||||||
if (type === 'door' && (data.doorStatus !== undefined || data.door_status !== undefined || data.status !== undefined)) {
|
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) })
|
metrics.unshift({ label: '门禁状态', value: translateSwitch(data.doorStatus ?? data.door_status ?? data.status) })
|
||||||
}
|
}
|
||||||
@@ -634,6 +862,13 @@ function translateSwitch(value) {
|
|||||||
return value || '--'
|
return value || '--'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function translateSwitchShort(value) {
|
||||||
|
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 || '--'
|
||||||
|
}
|
||||||
|
|
||||||
function formatTime(value) {
|
function formatTime(value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return ''
|
return ''
|
||||||
@@ -658,7 +893,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
closeWs()
|
closeWs(handleWsMessage)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -896,7 +1131,9 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
&.smoke { color: #ea580c; background: linear-gradient(135deg, #ffedd5, #fff7ed); border-color: #fed7aa; }
|
&.smoke { color: #ea580c; background: linear-gradient(135deg, #ffedd5, #fff7ed); border-color: #fed7aa; }
|
||||||
&.water { color: #0284c7; background: linear-gradient(135deg, #e0f2fe, #f0f9ff); border-color: #bae6fd; }
|
&.water { color: #0284c7; background: linear-gradient(135deg, #e0f2fe, #f0f9ff); border-color: #bae6fd; }
|
||||||
|
&.acSocket { color: #2563eb; background: linear-gradient(135deg, #dbeafe, #eff6ff); border-color: #bfdbfe; }
|
||||||
&.socket { color: #16a34a; background: linear-gradient(135deg, #dcfce7, #f0fdf4); border-color: #bbf7d0; }
|
&.socket { color: #16a34a; background: linear-gradient(135deg, #dcfce7, #f0fdf4); border-color: #bbf7d0; }
|
||||||
|
&.switch { color: #ca8a04; background: linear-gradient(135deg, #fef3c7, #fffbeb); border-color: #fcd34d; }
|
||||||
&.door { color: #475569; background: linear-gradient(135deg, #e2e8f0, #f8fafc); border-color: #cbd5e1; }
|
&.door { color: #475569; background: linear-gradient(135deg, #e2e8f0, #f8fafc); border-color: #cbd5e1; }
|
||||||
&.env { color: #0d9488; background: linear-gradient(135deg, #ccfbf1, #f0fdfa); border-color: #99f6e4; }
|
&.env { color: #0d9488; background: linear-gradient(135deg, #ccfbf1, #f0fdfa); border-color: #99f6e4; }
|
||||||
&.tempHum { color: #0d9488; background: linear-gradient(135deg, #ccfbf1, #f0fdfa); border-color: #99f6e4; }
|
&.tempHum { color: #0d9488; background: linear-gradient(135deg, #ccfbf1, #f0fdfa); border-color: #99f6e4; }
|
||||||
@@ -1008,6 +1245,25 @@ onBeforeUnmount(() => {
|
|||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-action-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 48px repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-channel-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
.control-btn {
|
.control-btn {
|
||||||
min-width: 94px;
|
min-width: 94px;
|
||||||
padding: 9px 14px;
|
padding: 9px 14px;
|
||||||
|
|||||||
Reference in New Issue
Block a user