Merge branch 'feature/iot' of https://gitee.com/alwayssuper/yudao-ui-admin-vue3 into feature/iot

# Conflicts:
#	pnpm-lock.yaml
This commit is contained in:
YunaiV 2025-01-09 12:38:43 +08:00
commit f2f414a965
6 changed files with 591 additions and 2 deletions

View File

@ -87,7 +87,7 @@
"source.fixAll.stylelint": "explicit"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "octref.vetur"
},
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",

View File

@ -63,6 +63,16 @@ export enum DeviceStatusEnum {
DISABLED = 3 // 已禁用
}
// IoT 模拟设备数据
export interface SimulatorDataVO {
productKey: string
deviceKey: string
type: string
subType: string
reportTime: number // 时间戳
content: string // 存储 JSON 字符串
}
// 设备 API
export const DeviceApi = {
// 查询设备分页
@ -136,5 +146,14 @@ export const DeviceApi = {
// 获取导入模板
importDeviceTemplate: async () => {
return await request.download({ url: `/iot/device/get-import-template` })
},
// 模拟设备
simulatorDevice: async (data: SimulatorDataVO) => {
return await request.post({ url: `/iot/device/data/simulator`, data })
},
//查询设备日志分页
getDeviceLogPage: async (params: any) => {
return await request.get({ url: `/iot/device/data/log/page`, params })
}
}

View File

@ -17,6 +17,13 @@ export interface ThingModelData {
service?: ThingModelService // 服务
}
/**
* IoT
*/
export interface SimulatorData extends ThingModelData {
simulateValue?: string | number // 用于存储模拟值
}
/**
* ThingModelProperty
*/
@ -45,6 +52,11 @@ export const ThingModelApi = {
return await request.get({ url: `/iot/thing-model/page`, params })
},
// 获得产品物模型列表
getThingModelList: async (params: any) => {
return await request.get({ url: `/iot/thing-model/list`, params })
},
// 获得产品物模型
getThingModelListByProductId: async (params: any) => {
return await request.get({

View File

@ -0,0 +1,164 @@
<template>
<ContentWrap>
<!-- 搜索区域 -->
<el-form :model="queryParams" inline>
<el-form-item>
<el-select v-model="queryParams.type" placeholder="所有" class="!w-120px">
<el-option label="所有" value="" />
<el-option label="状态" value="state" />
<el-option label="事件" value="event" />
<el-option label="属性" value="property" />
<el-option label="服务" value="service" />
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="queryParams.keyword" placeholder="日志识符" class="!w-200px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-switch v-model="autoRefresh" class="ml-10px" /> 定时刷新
</el-form-item>
</el-form>
<!-- 日志列表 -->
<el-table v-loading="loading" :data="logList" :stripe="true" class="whitespace-nowrap">
<el-table-column label="时间" align="center" prop="time" width="180">
<template #default="scope">
{{ formatDate(scope.row.time) }}
</template>
</el-table-column>
<el-table-column label="类型" align="center" prop="type" width="120" />
<el-table-column label="名称(标识符)" align="center" prop="subType" width="120" />
<el-table-column label="内容" align="center" prop="content" :show-overflow-tooltip="true" />
</el-table>
<!-- 分页 -->
<div class="mt-10px flex justify-end">
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getLogList"
/>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { DeviceApi } from '@/api/iot/device/device'
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
const props = defineProps<{
deviceKey: number
}>()
//TODO:使 type subType
//
const queryParams = reactive({
deviceKey: props.deviceKey,
// type: '',
// keyword: '',
pageNo: 1,
pageSize: 10
})
//
const loading = ref(false)
const total = ref(0)
const logList = ref([])
const autoRefresh = ref(false)
let timer: any = null
//
const typeMap = {
lifetime: '生命周期',
state: '设备状态',
property: '属性',
event: '事件',
service: '服务'
}
/** 查询日志列表 */
const getLogList = async () => {
if (!props.deviceKey) return
loading.value = true
try {
const res = await DeviceApi.getDeviceLogPage(queryParams)
total.value = res.total
logList.value = res.list.map((item: any) => {
const log = {
time: item.reportTime,
type: item.type,
subType: item.subType,
content: item.content
}
return log
})
} finally {
loading.value = false
}
}
/** 获取日志名称 */
const getLogName = (log: any) => {
const { type, identifier } = log
let name = '未知'
if (type === 'property') {
if (identifier === 'set_reply') name = '设置回复'
else if (identifier === 'report') name = '上报'
else if (identifier === 'set') name = '设置'
} else if (type === 'state') {
name = identifier === 'online' ? '上线' : '下线'
} else if (type === 'lifetime') {
name = identifier === 'register' ? '注册' : name
}
return `${name}(${identifier})`
}
/** 搜索操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getLogList()
}
/** 监听自动刷新 */
watch(autoRefresh, (newValue) => {
if (newValue) {
timer = setInterval(() => {
getLogList()
}, 5000)
} else {
clearInterval(timer)
timer = null
}
})
/** 监听设备ID变化 */
watch(
() => props.deviceKey,
(newValue) => {
if (newValue) {
handleQuery()
}
}
)
/** 组件卸载时清除定时器 */
onBeforeUnmount(() => {
if (timer) {
clearInterval(timer)
}
})
/** 初始化 */
onMounted(() => {
if (props.deviceKey) {
getLogList()
}
})
</script>

View File

@ -0,0 +1,387 @@
<template>
<ContentWrap>
<el-row :gutter="20">
<!-- 左侧指令调试区域 -->
<el-col :span="12">
<el-tabs v-model="activeTab" type="border-card">
<!-- 上行指令调试 -->
<el-tab-pane label="上行指令调试" name="up">
<el-tabs v-model="subTab" v-if="activeTab === 'up'">
<!-- 属性上报 -->
<el-tab-pane label="属性上报" name="property">
<ContentWrap>
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
<el-table-column align="center" label="功能名称" prop="name" />
<el-table-column align="center" label="标识符" prop="identifier" />
<el-table-column align="center" label="数据类型" prop="identifier">
<template #default="{ row }">
{{ dataTypeOptionsLabel(row.property?.dataType) ?? '-' }}
</template>
</el-table-column>
<el-table-column align="left" label="数据定义" prop="identifier">
<template #default="{ row }">
<!-- 属性 -->
<template v-if="row.type === ThingModelType.PROPERTY">
<!-- 非列表型数值 -->
<div
v-if="
[
DataSpecsDataType.INT,
DataSpecsDataType.DOUBLE,
DataSpecsDataType.FLOAT
].includes(row.property.dataType)
"
>
取值范围{{
`${row.property.dataSpecs.min}~${row.property.dataSpecs.max}`
}}
</div>
<!-- 非列表型文本 -->
<div v-if="DataSpecsDataType.TEXT === row.property.dataType">
数据长度{{ row.property.dataSpecs.length }}
</div>
<!-- 列表型: 数组结构时间特殊 -->
<div
v-if="
[
DataSpecsDataType.ARRAY,
DataSpecsDataType.STRUCT,
DataSpecsDataType.DATE
].includes(row.property.dataType)
"
>
-
</div>
<!-- 列表型: 布尔值枚举 -->
<div
v-if="
[DataSpecsDataType.BOOL, DataSpecsDataType.ENUM].includes(
row.property.dataType
)
"
>
<div>
{{
DataSpecsDataType.BOOL === row.property.dataType
? '布尔值'
: '枚举值'
}}
</div>
<div v-for="item in row.property.dataSpecsList" :key="item.value">
{{ `${item.name}-${item.value}` }}
</div>
</div>
</template>
<!-- 服务 -->
<div v-if="row.type === ThingModelType.SERVICE">
调用方式{{ getCallTypeByValue(row.service.callType) }}
</div>
<!-- 事件 -->
<div v-if="row.type === ThingModelType.EVENT">
事件类型{{ getEventTypeByValue(row.event.type) }}
</div>
</template>
</el-table-column>
<el-table-column label="值" align="center" width="80">
<template #default="scope">
<el-input v-model="scope.row.simulateValue" class="!w-60px" />
</template>
</el-table-column>
</el-table>
<div class="mt-10px">
<el-button type="primary" @click="handlePropertyReport">发送</el-button>
</div>
</ContentWrap>
</el-tab-pane>
<!-- 事件上报 -->
<el-tab-pane label="事件上报" name="event">
<ContentWrap>
<!-- <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
label="数据定义"
align="center"
prop="specs"
:show-overflow-tooltip="true"
/>
<el-table-column label="值" align="center" width="80">
<template #default="scope">
<el-input v-model="scope.row.simulateValue" class="!w-60px" />
</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">
<ContentWrap>
<div class="flex gap-4">
<el-button type="primary" @click="handleDeviceState('online')"
>设备上线</el-button
>
<el-button type="primary" @click="handleDeviceState('offline')"
>设备下线</el-button
>
</div>
</ContentWrap>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<!-- 下行指令调试 -->
<el-tab-pane label="下行指令调试" name="down">
<el-tabs v-model="subTab" v-if="activeTab === 'down'">
<!-- 属性调试 -->
<el-tab-pane label="属性调试" name="propertyDebug">
<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-column
label="数据定义"
align="center"
prop="specs"
:show-overflow-tooltip="true"
/>
<el-table-column label="值" align="center" width="80">
<template #default="scope">
<el-input v-model="scope.row.simulateValue" class="!w-60px" />
</template>
</el-table-column>
</el-table>
<div class="mt-10px">
<el-button type="primary" @click="handlePropertyGet">获取</el-button>
</div> -->
</ContentWrap>
</el-tab-pane>
<!-- 服务调用 -->
<el-tab-pane label="服务调用" name="service">
<ContentWrap>
<!-- 服务调用相关内容 -->
</ContentWrap>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
</el-tabs>
</el-col>
<!-- 右侧设备日志区域 -->
<el-col :span="12">
<el-tabs type="border-card">
<el-tab-pane label="设备日志">
<DeviceDetailsLog :deviceKey="device.deviceKey" />
</el-tab-pane>
</el-tabs>
</el-col>
</el-row>
</ContentWrap>
</template>
<script setup lang="ts">
import { ProductVO } from '@/api/iot/product/product'
import { ThingModelApi, ThingModelData,SimulatorData } from '@/api/iot/thingmodel'
import { DeviceApi, DeviceVO,SimulatorDataVO } from '@/api/iot/device/device'
import DeviceDetailsLog from './DeviceDetailsLog.vue'
import {
DataSpecsDataType,
getCallTypeByValue,
getDataTypeOptionsLabel,
getEventTypeByValue,
ThingModelType
} from '@/views/iot/thingmodel/config'
const message = useMessage() //
const loading = ref(false)
const activeTab = ref('up')
const subTab = ref('property')
const queryParams = reactive({
type: undefined,
productId: -1
})
const dataTypeOptionsLabel = computed(() => (value: string) => getDataTypeOptionsLabel(value)) //
const props = defineProps<{ product: ProductVO; device: DeviceVO }>()
const list = ref<SimulatorData[]>([]) //
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
queryParams.productId = props.product?.id || -1
const data = await ThingModelApi.getThingModelList(queryParams)
// simulateValue
list.value = data.map(item => ({
...item,
simulateValue: ''
}))
} finally {
loading.value = false
}
}
// //
// 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
// case 'status':
// queryParams.type = 'status'
// break
}
} else if (newActiveTab === 'down') {
switch (newSubTab) {
case 'propertyDebug':
queryParams.type = 1
break
case 'service':
queryParams.type = 2
break
}
}
getList() //
},
{ immediate: true }
)
// interface ReportData {
// productKey: string
// deviceKey: string
// type: string
// subType: string
// reportTime: string
// content: string // string JSON
// }
// TODO:
const handlePropertyReport = async () => {
const contentObj: Record<string, any> = {}
list.value.forEach((item) => {
// simulateValue content
if (item.simulateValue !== undefined && item.simulateValue !== '') {
contentObj[item.identifier] = item.simulateValue
}
})
const reportData: SimulatorDataVO = {
productKey: props.product.productKey,
deviceKey: props.device.deviceKey,
type: 'property',
subType: 'report',
reportTime: Date.now(), // reportTime
content: JSON.stringify(contentObj) // JSON
}
try {
await DeviceApi.simulatorDevice(reportData)
message.success('属性上报成功')
} 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 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 {
// // TODO: API
// console.log(':', reportData)
// message.success('')
// } catch (error) {
// message.error('')
// }
// }
// //
// const handleDeviceState = async (state: 'online' | 'offline') => {
// const reportData: ReportData = {
// productKey: props.product.productKey,
// deviceKey: props.device.deviceKey,
// type: 'status',
// subType: state,
// reportTime: new Date().toISOString(),
// content: JSON.stringify({ status: state }) // JSON
// }
// try {
// // TODO: API
// console.log(':', reportData)
// console.log('reportData.content111111111', reportData.content)
// message.success(`${state === 'online' ? '线' : '线'}`)
// } catch (error) {
// message.error(`${state === 'online' ? '线' : '线'}`)
// }
// }
//
const handlePropertyGet = async () => {
// TODO:
message.success('属性获取成功')
}
//
onMounted(() => {
getList()
})
</script>

View File

@ -16,6 +16,12 @@
</el-tab-pane>
<el-tab-pane label="子设备管理" v-if="product.deviceType === DeviceTypeEnum.GATEWAY" />
<el-tab-pane label="设备影子" />
<el-tab-pane label="设备日志" name="log">
<DeviceDetailsLog v-if="activeTab === 'log'" :deviceKey="device.deviceKey" />
</el-tab-pane>
<el-tab-pane label="模拟设备" name="simulator">
<DeviceDetailsSimulator v-if="activeTab === 'simulator'" :product="product" :device="device" />
</el-tab-pane>
</el-tabs>
</el-col>
</template>
@ -26,7 +32,8 @@ import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product
import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
import DeviceDetailsModel from './DeviceDetailsModel.vue'
import DeviceDetailsLog from './DeviceDetailsLog.vue'
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
defineOptions({ name: 'IoTDeviceDetail' })
const route = useRoute()