feat:【IoT 物联网】完整实现“数据流转”功能(后台管理)

This commit is contained in:
YunaiV
2025-06-25 21:43:16 +08:00
parent b1ae56281d
commit b5bd1a8fb4
7 changed files with 58 additions and 62 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<Dialog :title="dialogTitle" v-model="dialogVisible" width="860"> <Dialog :title="dialogTitle" v-model="dialogVisible" width="870">
<el-form <el-form
ref="formRef" ref="formRef"
:model="formData" :model="formData"
@ -78,7 +78,7 @@ const formData = ref({
const formRules = reactive({ const formRules = reactive({
name: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }], name: [{ required: true, message: '规则名称不能为空', trigger: 'blur' }],
status: [{ required: true, message: '规则状态不能为空', trigger: 'blur' }], status: [{ required: true, message: '规则状态不能为空', trigger: 'blur' }],
// sourceConfigs: [{ required: true, message: '数据源配置数组不能为空', trigger: 'blur' }], sourceConfigs: [{ required: true, message: '数据源配置数组不能为空', trigger: 'blur' }],
sinkIds: [{ required: true, message: '数据目的编号数组不能为空', trigger: 'blur' }] sinkIds: [{ required: true, message: '数据目的编号数组不能为空', trigger: 'blur' }]
}) })
const formRef = ref() // 表单 Ref const formRef = ref() // 表单 Ref
@ -115,16 +115,16 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => { const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 校验数据源配置 // 校验数据源配置
await sourceConfigRef.value?.validate() await sourceConfigRef.value?.validate()
formData.value.sourceConfigs = sourceConfigRef.value?.getData() || []
// 校验表单
await formRef.value.validate()
// 提交请求 // 提交请求
formLoading.value = true formLoading.value = true
try { try {
const data = { ...formData.value } as unknown as DataRule const data = { ...formData.value } as unknown as DataRule
data.sourceConfigs = sourceConfigRef.value?.getData() || []
if (formType.value === 'create') { if (formType.value === 'create') {
await DataRuleApi.createDataRule(data) await DataRuleApi.createDataRule(data)
message.success(t('common.createSuccess')) message.success(t('common.createSuccess'))
@ -141,7 +141,7 @@ const submitForm = async () => {
} }
/** 重置表单 */ /** 重置表单 */
const resetForm = () => { const resetForm = async () => {
formData.value = { formData.value = {
id: undefined, id: undefined,
name: undefined, name: undefined,
@ -152,8 +152,7 @@ const resetForm = () => {
} }
formRef.value?.resetFields() formRef.value?.resetFields()
// 重置数据源配置 // 重置数据源配置
nextTick(() => { await nextTick()
sourceConfigRef.value?.setData([]) sourceConfigRef.value?.setData([])
})
} }
</script> </script>

View File

@ -15,6 +15,7 @@
placeholder="请选择产品" placeholder="请选择产品"
@change="handleProductChange(row, $index)" @change="handleProductChange(row, $index)"
clearable clearable
filterable
style="width: 100%" style="width: 100%"
> >
<el-option <el-option
@ -33,10 +34,11 @@
<el-select <el-select
v-model="row.deviceId" v-model="row.deviceId"
placeholder="请选择设备" placeholder="请选择设备"
@change="handleDeviceChange(row, $index)"
clearable clearable
filterable
style="width: 100%" style="width: 100%"
> >
<el-option label="全部设备" :value="0" />
<el-option <el-option
v-for="device in getFilteredDevices(row.productId)" v-for="device in getFilteredDevices(row.productId)"
:key="device.id" :key="device.id"
@ -47,14 +49,15 @@
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="消息方法" min-width="180"> <el-table-column label="消息" min-width="150">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<el-form-item :prop="`${$index}.method`" :rules="formRules.method" class="mb-0px!"> <el-form-item :prop="`${$index}.method`" :rules="formRules.method" class="mb-0px!">
<el-select <el-select
v-model="row.method" v-model="row.method"
placeholder="请选择消息方法" placeholder="请选择消息"
@change="handleMethodChange(row, $index)" @change="handleMethodChange(row, $index)"
clearable clearable
filterable
style="width: 100%" style="width: 100%"
> >
<el-option <el-option
@ -67,7 +70,7 @@
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="标识符" min-width="150"> <el-table-column label="标识符" min-width="200">
<template #default="{ row, $index }"> <template #default="{ row, $index }">
<el-form-item :prop="`${$index}.identifier`" class="mb-0px!"> <el-form-item :prop="`${$index}.identifier`" class="mb-0px!">
<el-select <el-select
@ -75,6 +78,7 @@
v-model="row.identifier" v-model="row.identifier"
placeholder="请选择标识符" placeholder="请选择标识符"
clearable clearable
filterable
style="width: 100%" style="width: 100%"
v-loading="row.identifierLoading" v-loading="row.identifierLoading"
> >
@ -85,7 +89,6 @@
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
<span v-else>-</span>
</el-form-item> </el-form-item>
</template> </template>
</el-table-column> </el-table-column>
@ -105,12 +108,12 @@
import { ProductApi } from '@/api/iot/product/product' import { ProductApi } from '@/api/iot/product/product'
import { DeviceApi } from '@/api/iot/device/device' import { DeviceApi } from '@/api/iot/device/device'
import { ThingModelApi } from '@/api/iot/thingmodel' import { ThingModelApi } from '@/api/iot/thingmodel'
import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants' import { IotDeviceMessageMethodEnum, IotThingModelTypeEnum } from '@/views/iot/utils/constants'
const formData = ref<any[]>([]) const formData = ref<any[]>([])
const productList = ref<any[]>([]) // 产品列表 const productList = ref<any[]>([]) // 产品列表
const deviceList = ref<any[]>([]) // 设备列表 const deviceList = ref<any[]>([]) // 设备列表
const thingModelMap = ref<Map<number, any[]>>(new Map()) // 缓存物模型数据key 为 productId const thingModelCache = ref<Map<number, any[]>>(new Map()) // 缓存物模型数据key 为 productId
const formRules = reactive({ const formRules = reactive({
productId: [{ required: true, message: '产品不能为空', trigger: 'change' }], productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
@ -124,7 +127,7 @@ const upstreamMethods = computed(() => {
return Object.values(IotDeviceMessageMethodEnum).filter((item) => item.upstream) return Object.values(IotDeviceMessageMethodEnum).filter((item) => item.upstream)
}) })
/** 根据产品ID过滤设备 */ /** 根据产品 ID 过滤设备 */
const getFilteredDevices = (productId: number) => { const getFilteredDevices = (productId: number) => {
if (!productId) return [] if (!productId) return []
return deviceList.value.filter((device: any) => device.productId === productId) return deviceList.value.filter((device: any) => device.productId === productId)
@ -132,27 +135,24 @@ const getFilteredDevices = (productId: number) => {
/** 判断是否需要显示标识符选择器 */ /** 判断是否需要显示标识符选择器 */
const shouldShowIdentifierSelect = (row: any) => { const shouldShowIdentifierSelect = (row: any) => {
return ( return [
row.method === IotDeviceMessageMethodEnum.EVENT_POST.method || IotDeviceMessageMethodEnum.EVENT_POST.method,
row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method IotDeviceMessageMethodEnum.PROPERTY_POST.method
) ].includes(row.method)
} }
/** 获取物模型选项 */ /** 获取物模型选项 */
const getThingModelOptions = (row: any) => { const getThingModelOptions = (row: any) => {
if (!row.productId || !shouldShowIdentifierSelect(row)) return [] if (!row.productId || !shouldShowIdentifierSelect(row)) {
return []
const thingModels: any[] = thingModelMap.value.get(row.productId) || [] }
let filteredModels: any[] = [] const thingModels: any[] = thingModelCache.value.get(row.productId) || []
let filteredModels: any[] = []
if (row.method === IotDeviceMessageMethodEnum.EVENT_POST.method) { if (row.method === IotDeviceMessageMethodEnum.EVENT_POST.method) {
// 事件类型type = 3 filteredModels = thingModels.filter((item: any) => item.type === IotThingModelTypeEnum.EVENT)
filteredModels = thingModels.filter((item: any) => item.type === 3) } else if (row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method) {
} else if (row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method) { filteredModels = thingModels.filter((item: any) => item.type === IotThingModelTypeEnum.PROPERTY)
// 属性类型type = 1
filteredModels = thingModels.filter((item: any) => item.type === 1)
} }
return filteredModels.map((item: any) => ({ return filteredModels.map((item: any) => ({
label: `${item.name} (${item.identifier})`, label: `${item.name} (${item.identifier})`,
value: item.identifier value: item.identifier
@ -179,39 +179,30 @@ const loadDeviceList = async () => {
/** 加载物模型数据 */ /** 加载物模型数据 */
const loadThingModel = async (productId: number) => { const loadThingModel = async (productId: number) => {
if (thingModelMap.value.has(productId)) { // 已缓存,无需重复加载
return // 已缓存,无需重复加载 if (thingModelCache.value.has(productId)) {
return
} }
try { try {
const thingModels = await ThingModelApi.getThingModelList({ productId }) const thingModels = await ThingModelApi.getThingModelList({ productId })
thingModelMap.value.set(productId, thingModels) thingModelCache.value.set(productId, thingModels)
} catch (error) { } catch (error) {
console.error('加载物模型失败:', error) console.error('加载物模型失败:', error)
thingModelMap.value.set(productId, [])
} }
} }
/** 产品变化时处理 */ /** 产品变化时处理 */
const handleProductChange = async (row: any, _index: number) => { const handleProductChange = async (row: any, _index: number) => {
// 清空其他字段 row.deviceId = 0
row.deviceId = undefined
row.method = undefined row.method = undefined
row.identifier = undefined row.identifier = undefined
row.identifierLoading = false
// 根据产品ID过滤设备列表不需要额外处理计算属性会自动过滤
}
/** 设备变化时处理 */
const handleDeviceChange = (row: any, _index: number) => {
// 设备变化时可以做一些额外处理
} }
/** 消息方法变化时处理 */ /** 消息方法变化时处理 */
const handleMethodChange = async (row: any, _index: number) => { const handleMethodChange = async (row: any, _index: number) => {
// 清空标识符 // 清空标识符
row.identifier = undefined row.identifier = undefined
// 如果需要加载物模型数据 // 如果需要加载物模型数据
if (shouldShowIdentifierSelect(row) && row.productId) { if (shouldShowIdentifierSelect(row) && row.productId) {
row.identifierLoading = true row.identifierLoading = true
@ -254,7 +245,6 @@ const setData = (data: any[]) => {
...item, ...item,
identifierLoading: false identifierLoading: false
})) }))
// 为已有数据预加载物模型 // 为已有数据预加载物模型
data?.forEach(async (item) => { data?.forEach(async (item) => {
if (item.productId && shouldShowIdentifierSelect(item)) { if (item.productId && shouldShowIdentifierSelect(item)) {

View File

@ -76,8 +76,12 @@
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="数据源配置数组" align="center" prop="sourceConfigs" /> <el-table-column label="数据源" align="center" prop="sourceConfigs">
<el-table-column label="数据目的编号数组" align="center" prop="sinkIds" /> <template #default="scope"> {{ scope.row.sourceConfigs?.length || 0 }} </template>
</el-table-column>
<el-table-column label="数据目的" align="center" prop="sinkIds">
<template #default="scope"> {{ scope.row.sinkIds?.length || 0 }} </template>
</el-table-column>
<el-table-column <el-table-column
label="创建时间" label="创建时间"
align="center" align="center"
@ -142,7 +146,6 @@ const queryParams = reactive({
createTime: [] createTime: []
}) })
const queryFormRef = ref() // 搜索的表单 const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */ /** 查询列表 */
const getList = async () => { const getList = async () => {

View File

@ -198,7 +198,7 @@ const handleDelete = async (id: number) => {
// 删除的二次确认 // 删除的二次确认
await message.delConfirm() await message.delConfirm()
// 发起删除 // 发起删除
await DataSinkApi.deleteDataBridge(id) await DataSinkApi.deleteDataSink(id)
message.success(t('common.delSuccess')) message.success(t('common.delSuccess'))
// 刷新列表 // 刷新列表
await getList() await getList()

View File

@ -4,7 +4,7 @@
<span class="mr-10px w-80px">数据流转目的</span> <span class="mr-10px w-80px">数据流转目的</span>
<el-select v-model="dataBridgeId" class="!w-240px" clearable placeholder="选择数据流转目的"> <el-select v-model="dataBridgeId" class="!w-240px" clearable placeholder="选择数据流转目的">
<el-option <el-option
v-for="bridge in dataBridgeList" v-for="bridge in dataSinkList"
:key="bridge.id" :key="bridge.id"
:label="bridge.name" :label="bridge.name"
:value="bridge.id" :value="bridge.id"
@ -25,14 +25,10 @@ const props = defineProps<{ modelValue: any }>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const dataBridgeId = useVModel(props, 'modelValue', emits) const dataBridgeId = useVModel(props, 'modelValue', emits)
const dataBridgeList = ref<any[]>([]) // 数据流转目的列表 const dataSinkList = ref<any[]>([]) // 数据流转目的列表
/** 获取数据流转目的列表 */ onMounted(async () => {
const getDataBridgeList = async () => { // 获取数据流转目的列表
dataBridgeList.value = await DataSinkApi.getDataSinkSimpleList() dataSinkList.value = await DataSinkApi.getDataSinkSimpleList()
}
onMounted(() => {
getDataBridgeList()
}) })
</script> </script>

View File

@ -55,6 +55,7 @@ export const getDataTypeOptionsLabel = (value: string) => {
return dataType && `${dataType.value}(${dataType.label})` return dataType && `${dataType.value}(${dataType.label})`
} }
// TODO @puhui999使用 ThingModelTypeEnum 替换
// IOT 产品物模型类型枚举类 // IOT 产品物模型类型枚举类
export const ThingModelType = { export const ThingModelType = {
PROPERTY: 1, // 属性 PROPERTY: 1, // 属性

View File

@ -52,3 +52,10 @@ export const IotDeviceMessageMethodEnum = {
upstream: false upstream: false
} }
} }
// IOT 产品物模型类型枚举类
export const IotThingModelTypeEnum = {
PROPERTY: 1, // 属性
SERVICE: 2, // 服务
EVENT: 3 // 事件
}