+
+
+
+ 开关{{ channel }}
+
+
+
+
@@ -109,7 +134,7 @@ 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 { controlLightSwitch, controlSocket } from '@/api/worn/socket'
import { closeWs, connectWs } from '@/utils/ws'
const route = useRoute()
@@ -208,6 +233,10 @@ function handleWsMessage(data) {
return
}
+ const runtimeStatus = data.deviceStatus !== undefined && data.deviceStatus !== null && data.deviceStatus !== ''
+ ? String(data.deviceStatus)
+ : null
+
const index = devices.value.findIndex((device) => {
return String(device.id || '') === String(data.deviceId || '') ||
String(device.devEui || '').toLowerCase() === String(data.devEUI || data.devEui || '').toLowerCase()
@@ -224,7 +253,7 @@ function handleWsMessage(data) {
devices.value[index] = {
...device,
- status: '0',
+ status: runtimeStatus ?? '0',
deviceName: data.deviceName || device.deviceName
}
return
@@ -240,6 +269,24 @@ function handleWsMessage(data) {
function patchRealtimeState(previous, 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 })
if (waterStatus !== undefined) {
next.waterStatus = waterStatus
@@ -279,12 +326,78 @@ async function handleSocketControl(item, action) {
try {
const status = action === 'on' ? 1 : 0
await controlSocket({ devEui, deviceId, status })
- ElMessage.success(action === 'on' ? '开启排风指令已发送' : '关闭排风指令已发送')
+ syncSocketLocalState(item, status)
+ ElMessage.success(getSocketSuccessMessage(item.type, action))
} finally {
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) {
const data = latestDataMap[getDeviceKey(device)] || {}
const type = getSensorType(device, data)
@@ -298,7 +411,7 @@ function buildSensorCard(device, displayType) {
const data = latestDataMap[getDeviceKey(device)] || {}
const sourceType = getSensorType(device, data)
const type = displayType || sourceType
- const metrics = buildMetrics(data, type)
+ const metrics = buildDisplayMetrics(data, type)
const reportTime = formatTime(data.createTime || data.gatewayTime || data.time)
const isOnline = String(device.status) === '0'
@@ -307,10 +420,10 @@ function buildSensorCard(device, displayType) {
cardKey: `${getDeviceKey(device) || device.id || device.devEui || 'sensor'}-${type}`,
sourceType,
type,
- icon: getSensorIcon(type),
- typeName: getSensorTypeName(type),
+ icon: getDisplayIcon(type),
+ typeName: getDisplayTypeName(type),
locationName: getLocationName(device, data),
- summary: getSensorSummary(data, type),
+ summary: getDisplaySummary(data, type),
metrics,
reportTime,
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) {
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,
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,
+ 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,
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) {
- 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('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('switch')) return 'switch'
if (raw.includes('door')) return 'door'
if (raw.includes('env') || raw.includes('temperature') || raw.includes('humidity')) return 'env'
return 'sensor'
@@ -465,7 +667,7 @@ function getSensorType(device, data) {
function isStateDevice(device) {
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) {
@@ -473,6 +675,7 @@ function getSensorIcon(type) {
smoke: '烟',
water: '水',
socket: '电',
+ switch: '灯',
door: '门',
env: '温',
tempHum: '温',
@@ -488,6 +691,7 @@ function getSensorTypeName(type) {
smoke: '烟雾传感器',
water: '水浸传感器',
socket: '智能排风',
+ switch: '智慧照明开关',
door: '门禁状态',
env: '环境传感器',
tempHum: '温湿度传感器',
@@ -519,6 +723,20 @@ function getSensorSummary(data, type) {
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') {
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) })
}
+ 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)) {
metrics.unshift({ label: '门禁状态', value: translateSwitch(data.doorStatus ?? data.door_status ?? data.status) })
}
@@ -634,6 +862,13 @@ function translateSwitch(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) {
if (!value) {
return ''
@@ -658,7 +893,7 @@ onMounted(() => {
})
onBeforeUnmount(() => {
- closeWs()
+ closeWs(handleWsMessage)
})
@@ -896,7 +1131,9 @@ onBeforeUnmount(() => {
&.smoke { color: #ea580c; background: linear-gradient(135deg, #ffedd5, #fff7ed); border-color: #fed7aa; }
&.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; }
+ &.switch { color: #ca8a04; background: linear-gradient(135deg, #fef3c7, #fffbeb); border-color: #fcd34d; }
&.door { color: #475569; background: linear-gradient(135deg, #e2e8f0, #f8fafc); border-color: #cbd5e1; }
&.env { 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;
}
+.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 {
min-width: 94px;
padding: 9px 14px;