fix: iot warn

This commit is contained in:
xingyu4j
2025-10-20 10:37:23 +08:00
parent 948cb916c4
commit c6ef77694e
35 changed files with 476 additions and 455 deletions

View File

@@ -5,6 +5,7 @@ import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { z } from '#/adapter/form';
import { getSimpleDeviceList } from '#/api/iot/device/device';
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
import {
DeviceTypeEnum,
@@ -27,7 +28,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',
@@ -55,12 +56,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '网关设备',
component: 'ApiSelect',
componentProps: {
api: async () => {
const { getSimpleDeviceList } = await import(
'#/api/iot/device/device'
);
return getSimpleDeviceList(DeviceTypeEnum.GATEWAY);
},
api: () => getSimpleDeviceList(DeviceTypeEnum.GATEWAY),
labelField: 'nickname',
valueField: 'id',
placeholder: '子设备可选择父设备',
@@ -93,7 +89,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '设备分组',
component: 'ApiSelect',
componentProps: {
api: getSimpleDeviceGroupList,
api: () => getSimpleDeviceGroupList(),
labelField: 'name',
valueField: 'id',
mode: 'multiple',
@@ -160,7 +156,7 @@ export function useGroupFormSchema(): VbenFormSchema[] {
label: '设备分组',
component: 'ApiSelect',
componentProps: {
api: getSimpleDeviceGroupList,
api: () => getSimpleDeviceGroupList(),
labelField: 'name',
valueField: 'id',
mode: 'multiple',
@@ -203,7 +199,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',
@@ -253,7 +249,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '设备分组',
component: 'ApiSelect',
componentProps: {
api: getSimpleDeviceGroupList,
api: () => getSimpleDeviceGroupList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择设备分组',

View File

@@ -32,7 +32,6 @@ import { getSimpleProductList } from '#/api/iot/product/product';
import { $t } from '#/locales';
import { useGridColumns } from './data';
// @ts-ignore
import DeviceCardView from './modules/DeviceCardView.vue';
import DeviceForm from './modules/DeviceForm.vue';
import DeviceGroupForm from './modules/DeviceGroupForm.vue';

View File

@@ -1,15 +1,13 @@
<script setup lang="ts">
import type { IotDeviceApi } from '#/api/iot/device/device';
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import {
createDevice,
getDevice,
updateDevice,
} from '#/api/iot/device/device';
import type { IotDeviceApi } from '#/api/iot/device/device';
import { message } from 'ant-design-vue';
import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
import { $t } from '#/locales';
import { useFormSchema } from '../data';

View File

@@ -44,7 +44,7 @@ const [Modal, modalApi] = useVbenModal({
// 关闭并提示
await modalApi.close();
emit('success');
message.success($t('common.updateSuccess'));
message.success($t('ui.actionMessage.operationSuccess'));
} finally {
modalApi.unlock();
}

View File

@@ -80,6 +80,7 @@ async function saveConfig() {
try {
config.value = JSON.parse(configString.value);
} catch (error) {
console.error('JSON格式错误:', error);
message.error({ content: 'JSON格式错误请修正后再提交' });
return;
}
@@ -176,33 +177,31 @@ async function updateDeviceConfig() {
placeholder="请输入 JSON 格式的配置信息"
class="json-editor"
/>
</div>
</template>
<style scoped>
.json-viewer-container {
max-height: 600px;
padding: 12px;
overflow-y: auto;
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 12px;
max-height: 600px;
overflow-y: auto;
}
.json-code {
margin: 0;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
font-size: 13px;
line-height: 1.5;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
white-space: pre-wrap;
}
.json-editor {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-family: Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace;
font-size: 13px;
}
</style>

View File

@@ -1,12 +1,27 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import {
computed,
onBeforeUnmount,
onMounted,
reactive,
ref,
watch,
} from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { formatDate } from '@vben/utils';
import { Button, Form, Pagination, Select, Switch, Table, Tag } from 'ant-design-vue';
import {
Button,
Form,
Pagination,
Select,
Switch,
Table,
Tag,
} from 'ant-design-vue';
import { getDeviceMessagePage } from '#/api/iot/device/device';
import { DictTag } from '#/components/dict-tag';

View File

@@ -9,6 +9,7 @@ import type { ThingModelData } from '#/api/iot/thingmodel';
import { computed, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import {
Button,
@@ -19,16 +20,14 @@ import {
Tabs,
Textarea,
} from 'ant-design-vue';
import { DownOutlined, UpOutlined } from '@ant-design/icons-vue';
import { DeviceStateEnum, sendDeviceMessage } from '#/api/iot/device/device';
import DataDefinition from '#/views/iot/thingmodel/modules/components/DataDefinition.vue';
import {
IotDeviceMessageMethodEnum,
IoTThingModelTypeEnum,
} from '#/views/iot/utils/constants';
import DataDefinition from '#/views/iot/thingmodel/modules/components/DataDefinition.vue';
import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
const props = defineProps<{
@@ -340,7 +339,7 @@ async function handleServiceInvoke(row: ThingModelData) {
<template>
<ContentWrap>
<!-- 上方指令调试区域 -->
<Card class="mb-4 simulator-tabs">
<Card class="simulator-tabs mb-4">
<template #title>
<div class="flex items-center justify-between">
<span>指令调试</span>
@@ -349,233 +348,233 @@ async function handleServiceInvoke(row: ThingModelData) {
size="small"
@click="debugCollapsed = !debugCollapsed"
>
<UpOutlined v-if="!debugCollapsed" />
<DownOutlined v-if="debugCollapsed" />
<IconifyIcon icon="lucide:chevron-up" v-if="!debugCollapsed" />
<IconifyIcon icon="lucide:chevron-down" v-if="debugCollapsed" />
</Button>
</div>
</template>
<div v-show="!debugCollapsed">
<Tabs v-model:active-key="activeTab" size="small">
<!-- 上行指令调试 -->
<Tabs.TabPane key="upstream" tab="上行指令调试">
<Tabs
v-if="activeTab === 'upstream'"
v-model:active-key="upstreamTab"
size="small"
<!-- 上行指令调试 -->
<Tabs.TabPane key="upstream" tab="上行指令调试">
<Tabs
v-if="activeTab === 'upstream'"
v-model:active-key="upstreamTab"
size="small"
>
<!-- 属性上报 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
tab="属性上报"
>
<!-- 属性上报 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
tab="属性上报"
>
<ContentWrap>
<Table
:data-source="propertyList"
align="center"
:columns="propertyColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<Input
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
placeholder="输入值"
size="small"
/>
</template>
<ContentWrap>
<Table
:data-source="propertyList"
align="center"
:columns="propertyColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
</template>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性上报按钮
</span>
<Button type="primary" @click="handlePropertyPost">
发送属性上报
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
<!-- 事件上报 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
tab="事件上报"
>
<ContentWrap>
<Table
:data-source="eventList"
align="center"
:columns="eventColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.event?.dataType ?? '-' }}
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<Textarea
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
:rows="3"
placeholder="输入事件参数JSON格式"
size="small"
/>
</template>
<template v-else-if="column.key === 'action'">
<Button
type="primary"
size="small"
@click="handleEventPost(record)"
>
上报事件
</Button>
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
</Table>
</ContentWrap>
</Tabs.TabPane>
<template v-else-if="column.key === 'value'">
<Input
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
placeholder="输入值"
size="small"
/>
</template>
</template>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性上报按钮
</span>
<Button type="primary" @click="handlePropertyPost">
发送属性上报
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
<!-- 状态变更 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
tab="状态变更"
>
<ContentWrap>
<div class="flex gap-4">
<Button
type="primary"
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
>
设备上线
</Button>
<Button
danger
@click="handleDeviceState(DeviceStateEnum.OFFLINE)"
>
设备下线
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
</Tabs>
</Tabs.TabPane>
<!-- 下行指令调试 -->
<Tabs.TabPane key="downstream" tab="下行指令调试">
<Tabs
v-if="activeTab === 'downstream'"
v-model:active-key="downstreamTab"
size="small"
<!-- 事件上报 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
tab="事件上报"
>
<!-- 属性调试 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
tab="属性设置"
>
<ContentWrap>
<Table
:data-source="propertyList"
align="center"
:columns="propertyColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
</template>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<Input
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
placeholder="输入值"
size="small"
/>
</template>
<ContentWrap>
<Table
:data-source="eventList"
align="center"
:columns="eventColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.event?.dataType ?? '-' }}
</template>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性设置按钮
</span>
<Button type="primary" @click="handlePropertySet">
发送属性设置
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<Textarea
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
:rows="3"
placeholder="输入事件参数JSON格式"
size="small"
/>
</template>
<template v-else-if="column.key === 'action'">
<Button
type="primary"
size="small"
@click="handleEventPost(record)"
>
上报事件
</Button>
</template>
</template>
</Table>
</ContentWrap>
</Tabs.TabPane>
<!-- 服务调用 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
tab="设备服务调用"
>
<ContentWrap>
<Table
:data-source="serviceList"
align="center"
:columns="serviceColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
<!-- 状态变更 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
tab="状态变更"
>
<ContentWrap>
<div class="flex gap-4">
<Button
type="primary"
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<Textarea
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
:rows="3"
placeholder="输入服务参数JSON格式"
size="small"
/>
</template>
<template v-else-if="column.key === 'action'">
<Button
type="primary"
size="small"
@click="handleServiceInvoke(record)"
>
服务调用
</Button>
</template>
设备上线
</Button>
<Button
danger
@click="handleDeviceState(DeviceStateEnum.OFFLINE)"
>
设备下线
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
</Tabs>
</Tabs.TabPane>
<!-- 下行指令调试 -->
<Tabs.TabPane key="downstream" tab="下行指令调试">
<Tabs
v-if="activeTab === 'downstream'"
v-model:active-key="downstreamTab"
size="small"
>
<!-- 属性调试 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
tab="属性设置"
>
<ContentWrap>
<Table
:data-source="propertyList"
align="center"
:columns="propertyColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataType'">
{{ record.property?.dataType ?? '-' }}
</template>
</Table>
</ContentWrap>
</Tabs.TabPane>
</Tabs>
</Tabs.TabPane>
</Tabs>
</div>
</Card>
<template v-else-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<Input
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
placeholder="输入值"
size="small"
/>
</template>
</template>
</Table>
<div class="mt-4 flex items-center justify-between">
<span class="text-sm text-gray-600">
设置属性值后点击发送属性设置按钮
</span>
<Button type="primary" @click="handlePropertySet">
发送属性设置
</Button>
</div>
</ContentWrap>
</Tabs.TabPane>
<!-- 服务调用 -->
<Tabs.TabPane
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
tab="设备服务调用"
>
<ContentWrap>
<Table
:data-source="serviceList"
align="center"
:columns="serviceColumns"
:pagination="false"
:scroll="{ y: 300 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dataDefinition'">
<DataDefinition :data="record" />
</template>
<template v-else-if="column.key === 'value'">
<Textarea
:value="getFormValue(record.identifier)"
@update:value="
setFormValue(record.identifier, $event)
"
:rows="3"
placeholder="输入服务参数JSON格式"
size="small"
/>
</template>
<template v-else-if="column.key === 'action'">
<Button
type="primary"
size="small"
@click="handleServiceInvoke(record)"
>
服务调用
</Button>
</template>
</template>
</Table>
</ContentWrap>
</Tabs.TabPane>
</Tabs>
</Tabs.TabPane>
</Tabs>
</div>
</Card>
<!-- 下方设备消息区域 -->
<Card>
@@ -587,8 +586,8 @@ async function handleServiceInvoke(row: ThingModelData) {
size="small"
@click="messageCollapsed = !messageCollapsed"
>
<UpOutlined v-if="!messageCollapsed" />
<DownOutlined v-if="messageCollapsed" />
<IconifyIcon icon="lucide:chevron-down" v-if="!messageCollapsed" />
<IconifyIcon icon="lucide:chevron-down" v-if="messageCollapsed" />
</Button>
</div>
</template>

View File

@@ -41,4 +41,3 @@ onMounted(() => {
<!-- TODO: 实现子设备列表展示和管理功能 -->
</Card>
</template>

View File

@@ -137,7 +137,9 @@ async function getList() {
try {
const data = await getHistoryDevicePropertyList(queryParams);
// 后端直接返回数组,不是 { list: [] } 格式
list.value = (Array.isArray(data) ? data : (data?.list || [])) as IotDeviceApi.DevicePropertyDetail[];
list.value = (
Array.isArray(data) ? data : data?.list || []
) as IotDeviceApi.DevicePropertyDetail[];
total.value = list.value.length;
// 如果是图表模式且不是复杂数据类型,渲染图表
@@ -176,29 +178,29 @@ function renderChart() {
}
renderEcharts({
title: {
text: '属性值趋势',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'normal',
title: {
text: '属性值趋势',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'normal',
},
},
},
grid: {
left: 60,
right: 60,
bottom: 100,
top: 80,
containLabel: true,
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
grid: {
left: 60,
right: 60,
bottom: 100,
top: 80,
containLabel: true,
},
formatter: (params: any) => {
const param = params[0];
return `
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
},
formatter: (params: any) => {
const param = params[0];
return `
<div style="padding: 8px;">
<div style="margin-bottom: 4px; font-weight: bold;">
${formatDate(new Date(param.value[0]), 'YYYY-MM-DD HH:mm:ss')}
@@ -209,76 +211,76 @@ function renderChart() {
</div>
</div>
`;
},
},
xAxis: {
type: 'time',
name: '时间',
nameTextStyle: {
padding: [10, 0, 0, 0],
},
axisLabel: {
formatter: (value: number) => {
return String(formatDate(new Date(value), 'MM-DD HH:mm') || '');
},
},
},
yAxis: {
type: 'value',
name: '属性值',
nameTextStyle: {
padding: [0, 0, 10, 0],
},
},
series: [
{
name: '属性值',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2,
color: '#1890FF',
xAxis: {
type: 'time',
name: '时间',
nameTextStyle: {
padding: [10, 0, 0, 0],
},
itemStyle: {
color: '#1890FF',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(24, 144, 255, 0.3)',
},
{
offset: 1,
color: 'rgba(24, 144, 255, 0.05)',
},
],
axisLabel: {
formatter: (value: number) => {
return String(formatDate(new Date(value), 'MM-DD HH:mm') || '');
},
},
data: chartData,
},
],
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
yAxis: {
type: 'value',
name: '属性值',
nameTextStyle: {
padding: [0, 0, 10, 0],
},
},
{
type: 'slider',
height: 30,
bottom: 20,
},
],
});
series: [
{
name: '属性值',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 2,
color: '#1890FF',
},
itemStyle: {
color: '#1890FF',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(24, 144, 255, 0.3)',
},
{
offset: 1,
color: 'rgba(24, 144, 255, 0.05)',
},
],
},
},
data: chartData,
},
],
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
{
type: 'slider',
height: 30,
bottom: 20,
},
],
});
}, 300); // 延迟300ms渲染确保 DOM 完全准备好
}

View File

@@ -34,11 +34,9 @@ export function useFormSchema(): VbenFormSchema[] {
label: '父级分组',
component: 'ApiTreeSelect',
componentProps: {
api: getSimpleDeviceGroupList,
fieldNames: {
label: 'name',
value: 'id',
},
api: () => getSimpleDeviceGroupList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择父级分组',
allowClear: true,
},