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

@@ -101,6 +101,7 @@ export interface Action {
identifier?: string;
value?: any;
alertConfigId?: number;
params?: string;
}
/** 查询场景联动规则分页 */

View File

@@ -63,7 +63,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '关联场景联动规则',
component: 'ApiSelect',
componentProps: {
api: getSimpleRuleSceneList,
api: () => getSimpleRuleSceneList(),
labelField: 'name',
valueField: 'id',
mode: 'multiple',
@@ -76,7 +76,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '接收的用户',
component: 'ApiSelect',
componentProps: {
api: getSimpleUserList,
api: () => getSimpleUserList(),
labelField: 'nickname',
valueField: 'id',
mode: 'multiple',

View File

@@ -17,7 +17,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '告警配置',
component: 'ApiSelect',
componentProps: {
api: getSimpleAlertConfigList,
api: () => getSimpleAlertConfigList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择告警配置',
@@ -40,7 +40,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',
@@ -53,7 +53,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '设备',
component: 'ApiSelect',
componentProps: {
api: getSimpleDeviceList,
api: () => getSimpleDeviceList(),
labelField: 'deviceName',
valueField: 'id',
placeholder: '请选择设备',

View File

@@ -101,7 +101,7 @@ async function handleProcess(row: AlertRecord) {
try {
await processAlertRecord(row.id as number, processRemark);
message.success('处理成功');
onRefresh();
handleRefresh();
} catch (error) {
console.error('处理失败:', error);
throw error;

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,
},

View File

@@ -29,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '所属产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',
@@ -85,7 +85,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',

View File

@@ -29,7 +29,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '所属产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',
@@ -86,7 +86,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',

View File

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

View File

@@ -93,7 +93,7 @@ export function useFormSchema(formApi?: any): VbenFormSchema[] {
label: '产品分类',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductCategoryList,
api: () => getSimpleProductCategoryList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品分类',
@@ -246,7 +246,7 @@ export function useBasicFormSchema(formApi?: any): VbenFormSchema[] {
label: '产品分类',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductCategoryList,
api: () => getSimpleProductCategoryList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品分类',

View File

@@ -11,16 +11,15 @@ import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, Card, Image, Input, message, Space } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import {
deleteProduct,
exportProduct,
getProductPage,
} from '#/api/iot/product/product';
import { getSimpleProductCategoryList } from '#/api/iot/product/category';
import { $t } from '#/locales';
import { useGridColumns, useImagePreview } from './data';
// @ts-ignore
import ProductCardView from './modules/ProductCardView.vue';
import ProductForm from './modules/ProductForm.vue';

View File

@@ -14,13 +14,18 @@ import {
} from '#/api/iot/product/product';
import { $t } from '#/locales';
import { generateProductKey, useBasicFormSchema, useAdvancedFormSchema } from '../data';
import {
generateProductKey,
useAdvancedFormSchema,
useBasicFormSchema,
} from '../data';
defineOptions({ name: 'IoTProductForm' });
const emit = defineEmits(['success']);
const CollapsePanel = Collapse.Panel;
const emit = defineEmits(['success']);
const formData = ref<any>();
const getTitle = computed(() => {
return formData.value?.id ? '编辑产品' : '新增产品';
@@ -84,7 +89,10 @@ const [Modal, modalApi] = useVbenModal({
};
}
const values = { ...basicValues, ...advancedValues } as IotProductApi.Product;
const values = {
...basicValues,
...advancedValues,
} as IotProductApi.Product;
const data = formData.value?.id
? { ...values, id: formData.value.id }
: values;
@@ -132,7 +140,11 @@ const [Modal, modalApi] = useVbenModal({
});
// 如果有图标、图片或描述,自动展开折叠面板以便显示
if (formData.value.icon || formData.value.picUrl || formData.value.description) {
if (
formData.value.icon ||
formData.value.picUrl ||
formData.value.description
) {
activeKey.value = ['advanced'];
}
} catch (error) {

View File

@@ -15,10 +15,10 @@ interface Props {
defineProps<Props>();
/** 格式化日期 */
const formatDate = (date?: Date | string) => {
function formatDate(date?: Date | string) {
if (!date) return '-';
return new Date(date).toLocaleString('zh-CN');
};
}
</script>
<template>

View File

@@ -24,7 +24,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '产品',
component: 'ApiSelect',
componentProps: {
api: getSimpleProductList,
api: () => getSimpleProductList(),
labelField: 'name',
valueField: 'id',
placeholder: '请选择产品',

View File

@@ -210,7 +210,8 @@ function handleOperatorChange() {
<!-- 设备状态条件配置 -->
<div
v-if="
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
condition.type ===
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS.toString()
"
class="gap-16px flex flex-col"
>
@@ -222,7 +223,7 @@ function handleOperatorChange() {
<Select
:model-value="condition.operator"
@update:model-value="
(value) => updateConditionField('operator', value)
(value: any) => updateConditionField('operator', value)
"
placeholder="请选择操作符"
class="w-full"

View File

@@ -120,7 +120,7 @@ const timeValue2 = computed(() => {
* @param value 字段值
*/
function updateConditionField(field: any, value: any) {
condition.value[field] = value;
(condition.value as any)[field] = value;
}
/**

View File

@@ -15,7 +15,6 @@ export function useGridFormSchema(): VbenFormSchema[] {
options: getDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE, 'number'),
placeholder: '请选择功能类型',
allowClear: true,
},
},
];

View File

@@ -1,19 +1,21 @@
<script setup lang="ts">
import { onMounted, provide, ref } from 'vue';
import { message } from 'ant-design-vue';
import { Page } from '@vben/common-ui';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
import { getProduct } from '#/api/iot/product/product';
import type { IotProductApi } from '#/api/iot/product/product';
import { useGridColumns, useGridFormSchema } from './data';
import { onMounted, provide, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProduct } from '#/api/iot/product/product';
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '../utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import { DataDefinition } from './modules/components';
import ThingModelForm from './modules/ThingModelForm.vue';
import ThingModelTSL from './modules/ThingModelTSL.vue';
import { DataDefinition } from './modules/components';
defineOptions({ name: 'IoTThingModel' });
@@ -119,7 +121,7 @@ onMounted(async () => {
description="管理产品的物模型定义,包括属性、服务和事件"
title="物模型管理"
>
<Grid ref="xGrid">
<Grid>
<template #toolbar-tools>
<TableAction
:actions="[

View File

@@ -21,11 +21,7 @@ defineOptions({ name: 'ThingModelEvent' });
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const thingModelEvent = useVModel(
props,
'modelValue',
emits,
) as Ref<any>;
const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<any>;
// 默认选中INFO 信息
watch(

View File

@@ -64,7 +64,8 @@ const formRef = ref(); // 表单 Ref
async function open(type: string, id?: number) {
dialogVisible.value = true;
// 设置标题create -> 新增update -> 编辑
dialogTitle.value = type === 'create' ? $t('page.action.add') : $t('page.action.edit');
dialogTitle.value =
type === 'create' ? $t('page.action.add') : $t('page.action.edit');
formType.value = type;
resetForm();
if (id) {

View File

@@ -42,8 +42,13 @@ const getDataTypeOptions2 = computed(() => {
if (!props.isStructDataSpecs) {
return getDataTypeOptions();
}
const excludedTypes = [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY];
return getDataTypeOptions().filter((item: any) => !excludedTypes.includes(item.value));
const excludedTypes = new Set([
IoTDataSpecsDataTypeEnum.ARRAY,
IoTDataSpecsDataTypeEnum.STRUCT,
]);
return getDataTypeOptions().filter(
(item: any) => !excludedTypes.has(item.value),
);
}); // 获得数据类型列表
/** 属性值的数据类型切换时初始化相关数据 */
@@ -52,19 +57,11 @@ function handleChange(dataType: any) {
property.value.dataSpecsList = [];
// 不是列表型数据才设置 dataSpecs.dataType
![
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.BOOL,
IoTDataSpecsDataTypeEnum.ENUM,
IoTDataSpecsDataTypeEnum.STRUCT,
].includes(dataType) && (property.value.dataSpecs.dataType = dataType);
switch (dataType) {
case IoTDataSpecsDataTypeEnum.ENUM: {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', // 枚举项的名称
value: undefined, // 枚举值
});
break;
}
case IoTDataSpecsDataTypeEnum.BOOL: {
for (let i = 0; i < 2; i++) {
property.value.dataSpecsList.push({
@@ -75,6 +72,14 @@ function handleChange(dataType: any) {
}
break;
}
case IoTDataSpecsDataTypeEnum.ENUM: {
property.value.dataSpecsList.push({
dataType: IoTDataSpecsDataTypeEnum.ENUM,
name: '', // 枚举项的名称
value: undefined, // 枚举值
});
break;
}
}
// useVModel 会自动同步数据到父组件,不需要手动 emit
}
@@ -95,9 +100,7 @@ watch(
</script>
<template>
<Form.Item
label="数据类型"
>
<Form.Item label="数据类型">
<Select
v-model:value="property.dataType"
placeholder="请选择数据类型"
@@ -135,7 +138,7 @@ watch(
label="布尔值"
>
<template v-for="item in property.dataSpecsList" :key="item.value">
<div class="flex items-center justify-start w-1/1 mb-5px">
<div class="w-1/1 mb-5px flex items-center justify-start">
<span>{{ item.value }}</span>
<span class="mx-2">-</span>
<div class="flex-1">

View File

@@ -92,26 +92,26 @@ watch(tslString, (newValue) => {
<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

@@ -19,22 +19,30 @@ const props = defineProps<{ data: ThingModelData }>();
// 格式化布尔值和枚举值列表为字符串
const formattedDataSpecsList = computed(() => {
if (!props.data.property?.dataSpecsList || props.data.property.dataSpecsList.length === 0) {
if (
!props.data.property?.dataSpecsList ||
props.data.property.dataSpecsList.length === 0
) {
return '';
}
return props.data.property.dataSpecsList
.map(item => `${item.value}-${item.name}`)
.map((item) => `${item.value}-${item.name}`)
.join('、');
});
// 显示的简短文本(第一个值)
const shortText = computed(() => {
if (!props.data.property?.dataSpecsList || props.data.property.dataSpecsList.length === 0) {
if (
!props.data.property?.dataSpecsList ||
props.data.property.dataSpecsList.length === 0
) {
return '-';
}
const first = props.data.property.dataSpecsList[0];
const count = props.data.property.dataSpecsList.length;
return count > 1 ? `${first.value}-${first.name}${count}` : `${first.value}-${first.name}`;
return count > 1
? `${first.value}-${first.name}${count}`
: `${first.value}-${first.name}`;
});
</script>
@@ -108,8 +116,8 @@ const shortText = computed(() => {
border-bottom: 1px dashed #d9d9d9;
&:hover {
border-bottom-color: #1890ff;
color: #1890ff;
border-bottom-color: #1890ff;
}
}
</style>

View File

@@ -51,7 +51,10 @@ function handleChange(val: any) {
</Radio.Group>
</Form.Item>
<Form.Item label="元素个数" name="property.dataSpecs.size">
<Input v-model:value="dataSpecs.size" placeholder="请输入数组中的元素个数" />
<Input
v-model:value="dataSpecs.size"
placeholder="请输入数组中的元素个数"
/>
</Form.Item>
<!-- Struct 型配置-->
<ThingModelStructDataSpecs

View File

@@ -28,7 +28,6 @@ function deleteEnum(index: number) {
}
dataSpecsList.value.splice(index, 1);
}
</script>
<template>
@@ -41,7 +40,7 @@ function deleteEnum(index: number) {
<div
v-for="(item, index) in dataSpecsList"
:key="index"
class="flex items-center justify-between mb-5px"
class="mb-5px flex items-center justify-between"
>
<div class="flex-1">
<Input v-model:value="item.value" placeholder="请输入枚举值,如'0'" />

View File

@@ -1,4 +1,35 @@
<!-- dataTypenumber 数组类型 -->
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { DataSpecsNumberData } from '#/api/iot/thingmodel';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'ant-design-vue';
/** 数值型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelNumberDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(
props,
'modelValue',
emits,
) as Ref<DataSpecsNumberData>;
/** 单位发生变化时触发 */
const unitChange = (UnitSpecs: any) => {
if (!UnitSpecs) return;
const [unitName, unit] = String(UnitSpecs).split('-');
dataSpecs.value.unitName = unitName;
dataSpecs.value.unit = unit;
};
</script>
<template>
<Form.Item label="取值范围">
<div class="flex items-center justify-between">
@@ -38,38 +69,6 @@
</Form.Item>
</template>
<script lang="ts" setup>
import type { Ref } from 'vue';
import type { DataSpecsNumberData } from '#/api/iot/thingmodel';
import { useVModel } from '@vueuse/core';
import { Form, Input, Select } from 'ant-design-vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
/** 数值型的 dataSpecs 配置组件 */
defineOptions({ name: 'ThingModelNumberDataSpecs' });
const props = defineProps<{ modelValue: any }>();
const emits = defineEmits(['update:modelValue']);
const dataSpecs = useVModel(
props,
'modelValue',
emits,
) as Ref<DataSpecsNumberData>;
/** 单位发生变化时触发 */
const unitChange = (UnitSpecs: any) => {
if (!UnitSpecs) return;
const [unitName, unit] = String(UnitSpecs).split('-');
dataSpecs.value.unitName = unitName;
dataSpecs.value.unit = unit;
};
</script>
<style lang="scss" scoped>
:deep(.ant-form-item) {
.ant-form-item {

View File

@@ -115,7 +115,6 @@ function resetForm() {
structFormRef.value?.resetFields();
}
/** 组件初始化 */
onMounted(async () => {
await nextTick();
@@ -134,18 +133,14 @@ onMounted(async () => {
>
<span>参数{{ item.name }}</span>
<div class="btn">
<Button type="link" @click="openStructForm(item)">
编辑
</Button>
<Button type="link" @click="openStructForm(item)"> 编辑 </Button>
<Divider type="vertical" />
<Button type="link" danger @click="deleteStructItem(index)">
删除
</Button>
</div>
</div>
<Button type="link" @click="openStructForm(null)">
+新增参数
</Button>
<Button type="link" @click="openStructForm(null)"> +新增参数 </Button>
</Form.Item>
<!-- struct 表单 -->