仓库传感器详情页修改

首页修改
This commit is contained in:
2026-04-20 09:11:41 +08:00
parent d9b9a76ac1
commit b513a327bb
5 changed files with 428 additions and 28 deletions

View File

@@ -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
})
}

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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,

View File

@@ -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;