parent
9b416c722c
commit
98845e72e3
|
|
@ -0,0 +1,51 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
/** 设备影子属性对比 VO */
|
||||
export interface DeviceShadowPropertyVO {
|
||||
identifier: string
|
||||
name: string
|
||||
dataType: string
|
||||
dataSpecs?: any
|
||||
dataSpecsList?: any[]
|
||||
desiredValue?: any
|
||||
reportedValue?: any
|
||||
desiredTime?: number
|
||||
reportedTime?: number
|
||||
}
|
||||
|
||||
/** 设备影子 VO */
|
||||
export interface DeviceShadowVO {
|
||||
version: number
|
||||
shadowTime: number
|
||||
status: number
|
||||
properties: DeviceShadowPropertyVO[]
|
||||
desired: Record<string, any>
|
||||
reported: Record<string, any>
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
/** 设备影子 API */
|
||||
export const DeviceShadowApi = {
|
||||
/** 获取设备影子 */
|
||||
getDeviceShadow: async (deviceId: number) => {
|
||||
return await request.get<DeviceShadowVO>({
|
||||
url: `/iot/device/shadow/get`,
|
||||
params: { deviceId }
|
||||
})
|
||||
},
|
||||
|
||||
/** 更新期望状态 */
|
||||
updateDesiredState: async (data: { deviceId: number; desired: Record<string, any> }) => {
|
||||
return await request.put({ url: `/iot/device/shadow/update-desired`, data })
|
||||
},
|
||||
|
||||
/** 更新影子开关状态 */
|
||||
updateShadowStatus: async (data: { deviceId: number; status: number }) => {
|
||||
return await request.put({ url: `/iot/device/shadow/update-status`, data })
|
||||
},
|
||||
|
||||
/** 重置影子 */
|
||||
resetShadow: async (deviceId: number) => {
|
||||
return await request.delete({ url: `/iot/device/shadow/reset`, params: { deviceId } })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
<!-- 设备影子 -->
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 顶部信息栏 -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm text-gray-600"
|
||||
>版本号:<b>{{ shadow.version ?? 0 }}</b></span
|
||||
>
|
||||
<span class="text-sm text-gray-600">
|
||||
最后更新:{{ shadow.shadowTime ? formatTime(shadow.shadowTime) : '-' }}
|
||||
</span>
|
||||
<span class="text-sm text-gray-600 flex items-center gap-2">
|
||||
影子开关:
|
||||
<el-switch
|
||||
v-model="shadowEnabled"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
@change="handleStatusChange"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<el-button @click="loadShadow" :loading="loading">
|
||||
<Icon icon="ep:refresh" class="mr-1" />刷新
|
||||
</el-button>
|
||||
<el-button type="danger" plain @click="handleReset" :loading="resetting">
|
||||
重置影子
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 属性对比表 -->
|
||||
<el-table
|
||||
:data="shadow.properties"
|
||||
:show-overflow-tooltip="true"
|
||||
:stripe="true"
|
||||
:row-class-name="rowClassName"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column align="center" label="标识符" prop="identifier" width="140" />
|
||||
<el-table-column align="center" label="名称" prop="name" width="120" />
|
||||
<el-table-column align="center" label="数据类型" prop="dataType" width="100" />
|
||||
<el-table-column align="center" label="期望值 (desired)" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-orange-500 font-bold': isDelta(row) }">
|
||||
{{ formatValue(row.desiredValue) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="上报值 (reported)" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-orange-500 font-bold': isDelta(row) }">
|
||||
{{ formatValue(row.reportedValue) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="期望更新时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.desiredTime ? formatTime(row.desiredTime) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="上报更新时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.reportedTime ? formatTime(row.reportedTime) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 下发期望 -->
|
||||
<el-divider content-position="left">下发期望状态</el-divider>
|
||||
<el-table :data="writableProperties" :show-overflow-tooltip="true" :stripe="true">
|
||||
<el-table-column align="center" label="标识符" prop="identifier" width="140" />
|
||||
<el-table-column align="center" label="名称" prop="name" width="120" />
|
||||
<el-table-column align="center" label="数据类型" prop="dataType" width="100" />
|
||||
<el-table-column align="center" label="期望值" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="desiredForm[row.identifier]" placeholder="输入期望值" size="small" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-4">
|
||||
<el-button type="primary" @click="handleUpdateDesired" :loading="updating">
|
||||
下发期望状态
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 原始 JSON -->
|
||||
<el-collapse class="mt-4">
|
||||
<el-collapse-item title="原始影子文档 (JSON)" name="raw">
|
||||
<pre class="text-xs bg-gray-50 p-4 rounded overflow-auto max-h-96">{{ rawJson }}</pre>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DeviceShadowVO } from '@/api/iot/device/shadow'
|
||||
import { DeviceShadowApi } from '@/api/iot/device/shadow'
|
||||
import { ThingModelData } from '@/api/iot/thingmodel'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { IoTThingModelAccessModeEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number
|
||||
thingModelList: ThingModelData[]
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(false)
|
||||
const updating = ref(false)
|
||||
const resetting = ref(false)
|
||||
const shadow = ref<DeviceShadowVO>({
|
||||
version: 0,
|
||||
shadowTime: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
properties: [],
|
||||
desired: {},
|
||||
reported: {},
|
||||
metadata: {}
|
||||
})
|
||||
const shadowEnabled = ref(CommonStatusEnum.ENABLE)
|
||||
const desiredForm = ref<Record<string, any>>({})
|
||||
|
||||
/** 可写属性列表(引导填写 desired) */
|
||||
const writableProperties = computed(() => {
|
||||
return props.thingModelList.filter((item) => {
|
||||
if (item.type !== IoTThingModelTypeEnum.PROPERTY) return false
|
||||
const accessMode = item.property?.accessMode
|
||||
return (
|
||||
accessMode === IoTThingModelAccessModeEnum.READ_WRITE.value ||
|
||||
accessMode === IoTThingModelAccessModeEnum.WRITE_ONLY.value
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const rawJson = computed(() =>
|
||||
JSON.stringify(
|
||||
{
|
||||
state: { desired: shadow.value.desired, reported: shadow.value.reported },
|
||||
metadata: shadow.value.metadata,
|
||||
version: shadow.value.version,
|
||||
timestamp: shadow.value.shadowTime
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
const loadShadow = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
shadow.value = await DeviceShadowApi.getDeviceShadow(props.deviceId)
|
||||
shadowEnabled.value = shadow.value.status ?? CommonStatusEnum.ENABLE
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleStatusChange = async (status: number) => {
|
||||
try {
|
||||
await DeviceShadowApi.updateShadowStatus({ deviceId: props.deviceId, status })
|
||||
message.success('影子开关已更新')
|
||||
} catch {
|
||||
shadowEnabled.value = shadow.value.status
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateDesired = async () => {
|
||||
const desired: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(desiredForm.value)) {
|
||||
if (value !== undefined && value !== '') {
|
||||
desired[key] = value
|
||||
}
|
||||
}
|
||||
if (Object.keys(desired).length === 0) {
|
||||
message.warning('请至少填写一个期望值')
|
||||
return
|
||||
}
|
||||
updating.value = true
|
||||
try {
|
||||
await DeviceShadowApi.updateDesiredState({ deviceId: props.deviceId, desired })
|
||||
message.success('期望状态已下发')
|
||||
desiredForm.value = {}
|
||||
await loadShadow()
|
||||
} finally {
|
||||
updating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
await message.confirm('确认重置该设备的影子文档?此操作将清空 desired 和 reported。')
|
||||
resetting.value = true
|
||||
try {
|
||||
await DeviceShadowApi.resetShadow(props.deviceId)
|
||||
message.success('影子已重置')
|
||||
await loadShadow()
|
||||
} finally {
|
||||
resetting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isDelta = (row: any) => {
|
||||
const d = row.desiredValue
|
||||
const r = row.reportedValue
|
||||
if (d === undefined || d === null) return false
|
||||
return String(d) !== String(r ?? '')
|
||||
}
|
||||
|
||||
const rowClassName = ({ row }: { row: any }) => (isDelta(row) ? 'shadow-delta-row' : '')
|
||||
|
||||
const formatValue = (val: any) => {
|
||||
if (val === undefined || val === null) return '-'
|
||||
if (typeof val === 'object') return JSON.stringify(val)
|
||||
return String(val)
|
||||
}
|
||||
|
||||
const formatTime = (epochSecond: number) => {
|
||||
return formatDate(new Date(epochSecond * 1000))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadShadow()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.shadow-delta-row) {
|
||||
background-color: var(--el-color-warning-light-9) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -27,6 +27,13 @@
|
|||
<el-tab-pane label="设备消息" name="log">
|
||||
<DeviceDetailsMessage v-if="activeTab === 'log'" :device-id="device.id" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="设备影子" name="shadow">
|
||||
<DeviceDetailsShadow
|
||||
v-if="activeTab === 'shadow'"
|
||||
:device-id="device.id"
|
||||
:thing-model-list="thingModelList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="模拟设备" name="simulator">
|
||||
<DeviceDetailsSimulator
|
||||
v-if="activeTab === 'simulator'"
|
||||
|
|
@ -70,6 +77,7 @@ import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
|
|||
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
|
||||
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
|
||||
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
|
||||
import DeviceDetailsShadow from './DeviceDetailsShadow.vue'
|
||||
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
|
||||
import DeviceDetailConfig from './DeviceDetailConfig.vue'
|
||||
import DeviceModbusConfig from './DeviceModbusConfig.vue'
|
||||
|
|
|
|||
Loading…
Reference in New Issue