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 // 服务
}
/**
* IoT 模拟设备
*/
// TODO @super和 ThingModelSimulatorData 会不会好点
export interface SimulatorData extends ThingModelData {
simulateValue?: string | number // 用于存储模拟值 TODO @super字段使用 value 会不会好点
}
/**
* ThingModelProperty 类型
*/

View File

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

View File

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