feat:【IoT 物联网】重构设备模拟器,优化属性和事件上报逻辑,增强用户交互体验

This commit is contained in:
YunaiV
2025-06-29 22:00:20 +08:00
parent 79d8616510
commit a37c168efe
3 changed files with 283 additions and 199 deletions

View File

@ -18,14 +18,6 @@ export interface ThingModelData {
service?: ThingModelService // 服务 service?: ThingModelService // 服务
} }
/**
* IoT 模拟设备
*/
// TODO @super和 ThingModelSimulatorData 会不会好点
export interface SimulatorData extends ThingModelData {
simulateValue?: string | number // 用于存储模拟值 TODO @super字段使用 value 会不会好点
}
/** /**
* ThingModelProperty 类型 * ThingModelProperty 类型
*/ */

View File

@ -6,75 +6,109 @@
<el-col :span="12"> <el-col :span="12">
<el-tabs v-model="activeTab" type="border-card"> <el-tabs v-model="activeTab" type="border-card">
<!-- 上行指令调试 --> <!-- 上行指令调试 -->
<el-tab-pane label="上行指令调试" name="up"> <el-tab-pane label="上行指令调试" name="upstream">
<el-tabs v-if="activeTab === 'up'" v-model="subTab"> <el-tabs v-if="activeTab === 'upstream'" v-model="upstreamTab">
<!-- 属性上报 --> <!-- 属性上报 -->
<el-tab-pane label="属性上报" name="property"> <el-tab-pane label="属性上报" :name="IotDeviceMessageMethodEnum.PROPERTY_POST.method">
<ContentWrap> <ContentWrap>
<el-table <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
v-loading="loading" <el-table-column
:data="list" fixed="left"
:show-overflow-tooltip="true" align="center"
:stripe="true" label="功能名称"
> prop="name"
<!-- TODO @super每个 colum 搞下宽度避免 table 每一列最后有个 . --> width="120"
<!-- TODO @super可以左侧 fixed --> />
<el-table-column align="center" label="功能名称" prop="name" /> <el-table-column
<el-table-column align="center" label="标识符" prop="identifier" /> fixed="left"
<el-table-column align="center" label="数据类型" prop="identifier"> align="center"
<!-- TODO @super不用翻译可以减少宽度的占用 --> label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="center" label="数据类型" width="100">
<template #default="{ row }"> <template #default="{ row }">
{{ getDataTypeOptionsLabel(row.property?.dataType) ?? '-' }} {{ row.property?.dataType ?? '-' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="left" label="数据定义" prop="identifier"> <el-table-column align="left" label="数据定义" min-width="200">
<template #default="{ row }"> <template #default="{ row }">
<DataDefinition :data="row" /> <DataDefinition :data="row" />
</template> </template>
</el-table-column> </el-table-column>
<!-- TODO @super可以右侧 fixed --> <el-table-column fixed="right" align="center" label="值" width="150">
<el-table-column align="center" label="值" width="80">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.simulateValue" class="!w-60px" /> <el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
placeholder="输入值"
size="small"
/>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- TODO @super发送按钮可以放在右侧哈因为我们的 simulateValue 就在最右侧 --> <div class="flex justify-between items-center mt-4">
<div class="mt-10px"> <span class="text-sm text-gray-600">
<el-button type="primary" @click="handlePropertyPost"> 发送</el-button> 设置属性值后点击发送属性上报按钮
</span>
<el-button type="primary" @click="handlePropertyPost">发送属性上报</el-button>
</div> </div>
</ContentWrap> </ContentWrap>
</el-tab-pane> </el-tab-pane>
<!-- 事件上报 --> <!-- 事件上报 -->
<!-- TODO @super待实现 --> <el-tab-pane label="事件上报" :name="IotDeviceMessageMethodEnum.EVENT_POST.method">
<el-tab-pane label="事件上报" name="event">
<ContentWrap> <ContentWrap>
<!-- TODO @super因为事件是每个 event 去模拟而不是类似属性的批量上传所以可以每一列后面有个"模拟"按钮另外"值"使用 textarea高度 3 --> <el-table :data="eventList" :show-overflow-tooltip="true" :stripe="true">
<!-- <el-table v-loading="loading" :data="eventList" :stripe="true">
<el-table-column label="功能名称" align="center" prop="name" />
<el-table-column label="标识符" align="center" prop="identifier" />
<el-table-column label="数据类型" align="center" prop="dataType" />
<el-table-column <el-table-column
label="数据定义" fixed="left"
align="center" align="center"
prop="specs" label="功能名称"
:show-overflow-tooltip="true" prop="name"
width="120"
/> />
<el-table-column label="值" align="center" width="80"> <el-table-column
fixed="left"
align="center"
label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="center" label="数据类型" width="100">
<template #default="{ row }">
{{ row.event?.dataType ?? '-' }}
</template>
</el-table-column>
<el-table-column align="left" label="数据定义" min-width="200">
<template #default="{ row }">
<DataDefinition :data="row" />
</template>
</el-table-column>
<el-table-column align="center" label="值" width="200">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.simulateValue" class="!w-60px" /> <el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
type="textarea"
:rows="3"
placeholder="输入事件参数JSON格式"
size="small"
/>
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="操作" width="100">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEventPost(scope.row)">
上报事件
</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="mt-10px">
<el-button type="primary" @click="handleEventReport">发送</el-button>
</div> -->
</ContentWrap> </ContentWrap>
</el-tab-pane> </el-tab-pane>
<!-- 状态变更 --> <!-- 状态变更 -->
<el-tab-pane label="状态变更" name="status"> <el-tab-pane label="状态变更" :name="IotDeviceMessageMethodEnum.STATE_UPDATE.method">
<ContentWrap> <ContentWrap>
<div class="flex gap-4"> <div class="flex gap-4">
<el-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)"> <el-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)">
@ -90,39 +124,106 @@
</el-tab-pane> </el-tab-pane>
<!-- 下行指令调试 --> <!-- 下行指令调试 -->
<!-- TODO @super待实现 --> <el-tab-pane label="下行指令调试" name="downstream">
<el-tab-pane label="下行指令调试" name="down"> <el-tabs v-if="activeTab === 'downstream'" v-model="downstreamTab">
<el-tabs v-if="activeTab === 'down'" v-model="subTab">
<!-- 属性调试 --> <!-- 属性调试 -->
<el-tab-pane label="属性调试" name="propertyDebug"> <el-tab-pane label="属性设置" :name="IotDeviceMessageMethodEnum.PROPERTY_SET.method">
<ContentWrap> <ContentWrap>
<!-- <el-table v-loading="loading" :data="propertyList" :stripe="true"> <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
<el-table-column label="功能名称" align="center" prop="name" />
<el-table-column label="标识符" align="center" prop="identifier" />
<el-table-column label="数据类型" align="center" prop="dataType" />
<el-table-column <el-table-column
label="数据定义" fixed="left"
align="center" align="center"
prop="specs" label="功能名称"
:show-overflow-tooltip="true" prop="name"
width="120"
/> />
<el-table-column label="值" align="center" width="80"> <el-table-column
fixed="left"
align="center"
label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="center" label="数据类型" width="100">
<template #default="{ row }">
{{ row.property?.dataType ?? '-' }}
</template>
</el-table-column>
<el-table-column align="left" label="数据定义" min-width="200">
<template #default="{ row }">
<DataDefinition :data="row" />
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="值" width="150">
<template #default="scope"> <template #default="scope">
<el-input v-model="scope.row.simulateValue" class="!w-60px" /> <el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
placeholder="输入值"
size="small"
/>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="mt-10px"> <div class="flex justify-between items-center mt-4">
<el-button type="primary" @click="handlePropertyGet">获取</el-button> <span class="text-sm text-gray-600">
</div> --> 设置属性值后点击发送属性设置按钮
</span>
<el-button type="primary" @click="handlePropertySet">发送属性设置</el-button>
</div>
</ContentWrap> </ContentWrap>
</el-tab-pane> </el-tab-pane>
<!-- 服务调用 --> <!-- 服务调用 -->
<!-- TODO @super待实现 --> <el-tab-pane
<el-tab-pane label="服务调用" name="service"> label="设备服务调用"
:name="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
>
<ContentWrap> <ContentWrap>
<!-- 服务调用相关内容 --> <el-table :data="serviceList" :show-overflow-tooltip="true" :stripe="true">
<el-table-column
fixed="left"
align="center"
label="服务名称"
prop="name"
width="120"
/>
<el-table-column
fixed="left"
align="center"
label="标识符"
prop="identifier"
width="120"
/>
<el-table-column align="left" label="输入参数" min-width="200">
<template #default="{ row }">
<DataDefinition :data="row" />
</template>
</el-table-column>
<el-table-column align="center" label="参数值" width="200">
<template #default="scope">
<el-input
:model-value="getFormValue(scope.row.identifier)"
@update:model-value="setFormValue(scope.row.identifier, $event)"
type="textarea"
:rows="3"
placeholder="输入服务参数JSON格式"
size="small"
/>
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="操作" width="100">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleServiceInvoke(scope.row)"
>
服务调用
</el-button>
</template>
</el-table-column>
</el-table>
</ContentWrap> </ContentWrap>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@ -142,119 +243,60 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ProductVO } from '@/api/iot/product/product' import { ProductVO } from '@/api/iot/product/product'
import { SimulatorData, ThingModelApi } from '@/api/iot/thingmodel' import { ThingModelData } from '@/api/iot/thingmodel'
import { DeviceApi, DeviceStateEnum, DeviceVO } from '@/api/iot/device/device' import { DeviceApi, DeviceStateEnum, DeviceVO } from '@/api/iot/device/device'
import DeviceDetailsMessage from './DeviceDetailsMessage.vue' import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
import { DataDefinition } from '@/views/iot/thingmodel/components' import { DataDefinition } from '@/views/iot/thingmodel/components'
import { getDataTypeOptionsLabel, IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants' import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
const props = defineProps<{ const props = defineProps<{
product: ProductVO product: ProductVO
device: DeviceVO device: DeviceVO
thingModelList: ThingModelData[]
}>() }>()
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const activeTab = ref('up') // TODO @superupstream 上行、downstream 下行 const activeTab = ref('upstream') // 上行upstream、下行downstream
const subTab = ref('property') // TODO @superupstreamTab const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method) // 上行子标签
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method) // 下行子标签
const deviceMessageRef = ref() // 设备消息组件引用 const deviceMessageRef = ref() // 设备消息组件引用
const deviceMessageRefresnhDelay = 2000 // 延迟 N 秒,保证模拟上行的消息被处理 const deviceMessageRefreshDelay = 2000 // 延迟 N 秒,保证模拟上行的消息被处理
const loading = ref(false) // 表单数据:存储用户输入的模拟值
const queryParams = reactive({ const formData = ref<Record<string, string>>({})
type: undefined as number | undefined, // TODO @supertype 默认给个第一个 tab 对应的,避免下面 watch 爆红
productId: -1
})
const list = ref<SimulatorData[]>([]) // 物模型列表的数据 TODO @superthingModelList
/** 查询物模型列表 */ // 根据类型过滤物模型数据
// TODO @supergetThingModelList 更精准 const getFilteredThingModelList = (type: number) => {
// TODO @haohao目前 index.vue 已经有了 thingModels可以考虑服用下 return props.thingModelList.filter((item) => item.type === type)
const getList = async () => { }
loading.value = true const propertyList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY))
try { const eventList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.EVENT))
queryParams.productId = props.product?.id || -1 const serviceList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE))
const data = await ThingModelApi.getThingModelList(queryParams)
// 转换数据,添加 simulateValue 字段 /** 获取表单值的辅助函数 */
// TODO @super貌似下面的 simulateValue 不设置也可以? const getFormValue = (identifier: string | number | undefined) => {
list.value = data.map((item) => ({ if (!identifier) return ''
...item, return formData.value[String(identifier)] || ''
simulateValue: '' }
})) /** 设置表单值的辅助函数 */
} finally { const setFormValue = (identifier: string | number | undefined, value: string) => {
loading.value = false if (!identifier) return
} formData.value[String(identifier)] = value
} }
// // 功能列表数据结构定义 /** 模拟属性上报 */
// interface TableItem {
// name: string
// identifier: string
// value: string | number
// }
// // 添加计算属性来过滤物模型数据
// const propertyList = computed(() => {
// return list.value
// .filter((item) => item.type === 'property')
// .map((item) => ({
// name: item.name,
// identifier: item.identifier,
// value: ''
// }))
// })
// const eventList = computed(() => {
// return list.value
// .filter((item) => item.type === 'event')
// .map((item) => ({
// name: item.name,
// identifier: item.identifier,
// value: ''
// }))
// })
/** 监听标签页变化 */
// todo:后续改成查询字典
watch(
[activeTab, subTab],
([newActiveTab, newSubTab]) => {
// 根据标签页设置查询类型
if (newActiveTab === 'up') {
switch (newSubTab) {
case 'property':
queryParams.type = 1
break
case 'event':
queryParams.type = 3
break
}
} else if (newActiveTab === 'down') {
switch (newSubTab) {
case 'propertyDebug':
queryParams.type = 1
break
case 'service':
queryParams.type = 2
break
}
}
getList() // 切换标签时重新获取数据
},
{ immediate: true }
)
/** 处理属性上报 */
const handlePropertyPost = async () => { const handlePropertyPost = async () => {
// TODO @super:数据类型效验
const data: Record<string, any> = {} const data: Record<string, any> = {}
list.value.forEach((item) => { propertyList.value.forEach((item) => {
// 只有当 simulateValue 有值时才添加到 content 中 const value = getFormValue(item.identifier)
// TODO @super直接 if (item.simulateValue) 就可以哈js 这块还是比较灵活的 if (value && item.identifier) {
if (item.simulateValue !== undefined && item.simulateValue !== '' && item.identifier) { data[String(item.identifier)] = value
// TODO @super这里有个红色的 idea 告警,觉得去除下
data[item.identifier] = item.simulateValue
} }
}) })
if (Object.keys(data).length === 0) {
message.warning('请至少设置一个属性值')
return
}
try { try {
await DeviceApi.sendDeviceMessage({ await DeviceApi.sendDeviceMessage({
@ -263,42 +305,45 @@ const handlePropertyPost = async () => {
params: data params: data
}) })
message.success('属性上报成功') message.success('属性上报成功')
deviceMessageRef.value.refresh(deviceMessageRefresnhDelay) deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) { } catch (error) {
message.error('属性上报失败') message.error('属性上报失败')
} }
} }
// // 处理事件上报 /** 模拟事件上报 */
// const handleEventReport = async () => { const handleEventPost = async (eventItem: ThingModelData) => {
// const contentObj: Record<string, any> = {} const value = getFormValue(eventItem.identifier)
// list.value if (!value) {
// .filter(item => item.type === 'event') message.warning('请输入事件参数')
// .forEach((item) => { return
// if (item.simulateValue !== undefined && item.simulateValue !== '') { }
// contentObj[item.identifier] = item.simulateValue let eventParams: any
// } try {
// }) eventParams = JSON.parse(value)
} catch {
message.error('事件参数格式不正确请输入有效的JSON格式')
return
}
// const reportData: ReportData = { try {
// productKey: props.product.productKey, await DeviceApi.sendDeviceMessage({
// deviceKey: props.device.deviceKey, deviceId: props.device.id,
// type: 'event', method: IotDeviceMessageMethodEnum.EVENT_POST.method,
// subType: list.value.find(item => item.type === 'event')?.identifier || '', params: {
// reportTime: new Date().toISOString(), identifier: String(eventItem.identifier),
// content: JSON.stringify(contentObj) // 转换为 JSON 字符串 value: eventParams,
// } time: Date.now()
}
})
message.success(`事件【${String(eventItem.name)}】上报成功`)
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error(`事件【${String(eventItem.name)}】上报失败`)
}
}
// try { /** 模拟设备状态 */
// // TODO: 调用API发送数据
// console.log('上报数据:', reportData)
// message.success('事件上报成功')
// } catch (error) {
// message.error('事件上报失败')
// }
// }
/** 处理设备状态 */
const handleDeviceState = async (state: number) => { const handleDeviceState = async (state: number) => {
try { try {
await DeviceApi.sendDeviceMessage({ await DeviceApi.sendDeviceMessage({
@ -309,21 +354,67 @@ const handleDeviceState = async (state: number) => {
} }
}) })
message.success(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}成功`) message.success(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}成功`)
deviceMessageRef.value.refresh(deviceMessageRefresnhDelay) deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) { } catch (error) {
message.error(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}失败`) message.error(`设备${state === DeviceStateEnum.ONLINE ? '上线' : '下线'}失败`)
} }
} }
// 处理属性获取 /** 模拟属性设置 */
const handlePropertyGet = async () => { const handlePropertySet = async () => {
// TODO: 实现属性获取逻辑 const data: Record<string, any> = {}
message.success('属性获取成功') propertyList.value.forEach((item) => {
const value = getFormValue(item.identifier)
if (value && item.identifier) {
data[String(item.identifier)] = value
}
})
if (Object.keys(data).length === 0) {
message.warning('请至少设置一个属性值')
return
}
try {
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
params: data
})
message.success('属性设置成功')
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error('属性设置失败')
}
} }
// 初始化 /** 模拟服务调用 */
onMounted(() => { const handleServiceInvoke = async (serviceItem: ThingModelData) => {
getList() const value = getFormValue(serviceItem.identifier)
}) if (!value) {
// TODO @芋艿:后续再详细 review 下; message.warning('请输入服务参数')
return
}
let serviceParams: any
try {
serviceParams = JSON.parse(value)
} catch {
message.error('服务参数格式不正确请输入有效的JSON格式')
return
}
try {
await DeviceApi.sendDeviceMessage({
deviceId: props.device.id,
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
params: {
identifier: String(serviceItem.identifier),
inputParams: serviceParams
}
})
message.success(`服务【${String(serviceItem.name)}】调用成功`)
deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
} catch (error) {
message.error(`服务【${String(serviceItem.name)}】调用失败`)
}
}
</script> </script>

View File

@ -26,6 +26,7 @@
v-if="activeTab === 'simulator'" v-if="activeTab === 'simulator'"
:product="product" :product="product"
:device="device" :device="device"
:thing-model-list="thingModelList"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="设备配置" name="config"> <el-tab-pane label="设备配置" name="config">