fix:【iot 物联网】linter 报错
This commit is contained in:
@@ -51,7 +51,9 @@ export function getAlertConfig(id: number) {
|
|||||||
|
|
||||||
/** 查询所有告警配置列表 */
|
/** 查询所有告警配置列表 */
|
||||||
export function getAlertConfigList() {
|
export function getAlertConfigList() {
|
||||||
return requestClient.get<AlertConfigApi.AlertConfig[]>('/iot/alert-config/list');
|
return requestClient.get<AlertConfigApi.AlertConfig[]>(
|
||||||
|
'/iot/alert-config/list',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增告警配置 */
|
/** 新增告警配置 */
|
||||||
@@ -92,4 +94,3 @@ export function getSimpleAlertConfigList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { AlertConfigApi };
|
export { AlertConfigApi };
|
||||||
|
|
||||||
|
|||||||
@@ -82,4 +82,3 @@ export function deleteAlertRecordList(ids: number[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { AlertRecordApi };
|
export { AlertRecordApi };
|
||||||
|
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ export namespace IotDeviceApi {
|
|||||||
/** IoT 设备状态枚举 */
|
/** IoT 设备状态枚举 */
|
||||||
export enum DeviceStateEnum {
|
export enum DeviceStateEnum {
|
||||||
INACTIVE = 0, // 未激活
|
INACTIVE = 0, // 未激活
|
||||||
ONLINE = 1, // 在线
|
|
||||||
OFFLINE = 2, // 离线
|
OFFLINE = 2, // 离线
|
||||||
|
ONLINE = 1, // 在线
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询设备分页 */
|
/** 查询设备分页 */
|
||||||
@@ -221,4 +221,3 @@ export type DeviceVO = IotDeviceApi.Device;
|
|||||||
export type IotDeviceAuthInfoVO = IotDeviceApi.DeviceAuthInfo;
|
export type IotDeviceAuthInfoVO = IotDeviceApi.DeviceAuthInfo;
|
||||||
export type IotDevicePropertyDetailRespVO = IotDeviceApi.DevicePropertyDetail;
|
export type IotDevicePropertyDetailRespVO = IotDeviceApi.DevicePropertyDetail;
|
||||||
export type IotDevicePropertyRespVO = IotDeviceApi.DeviceProperty;
|
export type IotDevicePropertyRespVO = IotDeviceApi.DeviceProperty;
|
||||||
|
|
||||||
|
|||||||
@@ -49,4 +49,3 @@ export function getSimpleDeviceGroupList() {
|
|||||||
'/iot/device-group/simple-list',
|
'/iot/device-group/simple-list',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,4 +98,3 @@ export function resumeOtaTask(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { IoTOtaTaskApi };
|
export { IoTOtaTaskApi };
|
||||||
|
|
||||||
|
|||||||
@@ -101,4 +101,3 @@ export function getOtaTaskRecordStatusStatistics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { IoTOtaTaskRecordApi };
|
export { IoTOtaTaskRecordApi };
|
||||||
|
|
||||||
|
|||||||
@@ -55,4 +55,3 @@ export function getSimpleProductCategoryList() {
|
|||||||
'/iot/product-category/simple-list',
|
'/iot/product-category/simple-list',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,15 +30,15 @@ export namespace IotProductApi {
|
|||||||
/** IOT 产品设备类型枚举类 */
|
/** IOT 产品设备类型枚举类 */
|
||||||
export enum DeviceTypeEnum {
|
export enum DeviceTypeEnum {
|
||||||
DEVICE = 0, // 直连设备
|
DEVICE = 0, // 直连设备
|
||||||
GATEWAY_SUB = 1, // 网关子设备
|
|
||||||
GATEWAY = 2, // 网关设备
|
GATEWAY = 2, // 网关设备
|
||||||
|
GATEWAY_SUB = 1, // 网关子设备
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IOT 产品定位类型枚举类 */
|
/** IOT 产品定位类型枚举类 */
|
||||||
export enum LocationTypeEnum {
|
export enum LocationTypeEnum {
|
||||||
IP = 1, // IP 定位
|
IP = 1, // IP 定位
|
||||||
MODULE = 2, // 设备定位
|
|
||||||
MANUAL = 3, // 手动定位
|
MANUAL = 3, // 手动定位
|
||||||
|
MODULE = 2, // 设备定位
|
||||||
}
|
}
|
||||||
|
|
||||||
/** IOT 数据格式(编解码器类型)枚举类 */
|
/** IOT 数据格式(编解码器类型)枚举类 */
|
||||||
|
|||||||
@@ -81,4 +81,3 @@ export function updateDataRuleStatus(id: number, status: number) {
|
|||||||
status,
|
status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export interface DataSinkVO {
|
|||||||
/** IoT 数据目的类型枚举 */
|
/** IoT 数据目的类型枚举 */
|
||||||
export enum IotDataSinkTypeEnum {
|
export enum IotDataSinkTypeEnum {
|
||||||
HTTP = 'HTTP',
|
HTTP = 'HTTP',
|
||||||
MQTT = 'MQTT',
|
|
||||||
KAFKA = 'KAFKA',
|
KAFKA = 'KAFKA',
|
||||||
|
MQTT = 'MQTT',
|
||||||
RABBITMQ = 'RABBITMQ',
|
RABBITMQ = 'RABBITMQ',
|
||||||
ROCKETMQ = 'ROCKETMQ',
|
|
||||||
REDIS_STREAM = 'REDIS_STREAM',
|
REDIS_STREAM = 'REDIS_STREAM',
|
||||||
|
ROCKETMQ = 'ROCKETMQ',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** HTTP 配置 */
|
/** HTTP 配置 */
|
||||||
@@ -148,4 +148,3 @@ export function updateDataSinkStatus(id: number, status: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { DataSinkApi };
|
export { DataSinkApi };
|
||||||
|
|
||||||
|
|||||||
@@ -156,8 +156,7 @@ export function getSimpleRuleSceneList() {
|
|||||||
|
|
||||||
// 别名导出(兼容旧代码)
|
// 别名导出(兼容旧代码)
|
||||||
export {
|
export {
|
||||||
getSceneRulePage as getRuleScenePage,
|
|
||||||
deleteSceneRule as deleteRuleScene,
|
deleteSceneRule as deleteRuleScene,
|
||||||
|
getSceneRulePage as getRuleScenePage,
|
||||||
updateSceneRuleStatus as updateRuleSceneStatus,
|
updateSceneRuleStatus as updateRuleSceneStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ export interface ThingModelFormRules {
|
|||||||
|
|
||||||
/** 验证布尔型名称 */
|
/** 验证布尔型名称 */
|
||||||
export const validateBoolName = (_rule: any, value: any, callback: any) => {
|
export const validateBoolName = (_rule: any, value: any, callback: any) => {
|
||||||
if (!value) {
|
if (value) {
|
||||||
callback(new Error('枚举描述不能为空'));
|
|
||||||
} else {
|
|
||||||
callback();
|
callback();
|
||||||
|
} else {
|
||||||
|
callback(new Error('枚举描述不能为空'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
|||||||
import { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config';
|
import { deleteAlertConfig, getAlertConfigPage } from '#/api/iot/alert/config';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
|
||||||
import AlertConfigForm from '../modules/AlertConfigForm.vue';
|
import AlertConfigForm from '../modules/AlertConfigForm.vue';
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTAlertConfig' });
|
defineOptions({ name: 'IoTAlertConfig' });
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
// 提交表单
|
// 提交表单
|
||||||
const data = (await formApi.getValues()) as AlertConfigApi.AlertConfig;
|
const data = (await formApi.getValues()) as AlertConfigApi.AlertConfig;
|
||||||
try {
|
try {
|
||||||
await (formData.value?.id ? updateAlertConfig(data) : createAlertConfig(data));
|
await (formData.value?.id
|
||||||
|
? updateAlertConfig(data)
|
||||||
|
: createAlertConfig(data));
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { AlertRecord } from '#/api/iot/alert/record';
|
||||||
|
|
||||||
import { h, onMounted, ref } from 'vue';
|
import { h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Modal, message } from 'ant-design-vue';
|
import { message, Modal } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import type { AlertRecord } from '#/api/iot/alert/record';
|
|
||||||
import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
|
import { getAlertRecordPage, processAlertRecord } from '#/api/iot/alert/record';
|
||||||
import { getSimpleDeviceList } from '#/api/iot/device/device';
|
import { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
@@ -83,12 +83,14 @@ const handleProcess = async (row: AlertRecord) => {
|
|||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
async onOk() {
|
async onOk() {
|
||||||
const textarea = document.getElementById('processRemark') as HTMLTextAreaElement;
|
const textarea = document.querySelector(
|
||||||
|
'#processRemark',
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
const processRemark = textarea?.value || '';
|
const processRemark = textarea?.value || '';
|
||||||
|
|
||||||
if (!processRemark) {
|
if (!processRemark) {
|
||||||
message.warning('请输入处理原因');
|
message.warning('请输入处理原因');
|
||||||
return Promise.reject();
|
throw undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideLoading = message.loading({
|
const hideLoading = message.loading({
|
||||||
@@ -125,7 +127,11 @@ const handleView = (row: AlertRecord) => {
|
|||||||
]),
|
]),
|
||||||
h('div', [
|
h('div', [
|
||||||
h('span', { class: 'font-semibold' }, '设备消息:'),
|
h('span', { class: 'font-semibold' }, '设备消息:'),
|
||||||
h('pre', { class: 'mt-1 text-xs bg-gray-50 p-2 rounded' }, row.deviceMessage || '-'),
|
h(
|
||||||
|
'pre',
|
||||||
|
{ class: 'mt-1 text-xs bg-gray-50 p-2 rounded' },
|
||||||
|
row.deviceMessage || '-',
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
h('div', [
|
h('div', [
|
||||||
h('span', { class: 'font-semibold' }, '处理结果:'),
|
h('span', { class: 'font-semibold' }, '处理结果:'),
|
||||||
@@ -133,7 +139,12 @@ const handleView = (row: AlertRecord) => {
|
|||||||
]),
|
]),
|
||||||
h('div', [
|
h('div', [
|
||||||
h('span', { class: 'font-semibold' }, '处理时间:'),
|
h('span', { class: 'font-semibold' }, '处理时间:'),
|
||||||
h('span', row.processTime ? new Date(row.processTime).toLocaleString('zh-CN') : '-'),
|
h(
|
||||||
|
'span',
|
||||||
|
row.processTime
|
||||||
|
? new Date(row.processTime).toLocaleString('zh-CN')
|
||||||
|
: '-',
|
||||||
|
),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
@@ -200,7 +211,7 @@ onMounted(() => {
|
|||||||
v-if="row.deviceMessage"
|
v-if="row.deviceMessage"
|
||||||
placement="topLeft"
|
placement="topLeft"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
:overlayStyle="{ maxWidth: '600px' }"
|
:overlay-style="{ maxWidth: '600px' }"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<pre class="text-xs">{{ row.deviceMessage }}</pre>
|
<pre class="text-xs">{{ row.deviceMessage }}</pre>
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { getDictOptions } from '@vben/hooks';
|
|||||||
|
|
||||||
import { z } from '#/adapter/form';
|
import { z } from '#/adapter/form';
|
||||||
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
||||||
import { DeviceTypeEnum, getSimpleProductList } from '#/api/iot/product/product';
|
import {
|
||||||
|
DeviceTypeEnum,
|
||||||
|
getSimpleProductList,
|
||||||
|
} from '#/api/iot/product/product';
|
||||||
|
|
||||||
/** 新增/修改的表单 */
|
/** 新增/修改的表单 */
|
||||||
export function useFormSchema(): VbenFormSchema[] {
|
export function useFormSchema(): VbenFormSchema[] {
|
||||||
@@ -43,7 +46,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
.min(4, 'DeviceName 长度不能少于 4 个字符')
|
.min(4, 'DeviceName 长度不能少于 4 个字符')
|
||||||
.max(32, 'DeviceName 长度不能超过 32 个字符')
|
.max(32, 'DeviceName 长度不能超过 32 个字符')
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-zA-Z0-9_.\-:@]{4,32}$/,
|
/^[\w.\-:@]{4,32}$/,
|
||||||
'支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@',
|
'支持英文字母、数字、下划线(_)、中划线(-)、点号(.)、半角冒号(:)和特殊字符@',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -79,7 +82,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
.min(4, '备注名称长度限制为 4~64 个字符')
|
.min(4, '备注名称长度限制为 4~64 个字符')
|
||||||
.max(64, '备注名称长度限制为 4~64 个字符')
|
.max(64, '备注名称长度限制为 4~64 个字符')
|
||||||
.regex(
|
.regex(
|
||||||
/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/,
|
/^[\u4E00-\u9FA5\u3040-\u30FF\w]+$/,
|
||||||
'备注名称只能包含中文、英文字母、日文、数字和下划线(_)',
|
'备注名称只能包含中文、英文字母、日文、数字和下划线(_)',
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
@@ -106,7 +109,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
rules: z
|
rules: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[a-zA-Z0-9-_]+$/, '序列号只能包含字母、数字、中划线和下划线')
|
.regex(/^[\w-]+$/, '序列号只能包含字母、数字、中划线和下划线')
|
||||||
.optional()
|
.optional()
|
||||||
.or(z.literal('')),
|
.or(z.literal('')),
|
||||||
},
|
},
|
||||||
@@ -318,4 +321,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,30 +4,39 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Button, Card, Input, message, Select, Space, Tag } from 'ant-design-vue';
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Tag,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
deleteDevice,
|
deleteDevice,
|
||||||
deleteDeviceList,
|
deleteDeviceList,
|
||||||
exportDeviceExcel,
|
exportDeviceExcel,
|
||||||
getDevicePage
|
getDevicePage,
|
||||||
} from '#/api/iot/device/device';
|
} from '#/api/iot/device/device';
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
|
||||||
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
||||||
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns } from './data';
|
||||||
|
// @ts-ignore
|
||||||
|
import DeviceCardView from './modules/DeviceCardView.vue';
|
||||||
import DeviceForm from './modules/DeviceForm.vue';
|
import DeviceForm from './modules/DeviceForm.vue';
|
||||||
import DeviceGroupForm from './modules/DeviceGroupForm.vue';
|
import DeviceGroupForm from './modules/DeviceGroupForm.vue';
|
||||||
import DeviceImportForm from './modules/DeviceImportForm.vue';
|
import DeviceImportForm from './modules/DeviceImportForm.vue';
|
||||||
// @ts-ignore
|
|
||||||
import DeviceCardView from './modules/DeviceCardView.vue';
|
|
||||||
import { useGridColumns } from './data';
|
|
||||||
|
|
||||||
/** IoT 设备列表 */
|
/** IoT 设备列表 */
|
||||||
defineOptions({ name: 'IoTDevice' });
|
defineOptions({ name: 'IoTDevice' });
|
||||||
@@ -36,7 +45,7 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const products = ref<any[]>([]);
|
const products = ref<any[]>([]);
|
||||||
const deviceGroups = ref<any[]>([]);
|
const deviceGroups = ref<any[]>([]);
|
||||||
const viewMode = ref<'list' | 'card'>('card');
|
const viewMode = ref<'card' | 'list'>('card');
|
||||||
const cardViewRef = ref();
|
const cardViewRef = ref();
|
||||||
|
|
||||||
// Modal instances
|
// Modal instances
|
||||||
@@ -120,7 +129,11 @@ function openProductDetail(productId: number) {
|
|||||||
|
|
||||||
/** 打开物模型数据 */
|
/** 打开物模型数据 */
|
||||||
function openModel(id: number) {
|
function openModel(id: number) {
|
||||||
router.push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'model' } });
|
router.push({
|
||||||
|
name: 'IoTDeviceDetail',
|
||||||
|
params: { id },
|
||||||
|
query: { tab: 'model' },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 新增设备 */
|
/** 新增设备 */
|
||||||
@@ -219,7 +232,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
} as VxeTableGridOptions,
|
} as VxeTableGridOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 初始化 **/
|
/** 初始化 */
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 获取产品列表
|
// 获取产品列表
|
||||||
products.value = await getSimpleProductList();
|
products.value = await getSimpleProductList();
|
||||||
@@ -245,7 +258,7 @@ onMounted(async () => {
|
|||||||
<!-- 统一搜索工具栏 -->
|
<!-- 统一搜索工具栏 -->
|
||||||
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="flex flex-wrap items-center gap-3 mb-3">
|
<div class="mb-3 flex flex-wrap items-center gap-3">
|
||||||
<Select
|
<Select
|
||||||
v-model:value="searchParams.productId"
|
v-model:value="searchParams.productId"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
@@ -265,14 +278,14 @@ onMounted(async () => {
|
|||||||
placeholder="请输入 DeviceName"
|
placeholder="请输入 DeviceName"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@pressEnter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="searchParams.nickname"
|
v-model:value="searchParams.nickname"
|
||||||
placeholder="请输入备注名称"
|
placeholder="请输入备注名称"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@pressEnter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
v-model:value="searchParams.deviceType"
|
v-model:value="searchParams.deviceType"
|
||||||
@@ -329,11 +342,19 @@ onMounted(async () => {
|
|||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<Space :size="12">
|
<Space :size="12">
|
||||||
<Button type="primary" @click="handleCreate" v-hasPermi="['iot:device:create']">
|
<Button
|
||||||
|
type="primary"
|
||||||
|
@click="handleCreate"
|
||||||
|
v-hasPermi="['iot:device:create']"
|
||||||
|
>
|
||||||
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
||||||
新增
|
新增
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="primary" @click="handleExport" v-hasPermi="['iot:device:export']">
|
<Button
|
||||||
|
type="primary"
|
||||||
|
@click="handleExport"
|
||||||
|
v-hasPermi="['iot:device:export']"
|
||||||
|
>
|
||||||
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
|
<IconifyIcon icon="ant-design:download-outlined" class="mr-1" />
|
||||||
导出
|
导出
|
||||||
</Button>
|
</Button>
|
||||||
@@ -385,7 +406,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<!-- 所属产品列 -->
|
<!-- 所属产品列 -->
|
||||||
<template #product="{ row }">
|
<template #product="{ row }">
|
||||||
<a class="cursor-pointer text-primary" @click="openProductDetail(row.productId)">
|
<a
|
||||||
|
class="text-primary cursor-pointer"
|
||||||
|
@click="openProductDetail(row.productId)"
|
||||||
|
>
|
||||||
{{ products.find((p: any) => p.id === row.productId)?.name || '-' }}
|
{{ products.find((p: any) => p.id === row.productId)?.name || '-' }}
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -11,38 +15,35 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Tag,
|
Tag,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
|
||||||
import { getDictLabel } from '@vben/hooks';
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { DeviceStateEnum, getDevicePage } from '#/api/iot/device/device';
|
import { DeviceStateEnum, getDevicePage } from '#/api/iot/device/device';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceCardView' });
|
defineOptions({ name: 'DeviceCardView' });
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
create: [];
|
||||||
|
delete: [row: any];
|
||||||
|
detail: [id: number];
|
||||||
|
edit: [row: any];
|
||||||
|
model: [id: number];
|
||||||
|
productDetail: [productId: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
products: any[];
|
products: any[];
|
||||||
deviceGroups: any[];
|
deviceGroups: any[];
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
|
deviceType?: number;
|
||||||
|
groupId?: number;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
deviceType?: number;
|
|
||||||
status?: number;
|
status?: number;
|
||||||
groupId?: number;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
create: [];
|
|
||||||
edit: [row: any];
|
|
||||||
delete: [row: any];
|
|
||||||
detail: [id: number];
|
|
||||||
model: [id: number];
|
|
||||||
productDetail: [productId: number];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const list = ref<any[]>([]);
|
const list = ref<any[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -169,15 +170,28 @@ defineExpose({
|
|||||||
<span class="label">所属产品</span>
|
<span class="label">所属产品</span>
|
||||||
<a
|
<a
|
||||||
class="value link"
|
class="value link"
|
||||||
@click="(e: MouseEvent) => { e.stopPropagation(); emit('productDetail', item.productId); }"
|
@click="
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
emit('productDetail', item.productId);
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ getProductName(item.productId) }}
|
{{ getProductName(item.productId) }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">设备类型</span>
|
<span class="label">设备类型</span>
|
||||||
<Tag :color="getDeviceTypeColor(item.deviceType)" size="small">
|
<Tag
|
||||||
{{ getDictLabel(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, item.deviceType) }}
|
:color="getDeviceTypeColor(item.deviceType)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
getDictLabel(
|
||||||
|
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
|
||||||
|
item.deviceType,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@@ -194,7 +208,12 @@ defineExpose({
|
|||||||
type="default"
|
type="default"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn btn-edit"
|
class="action-btn btn-edit"
|
||||||
@click="(e: MouseEvent) => { e.stopPropagation(); emit('edit', item); }"
|
@click="
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
emit('edit', item);
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="ph:note-pencil" />
|
<IconifyIcon icon="ph:note-pencil" />
|
||||||
编辑
|
编辑
|
||||||
@@ -203,7 +222,12 @@ defineExpose({
|
|||||||
type="default"
|
type="default"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn btn-view"
|
class="action-btn btn-view"
|
||||||
@click="(e: MouseEvent) => { e.stopPropagation(); emit('detail', item.id); }"
|
@click="
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
emit('detail', item.id);
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="ph:eye" />
|
<IconifyIcon icon="ph:eye" />
|
||||||
详情
|
详情
|
||||||
@@ -212,7 +236,12 @@ defineExpose({
|
|||||||
type="default"
|
type="default"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn btn-data"
|
class="action-btn btn-data"
|
||||||
@click="(e: MouseEvent) => { e.stopPropagation(); emit('model', item.id); }"
|
@click="
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
emit('model', item.id);
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="ph:database" />
|
<IconifyIcon icon="ph:database" />
|
||||||
数据
|
数据
|
||||||
@@ -262,13 +291,19 @@ defineExpose({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
box-shadow:
|
||||||
|
0 1px 2px 0 rgba(0, 0, 0, 0.03),
|
||||||
|
0 1px 6px -1px rgba(0, 0, 0, 0.02),
|
||||||
|
0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 1px 2px -2px rgba(0, 0, 0, 0.16), 0 3px 6px 0 rgba(0, 0, 0, 0.12), 0 5px 12px 4px rgba(0, 0, 0, 0.09);
|
box-shadow:
|
||||||
|
0 1px 2px -2px rgba(0, 0, 0, 0.16),
|
||||||
|
0 3px 6px 0 rgba(0, 0, 0, 0.12),
|
||||||
|
0 5px 12px 4px rgba(0, 0, 0, 0.09);
|
||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
border-color: #e6e6e6;
|
border-color: #e6e6e6;
|
||||||
}
|
}
|
||||||
@@ -379,7 +414,9 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.code {
|
&.code {
|
||||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas', monospace;
|
font-family:
|
||||||
|
'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Consolas',
|
||||||
|
monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #595959;
|
color: #595959;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import {
|
import { message } from 'ant-design-vue';
|
||||||
createDevice,
|
|
||||||
getDevice,
|
import { createDevice, getDevice, updateDevice } from '#/api/iot/device/device';
|
||||||
updateDevice,
|
|
||||||
type IotDeviceApi
|
|
||||||
} from '#/api/iot/device/device';
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { updateDeviceGroup } from '#/api/iot/device/device';
|
import { updateDeviceGroup } from '#/api/iot/device/device';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { downloadFileFromBlobPart } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { importDeviceTemplate } from '#/api/iot/device/device';
|
import { importDeviceTemplate } from '#/api/iot/device/device';
|
||||||
import { downloadFileFromBlobPart } from '@vben/utils';
|
|
||||||
|
|
||||||
import { useImportFormSchema } from '../data';
|
import { useImportFormSchema } from '../data';
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
const values = await formApi.getValues();
|
const values = await formApi.getValues();
|
||||||
const file = values.file;
|
const file = values.file;
|
||||||
|
|
||||||
if (!file || !file.length) {
|
if (!file || file.length === 0) {
|
||||||
message.error('请上传文件');
|
message.error('请上传文件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
},
|
},
|
||||||
body: formData,
|
body: formData,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -71,7 +72,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
const data = result.data;
|
const data = result.data;
|
||||||
let text = `上传成功数量:${data.createDeviceNames?.length || 0};`;
|
let text = `上传成功数量:${data.createDeviceNames?.length || 0};`;
|
||||||
if (data.createDeviceNames) {
|
if (data.createDeviceNames) {
|
||||||
for (let deviceName of data.createDeviceNames) {
|
for (const deviceName of data.createDeviceNames) {
|
||||||
text += `< ${deviceName} >`;
|
text += `< ${deviceName} >`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,217 @@
|
|||||||
<!-- IoT 设备选择,使用弹窗展示 -->
|
<!-- IoT 设备选择,使用弹窗展示 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
import type { IotDeviceGroupApi } from '#/api/iot/device/group';
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getDevicePage } from '#/api/iot/device/device';
|
||||||
|
import { getSimpleDeviceGroupList } from '#/api/iot/device/group';
|
||||||
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
defineOptions({ name: 'IoTDeviceTableSelect' });
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
productId: {
|
||||||
|
type: Number,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
// 获取字典选项
|
||||||
|
const getIntDictOptions = (dictType: string) => {
|
||||||
|
return getDictOptions(dictType, 'number');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 日期格式化
|
||||||
|
const dateFormatter = (_row: any, _column: any, cellValue: any) => {
|
||||||
|
return cellValue ? formatDate(cellValue, 'YYYY-MM-DD HH:mm:ss') : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const dialogTitle = ref('设备选择器');
|
||||||
|
const formLoading = ref(false);
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<IotDeviceApi.Device[]>([]); // 列表的数据
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const selectedDevices = ref<IotDeviceApi.Device[]>([]); // 选中的设备列表
|
||||||
|
const selectedId = ref<number>(); // 单选模式下选中的ID
|
||||||
|
const products = ref<IotProductApi.Product[]>([]); // 产品列表
|
||||||
|
const deviceGroups = ref<IotDeviceGroupApi.DeviceGroup[]>([]); // 设备分组列表
|
||||||
|
const selectedRowKeys = ref<number[]>([]); // 多选模式下选中的keys
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
deviceName: undefined as string | undefined,
|
||||||
|
productId: undefined as number | undefined,
|
||||||
|
deviceType: undefined as number | undefined,
|
||||||
|
nickname: undefined as string | undefined,
|
||||||
|
status: undefined as number | undefined,
|
||||||
|
groupId: undefined as number | undefined,
|
||||||
|
});
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = computed(() => {
|
||||||
|
const baseColumns = [
|
||||||
|
{
|
||||||
|
title: 'DeviceName',
|
||||||
|
dataIndex: 'deviceName',
|
||||||
|
key: 'deviceName',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '备注名称',
|
||||||
|
dataIndex: 'nickname',
|
||||||
|
key: 'nickname',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '所属产品',
|
||||||
|
key: 'productId',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '设备类型',
|
||||||
|
key: 'deviceType',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '所属分组',
|
||||||
|
key: 'groupIds',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '设备状态',
|
||||||
|
key: 'status',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后上线时间',
|
||||||
|
key: 'onlineTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 单选模式添加单选列
|
||||||
|
if (!props.multiple) {
|
||||||
|
baseColumns.unshift({
|
||||||
|
title: '',
|
||||||
|
key: 'radio',
|
||||||
|
width: 55,
|
||||||
|
align: 'center',
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseColumns;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 多选配置
|
||||||
|
const rowSelection = computed(() => ({
|
||||||
|
selectedRowKeys: selectedRowKeys.value,
|
||||||
|
onChange: (keys: number[], rows: IotDeviceApi.Device[]) => {
|
||||||
|
selectedRowKeys.value = keys;
|
||||||
|
selectedDevices.value = rows;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
if (props.productId) {
|
||||||
|
queryParams.productId = props.productId;
|
||||||
|
}
|
||||||
|
const data = await getDevicePage(queryParams);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value.resetFields();
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async () => {
|
||||||
|
dialogVisible.value = true;
|
||||||
|
// 重置选择状态
|
||||||
|
selectedDevices.value = [];
|
||||||
|
selectedId.value = undefined;
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
if (!props.productId) {
|
||||||
|
// 获取产品列表
|
||||||
|
products.value = await getSimpleProductList();
|
||||||
|
}
|
||||||
|
// 获取设备列表
|
||||||
|
await getList();
|
||||||
|
};
|
||||||
|
defineExpose({ open });
|
||||||
|
|
||||||
|
/** 处理行点击事件 */
|
||||||
|
const tableRef = ref();
|
||||||
|
const handleRowClick = (row: IotDeviceApi.Device) => {
|
||||||
|
if (!props.multiple) {
|
||||||
|
selectedId.value = row.id;
|
||||||
|
selectedDevices.value = [row];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理单选变更事件 */
|
||||||
|
const handleRadioChange = (row: IotDeviceApi.Device) => {
|
||||||
|
selectedId.value = row.id;
|
||||||
|
selectedDevices.value = [row];
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (selectedDevices.value.length === 0) {
|
||||||
|
message.warning({
|
||||||
|
content: props.multiple ? '请至少选择一个设备' : '请选择一个设备',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit(
|
||||||
|
'success',
|
||||||
|
props.multiple ? selectedDevices.value : selectedDevices.value[0],
|
||||||
|
);
|
||||||
|
dialogVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
// 获取产品列表
|
||||||
|
products.value = await getSimpleProductList();
|
||||||
|
// 获取分组列表
|
||||||
|
deviceGroups.value = await getSimpleDeviceGroupList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal
|
||||||
:title="dialogTitle"
|
:title="dialogTitle"
|
||||||
@@ -35,7 +248,7 @@
|
|||||||
v-model:value="queryParams.deviceName"
|
v-model:value="queryParams.deviceName"
|
||||||
placeholder="请输入 DeviceName"
|
placeholder="请输入 DeviceName"
|
||||||
allow-clear
|
allow-clear
|
||||||
@pressEnter="handleQuery"
|
@press-enter="handleQuery"
|
||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -44,7 +257,7 @@
|
|||||||
v-model:value="queryParams.nickname"
|
v-model:value="queryParams.nickname"
|
||||||
placeholder="请输入备注名称"
|
placeholder="请输入备注名称"
|
||||||
allow-clear
|
allow-clear
|
||||||
@pressEnter="handleQuery"
|
@press-enter="handleQuery"
|
||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -56,7 +269,9 @@
|
|||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
>
|
>
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
|
v-for="dict in getIntDictOptions(
|
||||||
|
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
|
||||||
|
)"
|
||||||
:key="dict.value"
|
:key="dict.value"
|
||||||
:value="dict.value"
|
:value="dict.value"
|
||||||
>
|
>
|
||||||
@@ -114,7 +329,7 @@
|
|||||||
<a-table
|
<a-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:dataSource="list"
|
:data-source="list"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
:row-selection="multiple ? rowSelection : undefined"
|
:row-selection="multiple ? rowSelection : undefined"
|
||||||
@@ -132,17 +347,28 @@
|
|||||||
{{ products.find((p) => p.id === record.productId)?.name || '-' }}
|
{{ products.find((p) => p.id === record.productId)?.name || '-' }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'deviceType'">
|
<template v-else-if="column.key === 'deviceType'">
|
||||||
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="record.deviceType" />
|
<dict-tag
|
||||||
|
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||||
|
:value="record.deviceType"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'groupIds'">
|
<template v-else-if="column.key === 'groupIds'">
|
||||||
<template v-if="record.groupIds?.length">
|
<template v-if="record.groupIds?.length">
|
||||||
<a-tag v-for="id in record.groupIds" :key="id" class="ml-5px" size="small">
|
<a-tag
|
||||||
|
v-for="id in record.groupIds"
|
||||||
|
:key="id"
|
||||||
|
class="ml-5px"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{{ deviceGroups.find((g) => g.id === id)?.name }}
|
{{ deviceGroups.find((g) => g.id === id)?.name }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'status'">
|
<template v-else-if="column.key === 'status'">
|
||||||
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="record.status" />
|
<dict-tag
|
||||||
|
:type="DICT_TYPE.IOT_DEVICE_STATUS"
|
||||||
|
:value="record.status"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'onlineTime'">
|
<template v-else-if="column.key === 'onlineTime'">
|
||||||
{{ dateFormatter(null, null, record.onlineTime) }}
|
{{ dateFormatter(null, null, record.onlineTime) }}
|
||||||
@@ -160,211 +386,10 @@
|
|||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-button @click="submitForm" type="primary" :disabled="formLoading">确 定</a-button>
|
<a-button @click="submitForm" type="primary" :disabled="formLoading">
|
||||||
|
确 定
|
||||||
|
</a-button>
|
||||||
<a-button @click="dialogVisible = false">取 消</a-button>
|
<a-button @click="dialogVisible = false">取 消</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, onMounted, computed } from 'vue'
|
|
||||||
import { DICT_TYPE } from '@vben/constants'
|
|
||||||
import { getDictOptions } from '@vben/hooks'
|
|
||||||
import { formatDate } from '@vben/utils'
|
|
||||||
import type { IotDeviceApi } from '#/api/iot/device/device'
|
|
||||||
import { getDevicePage } from '#/api/iot/device/device'
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product'
|
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product'
|
|
||||||
import type { IotDeviceGroupApi } from '#/api/iot/device/group'
|
|
||||||
import { getSimpleDeviceGroupList } from '#/api/iot/device/group'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceTableSelect' })
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
multiple: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
productId: {
|
|
||||||
type: Number,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取字典选项
|
|
||||||
const getIntDictOptions = (dictType: string) => {
|
|
||||||
return getDictOptions(dictType, 'number')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 日期格式化
|
|
||||||
const dateFormatter = (_row: any, _column: any, cellValue: any) => {
|
|
||||||
return cellValue ? formatDate(cellValue, 'YYYY-MM-DD HH:mm:ss') : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
|
||||||
const dialogTitle = ref('设备选择器')
|
|
||||||
const formLoading = ref(false)
|
|
||||||
const loading = ref(true) // 列表的加载中
|
|
||||||
const list = ref<IotDeviceApi.Device[]>([]) // 列表的数据
|
|
||||||
const total = ref(0) // 列表的总页数
|
|
||||||
const selectedDevices = ref<IotDeviceApi.Device[]>([]) // 选中的设备列表
|
|
||||||
const selectedId = ref<number>() // 单选模式下选中的ID
|
|
||||||
const products = ref<IotProductApi.Product[]>([]) // 产品列表
|
|
||||||
const deviceGroups = ref<IotDeviceGroupApi.DeviceGroup[]>([]) // 设备分组列表
|
|
||||||
const selectedRowKeys = ref<number[]>([]) // 多选模式下选中的keys
|
|
||||||
|
|
||||||
const queryParams = reactive({
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
deviceName: undefined as string | undefined,
|
|
||||||
productId: undefined as number | undefined,
|
|
||||||
deviceType: undefined as number | undefined,
|
|
||||||
nickname: undefined as string | undefined,
|
|
||||||
status: undefined as number | undefined,
|
|
||||||
groupId: undefined as number | undefined
|
|
||||||
})
|
|
||||||
const queryFormRef = ref() // 搜索的表单
|
|
||||||
|
|
||||||
// 表格列定义
|
|
||||||
const columns = computed(() => {
|
|
||||||
const baseColumns = [
|
|
||||||
{
|
|
||||||
title: 'DeviceName',
|
|
||||||
dataIndex: 'deviceName',
|
|
||||||
key: 'deviceName',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '备注名称',
|
|
||||||
dataIndex: 'nickname',
|
|
||||||
key: 'nickname',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '所属产品',
|
|
||||||
key: 'productId',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '设备类型',
|
|
||||||
key: 'deviceType',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '所属分组',
|
|
||||||
key: 'groupIds',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '设备状态',
|
|
||||||
key: 'status',
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '最后上线时间',
|
|
||||||
key: 'onlineTime',
|
|
||||||
align: 'center',
|
|
||||||
width: 180
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 单选模式添加单选列
|
|
||||||
if (!props.multiple) {
|
|
||||||
baseColumns.unshift({
|
|
||||||
title: '',
|
|
||||||
key: 'radio',
|
|
||||||
width: 55,
|
|
||||||
align: 'center'
|
|
||||||
} as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseColumns
|
|
||||||
})
|
|
||||||
|
|
||||||
// 多选配置
|
|
||||||
const rowSelection = computed(() => ({
|
|
||||||
selectedRowKeys: selectedRowKeys.value,
|
|
||||||
onChange: (keys: number[], rows: IotDeviceApi.Device[]) => {
|
|
||||||
selectedRowKeys.value = keys
|
|
||||||
selectedDevices.value = rows
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
/** 查询列表 */
|
|
||||||
const getList = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
if (props.productId) {
|
|
||||||
queryParams.productId = props.productId
|
|
||||||
}
|
|
||||||
const data = await getDevicePage(queryParams)
|
|
||||||
list.value = data.list
|
|
||||||
total.value = data.total
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 搜索按钮操作 */
|
|
||||||
const handleQuery = () => {
|
|
||||||
queryParams.pageNo = 1
|
|
||||||
getList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 重置按钮操作 */
|
|
||||||
const resetQuery = () => {
|
|
||||||
queryFormRef.value.resetFields()
|
|
||||||
handleQuery()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打开弹窗 */
|
|
||||||
const open = async () => {
|
|
||||||
dialogVisible.value = true
|
|
||||||
// 重置选择状态
|
|
||||||
selectedDevices.value = []
|
|
||||||
selectedId.value = undefined
|
|
||||||
selectedRowKeys.value = []
|
|
||||||
if (!props.productId) {
|
|
||||||
// 获取产品列表
|
|
||||||
products.value = await getSimpleProductList()
|
|
||||||
}
|
|
||||||
// 获取设备列表
|
|
||||||
await getList()
|
|
||||||
}
|
|
||||||
defineExpose({ open })
|
|
||||||
|
|
||||||
/** 处理行点击事件 */
|
|
||||||
const tableRef = ref()
|
|
||||||
const handleRowClick = (row: IotDeviceApi.Device) => {
|
|
||||||
if (!props.multiple) {
|
|
||||||
selectedId.value = row.id
|
|
||||||
selectedDevices.value = [row]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理单选变更事件 */
|
|
||||||
const handleRadioChange = (row: IotDeviceApi.Device) => {
|
|
||||||
selectedId.value = row.id
|
|
||||||
selectedDevices.value = [row]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 提交表单 */
|
|
||||||
const emit = defineEmits(['success'])
|
|
||||||
const submitForm = async () => {
|
|
||||||
if (selectedDevices.value.length === 0) {
|
|
||||||
message.warning({ content: props.multiple ? '请至少选择一个设备' : '请选择一个设备' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
emit('success', props.multiple ? selectedDevices.value : selectedDevices.value[0])
|
|
||||||
dialogVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 **/
|
|
||||||
onMounted(async () => {
|
|
||||||
// 获取产品列表
|
|
||||||
products.value = await getSimpleProductList()
|
|
||||||
// 获取分组列表
|
|
||||||
deviceGroups.value = await getSimpleDeviceGroupList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,4 +1,118 @@
|
|||||||
<!-- 设备配置 -->
|
<!-- 设备配置 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DeviceVO } from '#/api/iot/device/device';
|
||||||
|
|
||||||
|
import { ref, watchEffect } from 'vue';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
defineOptions({ name: 'DeviceDetailConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
device: DeviceVO;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'success'): void; // 定义 success 事件,不需要参数
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false); // 加载中
|
||||||
|
const pushLoading = ref(false); // 推送加载中
|
||||||
|
const config = ref<any>({}); // 只存储 config 字段
|
||||||
|
const hasJsonError = ref(false); // 是否有 JSON 格式错误
|
||||||
|
|
||||||
|
/** 监听 props.device 的变化,只更新 config 字段 */
|
||||||
|
watchEffect(() => {
|
||||||
|
try {
|
||||||
|
config.value = props.device.config ? JSON.parse(props.device.config) : {};
|
||||||
|
} catch {
|
||||||
|
config.value = {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isEditing = ref(false); // 编辑状态
|
||||||
|
/** 启用编辑模式的函数 */
|
||||||
|
const enableEdit = () => {
|
||||||
|
isEditing.value = true;
|
||||||
|
hasJsonError.value = false; // 重置错误状态
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 取消编辑的函数 */
|
||||||
|
const cancelEdit = () => {
|
||||||
|
try {
|
||||||
|
config.value = props.device.config ? JSON.parse(props.device.config) : {};
|
||||||
|
} catch {
|
||||||
|
config.value = {};
|
||||||
|
}
|
||||||
|
isEditing.value = false;
|
||||||
|
hasJsonError.value = false; // 重置错误状态
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 保存配置的函数 */
|
||||||
|
const saveConfig = async () => {
|
||||||
|
if (hasJsonError.value) {
|
||||||
|
message.error({ content: 'JSON格式错误,请修正后再提交!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await updateDeviceConfig();
|
||||||
|
isEditing.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 配置推送处理函数 */
|
||||||
|
const handleConfigPush = async () => {
|
||||||
|
try {
|
||||||
|
pushLoading.value = true;
|
||||||
|
|
||||||
|
// 调用配置推送接口
|
||||||
|
await DeviceApi.sendDeviceMessage({
|
||||||
|
deviceId: props.device.id!,
|
||||||
|
method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
|
||||||
|
params: config.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success({ content: '配置推送成功!' });
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
message.error({ content: '配置推送失败!' });
|
||||||
|
console.error('配置推送错误:', error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
pushLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 更新设备配置 */
|
||||||
|
const updateDeviceConfig = async () => {
|
||||||
|
try {
|
||||||
|
// 提交请求
|
||||||
|
loading.value = true;
|
||||||
|
await DeviceApi.updateDevice({
|
||||||
|
id: props.device.id,
|
||||||
|
config: JSON.stringify(config.value),
|
||||||
|
} as DeviceVO);
|
||||||
|
message.success({ content: '更新成功!' });
|
||||||
|
// 触发 success 事件
|
||||||
|
emit('success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理 JSON 编辑器错误的函数 */
|
||||||
|
const onError = (errors: any) => {
|
||||||
|
if (!errors || (Array.isArray(errors) && errors.length === 0)) {
|
||||||
|
hasJsonError.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hasJsonError.value = true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a-alert
|
<a-alert
|
||||||
@@ -16,125 +130,23 @@
|
|||||||
/>
|
/>
|
||||||
<div class="mt-5 text-center">
|
<div class="mt-5 text-center">
|
||||||
<a-button v-if="isEditing" @click="cancelEdit">取消</a-button>
|
<a-button v-if="isEditing" @click="cancelEdit">取消</a-button>
|
||||||
<a-button v-if="isEditing" type="primary" @click="saveConfig" :disabled="hasJsonError">
|
<a-button
|
||||||
|
v-if="isEditing"
|
||||||
|
type="primary"
|
||||||
|
@click="saveConfig"
|
||||||
|
:disabled="hasJsonError"
|
||||||
|
>
|
||||||
保存
|
保存
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button v-else @click="enableEdit">编辑</a-button>
|
<a-button v-else @click="enableEdit">编辑</a-button>
|
||||||
<a-button v-if="!isEditing" type="primary" @click="handleConfigPush" :loading="pushLoading">
|
<a-button
|
||||||
|
v-if="!isEditing"
|
||||||
|
type="primary"
|
||||||
|
@click="handleConfigPush"
|
||||||
|
:loading="pushLoading"
|
||||||
|
>
|
||||||
配置推送
|
配置推送
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, watchEffect } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import type { DeviceVO } from '#/api/iot/device/device'
|
|
||||||
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceDetailConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
device: DeviceVO
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'success'): void // 定义 success 事件,不需要参数
|
|
||||||
}>()
|
|
||||||
|
|
||||||
|
|
||||||
const loading = ref(false) // 加载中
|
|
||||||
const pushLoading = ref(false) // 推送加载中
|
|
||||||
const config = ref<any>({}) // 只存储 config 字段
|
|
||||||
const hasJsonError = ref(false) // 是否有 JSON 格式错误
|
|
||||||
|
|
||||||
/** 监听 props.device 的变化,只更新 config 字段 */
|
|
||||||
watchEffect(() => {
|
|
||||||
try {
|
|
||||||
config.value = props.device.config ? JSON.parse(props.device.config) : {}
|
|
||||||
} catch (e) {
|
|
||||||
config.value = {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isEditing = ref(false) // 编辑状态
|
|
||||||
/** 启用编辑模式的函数 */
|
|
||||||
const enableEdit = () => {
|
|
||||||
isEditing.value = true
|
|
||||||
hasJsonError.value = false // 重置错误状态
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 取消编辑的函数 */
|
|
||||||
const cancelEdit = () => {
|
|
||||||
try {
|
|
||||||
config.value = props.device.config ? JSON.parse(props.device.config) : {}
|
|
||||||
} catch (e) {
|
|
||||||
config.value = {}
|
|
||||||
}
|
|
||||||
isEditing.value = false
|
|
||||||
hasJsonError.value = false // 重置错误状态
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 保存配置的函数 */
|
|
||||||
const saveConfig = async () => {
|
|
||||||
if (hasJsonError.value) {
|
|
||||||
message.error({ content: 'JSON格式错误,请修正后再提交!' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await updateDeviceConfig()
|
|
||||||
isEditing.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 配置推送处理函数 */
|
|
||||||
const handleConfigPush = async () => {
|
|
||||||
try {
|
|
||||||
pushLoading.value = true
|
|
||||||
|
|
||||||
// 调用配置推送接口
|
|
||||||
await DeviceApi.sendDeviceMessage({
|
|
||||||
deviceId: props.device.id!,
|
|
||||||
method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
|
|
||||||
params: config.value
|
|
||||||
})
|
|
||||||
|
|
||||||
message.success({ content: '配置推送成功!' })
|
|
||||||
} catch (error) {
|
|
||||||
if (error !== 'cancel') {
|
|
||||||
message.error({ content: '配置推送失败!' })
|
|
||||||
console.error('配置推送错误:', error)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
pushLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 更新设备配置 */
|
|
||||||
const updateDeviceConfig = async () => {
|
|
||||||
try {
|
|
||||||
// 提交请求
|
|
||||||
loading.value = true
|
|
||||||
await DeviceApi.updateDevice({
|
|
||||||
id: props.device.id,
|
|
||||||
config: JSON.stringify(config.value)
|
|
||||||
} as DeviceVO)
|
|
||||||
message.success({ content: '更新成功!' })
|
|
||||||
// 触发 success 事件
|
|
||||||
emit('success')
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理 JSON 编辑器错误的函数 */
|
|
||||||
const onError = (errors: any) => {
|
|
||||||
if (!errors || (Array.isArray(errors) && errors.length === 0)) {
|
|
||||||
hasJsonError.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hasJsonError.value = true
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,4 +1,55 @@
|
|||||||
<!-- 设备信息(头部) -->
|
<!-- 设备信息(头部) -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DeviceVO } from '#/api/iot/device/device';
|
||||||
|
import type { ProductVO } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import DeviceForm from '../DeviceForm.vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
product: ProductVO;
|
||||||
|
device: DeviceVO;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
withDefaults(defineProps<Props>(), {
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
refresh: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/** 操作修改 */
|
||||||
|
const formRef = ref();
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 复制到剪贴板方法 */
|
||||||
|
const copyToClipboard = async (text: string | undefined) => {
|
||||||
|
if (!text) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
message.success({ content: '复制成功' });
|
||||||
|
} catch {
|
||||||
|
message.error({ content: '复制失败' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 跳转到产品详情页面 */
|
||||||
|
const goToProductDetail = (productId: number | undefined) => {
|
||||||
|
if (productId) {
|
||||||
|
router.push({ name: 'IoTProductDetail', params: { id: productId } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
@@ -20,13 +71,20 @@
|
|||||||
<a-card class="mt-4">
|
<a-card class="mt-4">
|
||||||
<a-descriptions :column="1">
|
<a-descriptions :column="1">
|
||||||
<a-descriptions-item label="产品">
|
<a-descriptions-item label="产品">
|
||||||
<a @click="goToProductDetail(product.id)" class="cursor-pointer text-blue-600">
|
<a
|
||||||
|
@click="goToProductDetail(product.id)"
|
||||||
|
class="cursor-pointer text-blue-600"
|
||||||
|
>
|
||||||
{{ product.name }}
|
{{ product.name }}
|
||||||
</a>
|
</a>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="ProductKey">
|
<a-descriptions-item label="ProductKey">
|
||||||
{{ product.productKey }}
|
{{ product.productKey }}
|
||||||
<a-button size="small" class="ml-2" @click="copyToClipboard(product.productKey)">
|
<a-button
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
@click="copyToClipboard(product.productKey)"
|
||||||
|
>
|
||||||
复制
|
复制
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
@@ -37,51 +95,3 @@
|
|||||||
<DeviceForm ref="formRef" @success="emit('refresh')" />
|
<DeviceForm ref="formRef" @success="emit('refresh')" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import DeviceForm from '../DeviceForm.vue'
|
|
||||||
import type { ProductVO } from '#/api/iot/product/product'
|
|
||||||
import type { DeviceVO } from '#/api/iot/device/device'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
product: ProductVO
|
|
||||||
device: DeviceVO
|
|
||||||
loading?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
withDefaults(defineProps<Props>(), {
|
|
||||||
loading: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
refresh: []
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
/** 操作修改 */
|
|
||||||
const formRef = ref()
|
|
||||||
const openForm = (type: string, id?: number) => {
|
|
||||||
formRef.value.open(type, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 复制到剪贴板方法 */
|
|
||||||
const copyToClipboard = async (text: string | undefined) => {
|
|
||||||
if (!text) return
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
message.success({ content: '复制成功' })
|
|
||||||
} catch (error) {
|
|
||||||
message.error({ content: '复制失败' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 跳转到产品详情页面 */
|
|
||||||
const goToProductDetail = (productId: number | undefined) => {
|
|
||||||
if (productId) {
|
|
||||||
router.push({ name: 'IoTProductDetail', params: { id: productId } })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,4 +1,64 @@
|
|||||||
<!-- 设备信息 -->
|
<!-- 设备信息 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { DeviceVO, IotDeviceAuthInfoVO } from '#/api/iot/device/device';
|
||||||
|
import type { ProductVO } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
|
// 消息提示
|
||||||
|
|
||||||
|
const { product, device } = defineProps<{
|
||||||
|
device: DeviceVO;
|
||||||
|
product: ProductVO;
|
||||||
|
}>(); // 定义 Props
|
||||||
|
const emit = defineEmits(['refresh']); // 定义 Emits
|
||||||
|
|
||||||
|
const authDialogVisible = ref(false); // 定义设备认证信息弹框的可见性
|
||||||
|
const authPasswordVisible = ref(false); // 定义密码可见性状态
|
||||||
|
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO); // 定义设备认证信息对象
|
||||||
|
|
||||||
|
/** 控制地图显示的标志 */
|
||||||
|
const showMap = computed(() => {
|
||||||
|
return !!(device.longitude && device.latitude);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 复制到剪贴板方法 */
|
||||||
|
const copyToClipboard = async (text: string) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
message.success({ content: '复制成功' });
|
||||||
|
} catch {
|
||||||
|
message.error({ content: '复制失败' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 打开设备认证信息弹框的方法 */
|
||||||
|
const handleAuthInfoDialogOpen = async () => {
|
||||||
|
if (!device.id) return;
|
||||||
|
try {
|
||||||
|
authInfo.value = await DeviceApi.getDeviceAuthInfo(device.id);
|
||||||
|
// 显示设备认证信息弹框
|
||||||
|
authDialogVisible.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备认证信息出错:', error);
|
||||||
|
message.error({
|
||||||
|
content: '获取设备认证信息失败,请检查网络连接或联系管理员',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 关闭设备认证信息弹框的方法 */
|
||||||
|
const handleAuthInfoDialogClose = () => {
|
||||||
|
authDialogVisible.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a-row :gutter="16">
|
<a-row :gutter="16">
|
||||||
@@ -7,7 +67,7 @@
|
|||||||
<a-card class="h-full">
|
<a-card class="h-full">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Icon icon="ep:info-filled" class="mr-2 text-primary" />
|
<Icon icon="ep:info-filled" class="text-primary mr-2" />
|
||||||
<span>设备信息</span>
|
<span>设备信息</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -19,7 +79,10 @@
|
|||||||
{{ product.productKey }}
|
{{ product.productKey }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="设备类型">
|
<a-descriptions-item label="设备类型">
|
||||||
<dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
|
<dict-tag
|
||||||
|
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||||
|
:value="product.deviceType"
|
||||||
|
/>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="DeviceName">
|
<a-descriptions-item label="DeviceName">
|
||||||
{{ device.deviceName }}
|
{{ device.deviceName }}
|
||||||
@@ -28,7 +91,10 @@
|
|||||||
{{ device.nickname || '--' }}
|
{{ device.nickname || '--' }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="当前状态">
|
<a-descriptions-item label="当前状态">
|
||||||
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATUS" :value="device.state" />
|
<dict-tag
|
||||||
|
:type="DICT_TYPE.IOT_DEVICE_STATUS"
|
||||||
|
:value="device.state"
|
||||||
|
/>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="创建时间">
|
<a-descriptions-item label="创建时间">
|
||||||
{{ formatDate(device.createTime) }}
|
{{ formatDate(device.createTime) }}
|
||||||
@@ -43,7 +109,11 @@
|
|||||||
{{ formatDate(device.offlineTime) }}
|
{{ formatDate(device.offlineTime) }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="MQTT 连接参数">
|
<a-descriptions-item label="MQTT 连接参数">
|
||||||
<a-button type="link" @click="handleAuthInfoDialogOpen" size="small">
|
<a-button
|
||||||
|
type="link"
|
||||||
|
@click="handleAuthInfoDialogOpen"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
查看
|
查看
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
@@ -57,18 +127,21 @@
|
|||||||
<template #title>
|
<template #title>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<Icon icon="ep:location" class="mr-2 text-primary" />
|
<Icon icon="ep:location" class="text-primary mr-2" />
|
||||||
<span>设备位置</span>
|
<span>设备位置</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="h-[500px] w-full">
|
<div class="h-[500px] w-full">
|
||||||
<div v-if="showMap" class="h-full w-full bg-gray-100 flex items-center justify-center rounded">
|
<div
|
||||||
|
v-if="showMap"
|
||||||
|
class="flex h-full w-full items-center justify-center rounded bg-gray-100"
|
||||||
|
>
|
||||||
<span class="text-gray-400">地图组件</span>
|
<span class="text-gray-400">地图组件</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex items-center justify-center h-full w-full bg-gray-50 text-gray-400 rounded"
|
class="flex h-full w-full items-center justify-center rounded bg-gray-50 text-gray-400"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:warning" class="mr-2" />
|
<Icon icon="ep:warning" class="mr-2" />
|
||||||
<span>暂无位置信息</span>
|
<span>暂无位置信息</span>
|
||||||
@@ -88,16 +161,30 @@
|
|||||||
<a-form :label-col="{ span: 6 }">
|
<a-form :label-col="{ span: 6 }">
|
||||||
<a-form-item label="clientId">
|
<a-form-item label="clientId">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-input v-model:value="authInfo.clientId" readonly style="width: calc(100% - 80px)" />
|
<a-input
|
||||||
<a-button @click="copyToClipboard(authInfo.clientId)" type="primary">
|
v-model:value="authInfo.clientId"
|
||||||
|
readonly
|
||||||
|
style="width: calc(100% - 80px)"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
@click="copyToClipboard(authInfo.clientId)"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
<Icon icon="ph:copy" />
|
<Icon icon="ph:copy" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="username">
|
<a-form-item label="username">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-input v-model:value="authInfo.username" readonly style="width: calc(100% - 80px)" />
|
<a-input
|
||||||
<a-button @click="copyToClipboard(authInfo.username)" type="primary">
|
v-model:value="authInfo.username"
|
||||||
|
readonly
|
||||||
|
style="width: calc(100% - 80px)"
|
||||||
|
/>
|
||||||
|
<a-button
|
||||||
|
@click="copyToClipboard(authInfo.username)"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
<Icon icon="ph:copy" />
|
<Icon icon="ph:copy" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
@@ -110,70 +197,24 @@
|
|||||||
:type="authPasswordVisible ? 'text' : 'password'"
|
:type="authPasswordVisible ? 'text' : 'password'"
|
||||||
style="width: calc(100% - 160px)"
|
style="width: calc(100% - 160px)"
|
||||||
/>
|
/>
|
||||||
<a-button @click="authPasswordVisible = !authPasswordVisible" type="primary">
|
<a-button
|
||||||
|
@click="authPasswordVisible = !authPasswordVisible"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
<Icon :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'" />
|
<Icon :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'" />
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="copyToClipboard(authInfo.password)" type="primary">
|
<a-button
|
||||||
|
@click="copyToClipboard(authInfo.password)"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
<Icon icon="ph:copy" />
|
<Icon icon="ph:copy" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-input-group>
|
</a-input-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="text-right mt-4">
|
<div class="mt-4 text-right">
|
||||||
<a-button @click="handleAuthInfoDialogClose">关闭</a-button>
|
<a-button @click="handleAuthInfoDialogClose">关闭</a-button>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { DICT_TYPE } from '@vben/constants'
|
|
||||||
import type { ProductVO } from '#/api/iot/product/product'
|
|
||||||
import { formatDate } from '@vben/utils'
|
|
||||||
import type { DeviceVO } from '#/api/iot/device/device'
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import type { IotDeviceAuthInfoVO } from '#/api/iot/device/device'
|
|
||||||
|
|
||||||
// 消息提示
|
|
||||||
|
|
||||||
const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 定义 Props
|
|
||||||
const emit = defineEmits(['refresh']) // 定义 Emits
|
|
||||||
|
|
||||||
const authDialogVisible = ref(false) // 定义设备认证信息弹框的可见性
|
|
||||||
const authPasswordVisible = ref(false) // 定义密码可见性状态
|
|
||||||
const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) // 定义设备认证信息对象
|
|
||||||
|
|
||||||
/** 控制地图显示的标志 */
|
|
||||||
const showMap = computed(() => {
|
|
||||||
return !!(device.longitude && device.latitude)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 复制到剪贴板方法 */
|
|
||||||
const copyToClipboard = async (text: string) => {
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
message.success({ content: '复制成功' })
|
|
||||||
} catch (error) {
|
|
||||||
message.error({ content: '复制失败' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打开设备认证信息弹框的方法 */
|
|
||||||
const handleAuthInfoDialogOpen = async () => {
|
|
||||||
if (!device.id) return
|
|
||||||
try {
|
|
||||||
authInfo.value = await DeviceApi.getDeviceAuthInfo(device.id)
|
|
||||||
// 显示设备认证信息弹框
|
|
||||||
authDialogVisible.value = true
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取设备认证信息出错:', error)
|
|
||||||
message.error({ content: '获取设备认证信息失败,请检查网络连接或联系管理员' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 关闭设备认证信息弹框的方法 */
|
|
||||||
const handleAuthInfoDialogClose = () => {
|
|
||||||
authDialogVisible.value = false
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,10 +1,178 @@
|
|||||||
<!-- 设备消息列表 -->
|
<!-- 设备消息列表 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
deviceId: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 查询参数
|
||||||
|
const queryParams = reactive({
|
||||||
|
deviceId: props.deviceId,
|
||||||
|
method: undefined,
|
||||||
|
upstream: undefined,
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 列表数据
|
||||||
|
const loading = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
const list = ref<any[]>([]);
|
||||||
|
const autoRefresh = ref(false); // 自动刷新开关
|
||||||
|
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||||
|
|
||||||
|
// 消息方法选项
|
||||||
|
const methodOptions = computed(() => {
|
||||||
|
return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
value: item.method,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'ts',
|
||||||
|
key: 'ts',
|
||||||
|
align: 'center',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '上行/下行',
|
||||||
|
dataIndex: 'upstream',
|
||||||
|
key: 'upstream',
|
||||||
|
align: 'center',
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否回复',
|
||||||
|
dataIndex: 'reply',
|
||||||
|
key: 'reply',
|
||||||
|
align: 'center',
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '请求编号',
|
||||||
|
dataIndex: 'requestId',
|
||||||
|
key: 'requestId',
|
||||||
|
align: 'center',
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '请求方法',
|
||||||
|
dataIndex: 'method',
|
||||||
|
key: 'method',
|
||||||
|
align: 'center',
|
||||||
|
width: 140,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '请求/响应数据',
|
||||||
|
dataIndex: 'params',
|
||||||
|
key: 'params',
|
||||||
|
align: 'center',
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 查询消息列表 */
|
||||||
|
const getMessageList = async () => {
|
||||||
|
if (!props.deviceId) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await DeviceApi.getDeviceMessagePage(queryParams);
|
||||||
|
total.value = data.total;
|
||||||
|
list.value = data.list;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getMessageList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听自动刷新 */
|
||||||
|
watch(autoRefresh, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
autoRefreshTimer = setInterval(() => {
|
||||||
|
getMessageList();
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
clearInterval(autoRefreshTimer);
|
||||||
|
autoRefreshTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听设备标识变化 */
|
||||||
|
watch(
|
||||||
|
() => props.deviceId,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 组件卸载时清除定时器 */
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (autoRefreshTimer) {
|
||||||
|
clearInterval(autoRefreshTimer);
|
||||||
|
autoRefreshTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.deviceId) {
|
||||||
|
getMessageList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新消息列表 */
|
||||||
|
const refresh = (delay = 0) => {
|
||||||
|
if (delay > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleQuery();
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
handleQuery();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 暴露方法给父组件 */
|
||||||
|
defineExpose({
|
||||||
|
refresh,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索区域 -->
|
<!-- 搜索区域 -->
|
||||||
<a-form :model="queryParams" layout="inline">
|
<a-form :model="queryParams" layout="inline">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-select v-model:value="queryParams.method" placeholder="所有方法" style="width: 160px" allow-clear>
|
<a-select
|
||||||
|
v-model:value="queryParams.method"
|
||||||
|
placeholder="所有方法"
|
||||||
|
style="width: 160px"
|
||||||
|
allow-clear
|
||||||
|
>
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-for="item in methodOptions"
|
v-for="item in methodOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
@@ -40,7 +208,13 @@
|
|||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<!-- 消息列表 -->
|
<!-- 消息列表 -->
|
||||||
<a-table :loading="loading" :dataSource="list" :columns="columns" :pagination="false" class="whitespace-nowrap">
|
<a-table
|
||||||
|
:loading="loading"
|
||||||
|
:data-source="list"
|
||||||
|
:columns="columns"
|
||||||
|
:pagination="false"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'ts'">
|
<template v-if="column.key === 'ts'">
|
||||||
{{ formatDate(record.ts) }}
|
{{ formatDate(record.ts) }}
|
||||||
@@ -51,14 +225,21 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'reply'">
|
<template v-else-if="column.key === 'reply'">
|
||||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="record.reply" />
|
<dict-tag
|
||||||
|
:type="DICT_TYPE.INFRA_BOOLEAN_STRING"
|
||||||
|
:value="record.reply"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'method'">
|
<template v-else-if="column.key === 'method'">
|
||||||
{{ methodOptions.find((item) => item.value === record.method)?.label }}
|
{{
|
||||||
|
methodOptions.find((item) => item.value === record.method)?.label
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'params'">
|
<template v-else-if="column.key === 'params'">
|
||||||
<span v-if="record.reply">
|
<span v-if="record.reply">
|
||||||
{{ `{"code":${record.code},"msg":"${record.msg}","data":${record.data}\}` }}
|
{{
|
||||||
|
`{"code":${record.code},"msg":"${record.msg}","data":${record.data}\}`
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ record.params }}</span>
|
<span v-else>{{ record.params }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -76,157 +257,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
||||||
import { DICT_TYPE } from '@vben/constants'
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import { formatDate } from '@vben/utils'
|
|
||||||
import { IotDeviceMessageMethodEnum } from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
deviceId: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 查询参数
|
|
||||||
const queryParams = reactive({
|
|
||||||
deviceId: props.deviceId,
|
|
||||||
method: undefined,
|
|
||||||
upstream: undefined,
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// 列表数据
|
|
||||||
const loading = ref(false)
|
|
||||||
const total = ref(0)
|
|
||||||
const list = ref<any[]>([])
|
|
||||||
const autoRefresh = ref(false) // 自动刷新开关
|
|
||||||
let autoRefreshTimer: any = null // 自动刷新定时器
|
|
||||||
|
|
||||||
// 消息方法选项
|
|
||||||
const methodOptions = computed(() => {
|
|
||||||
return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
value: item.method
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 表格列定义
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: '时间',
|
|
||||||
dataIndex: 'ts',
|
|
||||||
key: 'ts',
|
|
||||||
align: 'center',
|
|
||||||
width: 180
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '上行/下行',
|
|
||||||
dataIndex: 'upstream',
|
|
||||||
key: 'upstream',
|
|
||||||
align: 'center',
|
|
||||||
width: 140
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '是否回复',
|
|
||||||
dataIndex: 'reply',
|
|
||||||
key: 'reply',
|
|
||||||
align: 'center',
|
|
||||||
width: 140
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '请求编号',
|
|
||||||
dataIndex: 'requestId',
|
|
||||||
key: 'requestId',
|
|
||||||
align: 'center',
|
|
||||||
width: 300
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '请求方法',
|
|
||||||
dataIndex: 'method',
|
|
||||||
key: 'method',
|
|
||||||
align: 'center',
|
|
||||||
width: 140
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '请求/响应数据',
|
|
||||||
dataIndex: 'params',
|
|
||||||
key: 'params',
|
|
||||||
align: 'center',
|
|
||||||
ellipsis: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
/** 查询消息列表 */
|
|
||||||
const getMessageList = async () => {
|
|
||||||
if (!props.deviceId) return
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await DeviceApi.getDeviceMessagePage(queryParams)
|
|
||||||
total.value = data.total
|
|
||||||
list.value = data.list
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 搜索操作 */
|
|
||||||
const handleQuery = () => {
|
|
||||||
queryParams.pageNo = 1
|
|
||||||
getMessageList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听自动刷新 */
|
|
||||||
watch(autoRefresh, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
autoRefreshTimer = setInterval(() => {
|
|
||||||
getMessageList()
|
|
||||||
}, 5000)
|
|
||||||
} else {
|
|
||||||
clearInterval(autoRefreshTimer)
|
|
||||||
autoRefreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 监听设备标识变化 */
|
|
||||||
watch(
|
|
||||||
() => props.deviceId,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
handleQuery()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 组件卸载时清除定时器 */
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (autoRefreshTimer) {
|
|
||||||
clearInterval(autoRefreshTimer)
|
|
||||||
autoRefreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(() => {
|
|
||||||
if (props.deviceId) {
|
|
||||||
getMessageList()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 刷新消息列表 */
|
|
||||||
const refresh = (delay = 0) => {
|
|
||||||
if (delay > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
handleQuery()
|
|
||||||
}, delay)
|
|
||||||
} else {
|
|
||||||
handleQuery()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 暴露方法给父组件 */
|
|
||||||
defineExpose({
|
|
||||||
refresh
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,4 +1,339 @@
|
|||||||
<!-- 模拟设备 -->
|
<!-- 模拟设备 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DeviceVO } from '#/api/iot/device/device';
|
||||||
|
import type { ProductVO } from '#/api/iot/product/product';
|
||||||
|
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { DeviceApi, DeviceStateEnum } from '#/api/iot/device/device';
|
||||||
|
import {
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
device: DeviceVO;
|
||||||
|
product: ProductVO;
|
||||||
|
thingModelList: ThingModelData[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 消息弹窗
|
||||||
|
const activeTab = ref('upstream'); // 上行upstream、下行downstream
|
||||||
|
const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method); // 上行子标签
|
||||||
|
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method); // 下行子标签
|
||||||
|
const deviceMessageRef = ref(); // 设备消息组件引用
|
||||||
|
const deviceMessageRefreshDelay = 2000; // 延迟 N 秒,保证模拟上行的消息被处理
|
||||||
|
|
||||||
|
// 表单数据:存储用户输入的模拟值
|
||||||
|
const formData = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 根据类型过滤物模型数据
|
||||||
|
const getFilteredThingModelList = (type: number) => {
|
||||||
|
return props.thingModelList.filter(
|
||||||
|
(item) => String(item.type) === String(type),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算属性:属性列表
|
||||||
|
const propertyList = computed(() =>
|
||||||
|
getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算属性:事件列表
|
||||||
|
const eventList = computed(() =>
|
||||||
|
getFilteredThingModelList(IoTThingModelTypeEnum.EVENT),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 计算属性:服务列表
|
||||||
|
const serviceList = computed(() =>
|
||||||
|
getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 属性表格列定义
|
||||||
|
const propertyColumns = [
|
||||||
|
{
|
||||||
|
title: '功能名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标识符',
|
||||||
|
dataIndex: 'identifier',
|
||||||
|
key: 'identifier',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据类型',
|
||||||
|
key: 'dataType',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据定义',
|
||||||
|
key: 'dataDefinition',
|
||||||
|
minWidth: 200,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '值',
|
||||||
|
key: 'value',
|
||||||
|
width: 150,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 事件表格列定义
|
||||||
|
const eventColumns = [
|
||||||
|
{
|
||||||
|
title: '功能名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标识符',
|
||||||
|
dataIndex: 'identifier',
|
||||||
|
key: 'identifier',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据类型',
|
||||||
|
key: 'dataType',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '数据定义',
|
||||||
|
key: 'dataDefinition',
|
||||||
|
minWidth: 200,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '值',
|
||||||
|
key: 'value',
|
||||||
|
width: 200,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 服务表格列定义
|
||||||
|
const serviceColumns = [
|
||||||
|
{
|
||||||
|
title: '服务名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '标识符',
|
||||||
|
dataIndex: 'identifier',
|
||||||
|
key: 'identifier',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '输入参数',
|
||||||
|
key: 'dataDefinition',
|
||||||
|
minWidth: 200,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '参数值',
|
||||||
|
key: 'value',
|
||||||
|
width: 200,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取表单值
|
||||||
|
const getFormValue = (identifier: string) => {
|
||||||
|
return formData.value[identifier] || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置表单值
|
||||||
|
const setFormValue = (identifier: string, value: string) => {
|
||||||
|
formData.value[identifier] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 属性上报
|
||||||
|
const handlePropertyPost = async () => {
|
||||||
|
try {
|
||||||
|
const params: Record<string, any> = {};
|
||||||
|
propertyList.value.forEach((item) => {
|
||||||
|
const value = formData.value[item.identifier!];
|
||||||
|
if (value) {
|
||||||
|
params[item.identifier!] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(params).length === 0) {
|
||||||
|
message.warning({ content: '请至少输入一个属性值' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DeviceApi.sendDeviceMessage({
|
||||||
|
deviceId: props.device.id!,
|
||||||
|
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success({ content: '属性上报成功' });
|
||||||
|
// 延迟刷新设备消息列表
|
||||||
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
|
} catch (error) {
|
||||||
|
message.error({ content: '属性上报失败' });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 事件上报
|
||||||
|
const handleEventPost = async (row: ThingModelData) => {
|
||||||
|
try {
|
||||||
|
const valueStr = formData.value[row.identifier!];
|
||||||
|
let params: any = {};
|
||||||
|
|
||||||
|
if (valueStr) {
|
||||||
|
try {
|
||||||
|
params = JSON.parse(valueStr);
|
||||||
|
} catch {
|
||||||
|
message.error({ content: '事件参数格式错误,请输入有效的JSON格式' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await DeviceApi.sendDeviceMessage({
|
||||||
|
deviceId: props.device.id!,
|
||||||
|
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||||
|
params: {
|
||||||
|
identifier: row.identifier,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success({ content: '事件上报成功' });
|
||||||
|
// 延迟刷新设备消息列表
|
||||||
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
|
} catch (error) {
|
||||||
|
message.error({ content: '事件上报失败' });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态变更
|
||||||
|
const handleDeviceState = async (state: number) => {
|
||||||
|
try {
|
||||||
|
await DeviceApi.sendDeviceMessage({
|
||||||
|
deviceId: props.device.id!,
|
||||||
|
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
|
||||||
|
params: { state },
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success({ content: '状态变更成功' });
|
||||||
|
// 延迟刷新设备消息列表
|
||||||
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
|
} catch (error) {
|
||||||
|
message.error({ content: '状态变更失败' });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 属性设置
|
||||||
|
const handlePropertySet = async () => {
|
||||||
|
try {
|
||||||
|
const params: Record<string, any> = {};
|
||||||
|
propertyList.value.forEach((item) => {
|
||||||
|
const value = formData.value[item.identifier!];
|
||||||
|
if (value) {
|
||||||
|
params[item.identifier!] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(params).length === 0) {
|
||||||
|
message.warning({ content: '请至少输入一个属性值' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await DeviceApi.sendDeviceMessage({
|
||||||
|
deviceId: props.device.id!,
|
||||||
|
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success({ content: '属性设置成功' });
|
||||||
|
// 延迟刷新设备消息列表
|
||||||
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
|
} catch (error) {
|
||||||
|
message.error({ content: '属性设置失败' });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 服务调用
|
||||||
|
const handleServiceInvoke = async (row: ThingModelData) => {
|
||||||
|
try {
|
||||||
|
const valueStr = formData.value[row.identifier!];
|
||||||
|
let params: any = {};
|
||||||
|
|
||||||
|
if (valueStr) {
|
||||||
|
try {
|
||||||
|
params = JSON.parse(valueStr);
|
||||||
|
} catch {
|
||||||
|
message.error({ content: '服务参数格式错误,请输入有效的JSON格式' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await DeviceApi.sendDeviceMessage({
|
||||||
|
deviceId: props.device.id!,
|
||||||
|
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
||||||
|
params: {
|
||||||
|
identifier: row.identifier,
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success({ content: '服务调用成功' });
|
||||||
|
// 延迟刷新设备消息列表
|
||||||
|
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay);
|
||||||
|
} catch (error) {
|
||||||
|
message.error({ content: '服务调用失败' });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<a-row :gutter="20">
|
<a-row :gutter="20">
|
||||||
@@ -8,11 +343,21 @@
|
|||||||
<a-tabs v-model:active-key="activeTab">
|
<a-tabs v-model:active-key="activeTab">
|
||||||
<!-- 上行指令调试 -->
|
<!-- 上行指令调试 -->
|
||||||
<a-tab-pane key="upstream" tab="上行指令调试">
|
<a-tab-pane key="upstream" tab="上行指令调试">
|
||||||
<a-tabs v-if="activeTab === 'upstream'" v-model:active-key="upstreamTab">
|
<a-tabs
|
||||||
|
v-if="activeTab === 'upstream'"
|
||||||
|
v-model:active-key="upstreamTab"
|
||||||
|
>
|
||||||
<!-- 属性上报 -->
|
<!-- 属性上报 -->
|
||||||
<a-tab-pane :key="IotDeviceMessageMethodEnum.PROPERTY_POST.method" tab="属性上报">
|
<a-tab-pane
|
||||||
|
:key="IotDeviceMessageMethodEnum.PROPERTY_POST.method"
|
||||||
|
tab="属性上报"
|
||||||
|
>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<a-table :dataSource="propertyList" :columns="propertyColumns" :pagination="false">
|
<a-table
|
||||||
|
:data-source="propertyList"
|
||||||
|
:columns="propertyColumns"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'dataType'">
|
<template v-if="column.key === 'dataType'">
|
||||||
{{ record.property?.dataType ?? '-' }}
|
{{ record.property?.dataType ?? '-' }}
|
||||||
@@ -23,26 +368,37 @@
|
|||||||
<template v-else-if="column.key === 'value'">
|
<template v-else-if="column.key === 'value'">
|
||||||
<a-input
|
<a-input
|
||||||
:value="getFormValue(record.identifier)"
|
:value="getFormValue(record.identifier)"
|
||||||
@update:value="setFormValue(record.identifier, $event)"
|
@update:value="
|
||||||
|
setFormValue(record.identifier, $event)
|
||||||
|
"
|
||||||
placeholder="输入值"
|
placeholder="输入值"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
<div class="flex justify-between items-center mt-4">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">
|
<span class="text-sm text-gray-600">
|
||||||
设置属性值后,点击「发送属性上报」按钮
|
设置属性值后,点击「发送属性上报」按钮
|
||||||
</span>
|
</span>
|
||||||
<a-button type="primary" @click="handlePropertyPost">发送属性上报</a-button>
|
<a-button type="primary" @click="handlePropertyPost">
|
||||||
|
发送属性上报
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
<!-- 事件上报 -->
|
<!-- 事件上报 -->
|
||||||
<a-tab-pane :key="IotDeviceMessageMethodEnum.EVENT_POST.method" tab="事件上报">
|
<a-tab-pane
|
||||||
|
:key="IotDeviceMessageMethodEnum.EVENT_POST.method"
|
||||||
|
tab="事件上报"
|
||||||
|
>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<a-table :dataSource="eventList" :columns="eventColumns" :pagination="false">
|
<a-table
|
||||||
|
:data-source="eventList"
|
||||||
|
:columns="eventColumns"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'dataType'">
|
<template v-if="column.key === 'dataType'">
|
||||||
{{ record.event?.dataType ?? '-' }}
|
{{ record.event?.dataType ?? '-' }}
|
||||||
@@ -53,14 +409,20 @@
|
|||||||
<template v-else-if="column.key === 'value'">
|
<template v-else-if="column.key === 'value'">
|
||||||
<a-textarea
|
<a-textarea
|
||||||
:value="getFormValue(record.identifier)"
|
:value="getFormValue(record.identifier)"
|
||||||
@update:value="setFormValue(record.identifier, $event)"
|
@update:value="
|
||||||
|
setFormValue(record.identifier, $event)
|
||||||
|
"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="输入事件参数(JSON格式)"
|
placeholder="输入事件参数(JSON格式)"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'action'">
|
<template v-else-if="column.key === 'action'">
|
||||||
<a-button type="primary" size="small" @click="handleEventPost(record)">
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleEventPost(record)"
|
||||||
|
>
|
||||||
上报事件
|
上报事件
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,13 +432,22 @@
|
|||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
<!-- 状态变更 -->
|
<!-- 状态变更 -->
|
||||||
<a-tab-pane :key="IotDeviceMessageMethodEnum.STATE_UPDATE.method" tab="状态变更">
|
<a-tab-pane
|
||||||
|
:key="IotDeviceMessageMethodEnum.STATE_UPDATE.method"
|
||||||
|
tab="状态变更"
|
||||||
|
>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<a-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)">
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleDeviceState(DeviceStateEnum.ONLINE)"
|
||||||
|
>
|
||||||
设备上线
|
设备上线
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button danger @click="handleDeviceState(DeviceStateEnum.OFFLINE)">
|
<a-button
|
||||||
|
danger
|
||||||
|
@click="handleDeviceState(DeviceStateEnum.OFFLINE)"
|
||||||
|
>
|
||||||
设备下线
|
设备下线
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,11 +458,21 @@
|
|||||||
|
|
||||||
<!-- 下行指令调试 -->
|
<!-- 下行指令调试 -->
|
||||||
<a-tab-pane key="downstream" tab="下行指令调试">
|
<a-tab-pane key="downstream" tab="下行指令调试">
|
||||||
<a-tabs v-if="activeTab === 'downstream'" v-model:active-key="downstreamTab">
|
<a-tabs
|
||||||
|
v-if="activeTab === 'downstream'"
|
||||||
|
v-model:active-key="downstreamTab"
|
||||||
|
>
|
||||||
<!-- 属性调试 -->
|
<!-- 属性调试 -->
|
||||||
<a-tab-pane :key="IotDeviceMessageMethodEnum.PROPERTY_SET.method" tab="属性设置">
|
<a-tab-pane
|
||||||
|
:key="IotDeviceMessageMethodEnum.PROPERTY_SET.method"
|
||||||
|
tab="属性设置"
|
||||||
|
>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<a-table :dataSource="propertyList" :columns="propertyColumns" :pagination="false">
|
<a-table
|
||||||
|
:data-source="propertyList"
|
||||||
|
:columns="propertyColumns"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'dataType'">
|
<template v-if="column.key === 'dataType'">
|
||||||
{{ record.property?.dataType ?? '-' }}
|
{{ record.property?.dataType ?? '-' }}
|
||||||
@@ -102,26 +483,37 @@
|
|||||||
<template v-else-if="column.key === 'value'">
|
<template v-else-if="column.key === 'value'">
|
||||||
<a-input
|
<a-input
|
||||||
:value="getFormValue(record.identifier)"
|
:value="getFormValue(record.identifier)"
|
||||||
@update:value="setFormValue(record.identifier, $event)"
|
@update:value="
|
||||||
|
setFormValue(record.identifier, $event)
|
||||||
|
"
|
||||||
placeholder="输入值"
|
placeholder="输入值"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
<div class="flex justify-between items-center mt-4">
|
<div class="mt-4 flex items-center justify-between">
|
||||||
<span class="text-sm text-gray-600">
|
<span class="text-sm text-gray-600">
|
||||||
设置属性值后,点击「发送属性设置」按钮
|
设置属性值后,点击「发送属性设置」按钮
|
||||||
</span>
|
</span>
|
||||||
<a-button type="primary" @click="handlePropertySet">发送属性设置</a-button>
|
<a-button type="primary" @click="handlePropertySet">
|
||||||
|
发送属性设置
|
||||||
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
|
||||||
<!-- 服务调用 -->
|
<!-- 服务调用 -->
|
||||||
<a-tab-pane :key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method" tab="设备服务调用">
|
<a-tab-pane
|
||||||
|
:key="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
|
||||||
|
tab="设备服务调用"
|
||||||
|
>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<a-table :dataSource="serviceList" :columns="serviceColumns" :pagination="false">
|
<a-table
|
||||||
|
:data-source="serviceList"
|
||||||
|
:columns="serviceColumns"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'dataDefinition'">
|
<template v-if="column.key === 'dataDefinition'">
|
||||||
<DataDefinition :data="record" />
|
<DataDefinition :data="record" />
|
||||||
@@ -129,7 +521,9 @@
|
|||||||
<template v-else-if="column.key === 'value'">
|
<template v-else-if="column.key === 'value'">
|
||||||
<a-textarea
|
<a-textarea
|
||||||
:value="getFormValue(record.identifier)"
|
:value="getFormValue(record.identifier)"
|
||||||
@update:value="setFormValue(record.identifier, $event)"
|
@update:value="
|
||||||
|
setFormValue(record.identifier, $event)
|
||||||
|
"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="输入服务参数(JSON格式)"
|
placeholder="输入服务参数(JSON格式)"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -157,329 +551,13 @@
|
|||||||
<!-- 右侧设备日志区域 -->
|
<!-- 右侧设备日志区域 -->
|
||||||
<a-col :span="12">
|
<a-col :span="12">
|
||||||
<ContentWrap title="设备消息">
|
<ContentWrap title="设备消息">
|
||||||
<DeviceDetailsMessage v-if="device.id" ref="deviceMessageRef" :device-id="device.id" />
|
<DeviceDetailsMessage
|
||||||
|
v-if="device.id"
|
||||||
|
ref="deviceMessageRef"
|
||||||
|
:device-id="device.id"
|
||||||
|
/>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import type { ProductVO } from '#/api/iot/product/product'
|
|
||||||
import type { ThingModelData } from '#/api/iot/thingmodel'
|
|
||||||
import { DeviceApi, DeviceStateEnum } from '#/api/iot/device/device'
|
|
||||||
import type { DeviceVO } from '#/api/iot/device/device'
|
|
||||||
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
|
|
||||||
import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
product: ProductVO
|
|
||||||
device: DeviceVO
|
|
||||||
thingModelList: ThingModelData[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
// 消息弹窗
|
|
||||||
const activeTab = ref('upstream') // 上行upstream、下行downstream
|
|
||||||
const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method) // 上行子标签
|
|
||||||
const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method) // 下行子标签
|
|
||||||
const deviceMessageRef = ref() // 设备消息组件引用
|
|
||||||
const deviceMessageRefreshDelay = 2000 // 延迟 N 秒,保证模拟上行的消息被处理
|
|
||||||
|
|
||||||
// 表单数据:存储用户输入的模拟值
|
|
||||||
const formData = ref<Record<string, string>>({})
|
|
||||||
|
|
||||||
// 根据类型过滤物模型数据
|
|
||||||
const getFilteredThingModelList = (type: number) => {
|
|
||||||
return props.thingModelList.filter((item) => String(item.type) === String(type))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算属性:属性列表
|
|
||||||
const propertyList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY))
|
|
||||||
|
|
||||||
// 计算属性:事件列表
|
|
||||||
const eventList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.EVENT))
|
|
||||||
|
|
||||||
// 计算属性:服务列表
|
|
||||||
const serviceList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE))
|
|
||||||
|
|
||||||
// 属性表格列定义
|
|
||||||
const propertyColumns = [
|
|
||||||
{
|
|
||||||
title: '功能名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '标识符',
|
|
||||||
dataIndex: 'identifier',
|
|
||||||
key: 'identifier',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数据类型',
|
|
||||||
key: 'dataType',
|
|
||||||
width: 100,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数据定义',
|
|
||||||
key: 'dataDefinition',
|
|
||||||
minWidth: 200,
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '值',
|
|
||||||
key: 'value',
|
|
||||||
width: 150,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'right'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 事件表格列定义
|
|
||||||
const eventColumns = [
|
|
||||||
{
|
|
||||||
title: '功能名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '标识符',
|
|
||||||
dataIndex: 'identifier',
|
|
||||||
key: 'identifier',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数据类型',
|
|
||||||
key: 'dataType',
|
|
||||||
width: 100,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数据定义',
|
|
||||||
key: 'dataDefinition',
|
|
||||||
minWidth: 200,
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '值',
|
|
||||||
key: 'value',
|
|
||||||
width: 200,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'right'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 服务表格列定义
|
|
||||||
const serviceColumns = [
|
|
||||||
{
|
|
||||||
title: '服务名称',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '标识符',
|
|
||||||
dataIndex: 'identifier',
|
|
||||||
key: 'identifier',
|
|
||||||
width: 120,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '输入参数',
|
|
||||||
key: 'dataDefinition',
|
|
||||||
minWidth: 200,
|
|
||||||
align: 'left'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '参数值',
|
|
||||||
key: 'value',
|
|
||||||
width: 200,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
width: 100,
|
|
||||||
align: 'center',
|
|
||||||
fixed: 'right'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 获取表单值
|
|
||||||
const getFormValue = (identifier: string) => {
|
|
||||||
return formData.value[identifier] || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置表单值
|
|
||||||
const setFormValue = (identifier: string, value: string) => {
|
|
||||||
formData.value[identifier] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 属性上报
|
|
||||||
const handlePropertyPost = async () => {
|
|
||||||
try {
|
|
||||||
const params: Record<string, any> = {}
|
|
||||||
propertyList.value.forEach((item) => {
|
|
||||||
const value = formData.value[item.identifier!]
|
|
||||||
if (value) {
|
|
||||||
params[item.identifier!] = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Object.keys(params).length === 0) {
|
|
||||||
message.warning({ content: '请至少输入一个属性值' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await DeviceApi.sendDeviceMessage({
|
|
||||||
deviceId: props.device.id!,
|
|
||||||
method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
|
|
||||||
message.success({ content: '属性上报成功' })
|
|
||||||
// 延迟刷新设备消息列表
|
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay)
|
|
||||||
} catch (error) {
|
|
||||||
message.error({ content: '属性上报失败' })
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 事件上报
|
|
||||||
const handleEventPost = async (row: ThingModelData) => {
|
|
||||||
try {
|
|
||||||
const valueStr = formData.value[row.identifier!]
|
|
||||||
let params: any = {}
|
|
||||||
|
|
||||||
if (valueStr) {
|
|
||||||
try {
|
|
||||||
params = JSON.parse(valueStr)
|
|
||||||
} catch (e) {
|
|
||||||
message.error({ content: '事件参数格式错误,请输入有效的JSON格式' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await DeviceApi.sendDeviceMessage({
|
|
||||||
deviceId: props.device.id!,
|
|
||||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method,
|
|
||||||
params: {
|
|
||||||
identifier: row.identifier,
|
|
||||||
params
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
message.success({ content: '事件上报成功' })
|
|
||||||
// 延迟刷新设备消息列表
|
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay)
|
|
||||||
} catch (error) {
|
|
||||||
message.error({ content: '事件上报失败' })
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态变更
|
|
||||||
const handleDeviceState = async (state: number) => {
|
|
||||||
try {
|
|
||||||
await DeviceApi.sendDeviceMessage({
|
|
||||||
deviceId: props.device.id!,
|
|
||||||
method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
|
|
||||||
params: { state }
|
|
||||||
})
|
|
||||||
|
|
||||||
message.success({ content: '状态变更成功' })
|
|
||||||
// 延迟刷新设备消息列表
|
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay)
|
|
||||||
} catch (error) {
|
|
||||||
message.error({ content: '状态变更失败' })
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 属性设置
|
|
||||||
const handlePropertySet = async () => {
|
|
||||||
try {
|
|
||||||
const params: Record<string, any> = {}
|
|
||||||
propertyList.value.forEach((item) => {
|
|
||||||
const value = formData.value[item.identifier!]
|
|
||||||
if (value) {
|
|
||||||
params[item.identifier!] = value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Object.keys(params).length === 0) {
|
|
||||||
message.warning({ content: '请至少输入一个属性值' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await DeviceApi.sendDeviceMessage({
|
|
||||||
deviceId: props.device.id!,
|
|
||||||
method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
|
|
||||||
params
|
|
||||||
})
|
|
||||||
|
|
||||||
message.success({ content: '属性设置成功' })
|
|
||||||
// 延迟刷新设备消息列表
|
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay)
|
|
||||||
} catch (error) {
|
|
||||||
message.error({ content: '属性设置失败' })
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务调用
|
|
||||||
const handleServiceInvoke = async (row: ThingModelData) => {
|
|
||||||
try {
|
|
||||||
const valueStr = formData.value[row.identifier!]
|
|
||||||
let params: any = {}
|
|
||||||
|
|
||||||
if (valueStr) {
|
|
||||||
try {
|
|
||||||
params = JSON.parse(valueStr)
|
|
||||||
} catch (e) {
|
|
||||||
message.error({ content: '服务参数格式错误,请输入有效的JSON格式' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await DeviceApi.sendDeviceMessage({
|
|
||||||
deviceId: props.device.id!,
|
|
||||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
|
|
||||||
params: {
|
|
||||||
identifier: row.identifier,
|
|
||||||
params
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
message.success({ content: '服务调用成功' })
|
|
||||||
// 延迟刷新设备消息列表
|
|
||||||
deviceMessageRef.value?.refresh(deviceMessageRefreshDelay)
|
|
||||||
} catch (error) {
|
|
||||||
message.error({ content: '服务调用失败' })
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
<!-- 设备物模型:设备属性、事件管理、服务调用 -->
|
<!-- 设备物模型:设备属性、事件管理、服务调用 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { ContentWrap } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue';
|
||||||
|
import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue';
|
||||||
|
import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
deviceId: number;
|
||||||
|
thingModelList: ThingModelData[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const activeTab = ref('property'); // 默认选中设备属性
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<a-tabs v-model:active-key="activeTab" class="thing-model-tabs">
|
<a-tabs v-model:active-key="activeTab" class="thing-model-tabs">
|
||||||
@@ -20,21 +38,6 @@
|
|||||||
</a-tabs>
|
</a-tabs>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { ContentWrap } from '@vben/common-ui'
|
|
||||||
import type { ThingModelData } from '#/api/iot/thingmodel'
|
|
||||||
import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue'
|
|
||||||
import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue'
|
|
||||||
import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
deviceId: number
|
|
||||||
thingModelList: ThingModelData[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const activeTab = ref('property') // 默认选中设备属性
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.thing-model-tabs :deep(.ant-tabs-content) {
|
.thing-model-tabs :deep(.ant-tabs-content) {
|
||||||
|
|||||||
@@ -1,4 +1,113 @@
|
|||||||
<!-- 设备事件管理 -->
|
<!-- 设备事件管理 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ContentWrap } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Pagination } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
import {
|
||||||
|
getEventTypeLabel,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
deviceId: number;
|
||||||
|
thingModelList: ThingModelData[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false); // 列表的加载中
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const list = ref([] as any[]); // 列表的数据
|
||||||
|
const queryParams = reactive({
|
||||||
|
deviceId: props.deviceId,
|
||||||
|
method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 固定筛选事件消息
|
||||||
|
identifier: '',
|
||||||
|
times: [] as any[],
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
|
||||||
|
/** 事件类型的物模型数据 */
|
||||||
|
const eventThingModels = computed(() => {
|
||||||
|
return props.thingModelList.filter(
|
||||||
|
(item: ThingModelData) =>
|
||||||
|
String(item.type) === String(IoTThingModelTypeEnum.EVENT),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
if (!props.deviceId) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await DeviceApi.getDeviceMessagePairPage(queryParams);
|
||||||
|
list.value = data.list || [];
|
||||||
|
total.value = data.total || 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
queryParams.identifier = '';
|
||||||
|
queryParams.times = [];
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取事件名称 */
|
||||||
|
const getEventName = (identifier: string | undefined) => {
|
||||||
|
if (!identifier) return '-';
|
||||||
|
const event = eventThingModels.value.find(
|
||||||
|
(item: ThingModelData) => item.identifier === identifier,
|
||||||
|
);
|
||||||
|
return event?.name || identifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取事件类型 */
|
||||||
|
const getEventType = (identifier: string | undefined) => {
|
||||||
|
if (!identifier) return '-';
|
||||||
|
const event = eventThingModels.value.find(
|
||||||
|
(item: ThingModelData) => item.identifier === identifier,
|
||||||
|
);
|
||||||
|
if (!event?.event?.type) return '-';
|
||||||
|
return getEventTypeLabel(event.event.type) || '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 解析参数 */
|
||||||
|
const parseParams = (params: string) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(params);
|
||||||
|
if (parsed.params) {
|
||||||
|
return parsed.params;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
@@ -7,14 +116,14 @@
|
|||||||
ref="queryFormRef"
|
ref="queryFormRef"
|
||||||
layout="inline"
|
layout="inline"
|
||||||
@submit.prevent
|
@submit.prevent
|
||||||
style="margin-bottom: 16px;"
|
style="margin-bottom: 16px"
|
||||||
>
|
>
|
||||||
<a-form-item label="标识符" name="identifier">
|
<a-form-item label="标识符" name="identifier">
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="queryParams.identifier"
|
v-model:value="queryParams.identifier"
|
||||||
placeholder="请选择事件标识符"
|
placeholder="请选择事件标识符"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 240px;"
|
style="width: 240px"
|
||||||
>
|
>
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-for="event in eventThingModels"
|
v-for="event in eventThingModels"
|
||||||
@@ -30,7 +139,7 @@
|
|||||||
v-model:value="queryParams.times"
|
v-model:value="queryParams.times"
|
||||||
show-time
|
show-time
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
style="width: 360px;"
|
style="width: 360px"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
@@ -40,7 +149,7 @@
|
|||||||
</template>
|
</template>
|
||||||
搜索
|
搜索
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="resetQuery" style="margin-left: 8px;">
|
<a-button @click="resetQuery" style="margin-left: 8px">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconifyIcon icon="ep:refresh" />
|
<IconifyIcon icon="ep:refresh" />
|
||||||
</template>
|
</template>
|
||||||
@@ -49,34 +158,60 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<a-divider style="margin: 16px 0;" />
|
<a-divider style="margin: 16px 0" />
|
||||||
|
|
||||||
<!-- 事件列表 -->
|
<!-- 事件列表 -->
|
||||||
<a-table v-loading="loading" :data-source="list" :pagination="false">
|
<a-table v-loading="loading" :data-source="list" :pagination="false">
|
||||||
<a-table-column title="上报时间" align="center" data-index="reportTime" :width="180">
|
<a-table-column
|
||||||
|
title="上报时间"
|
||||||
|
align="center"
|
||||||
|
data-index="reportTime"
|
||||||
|
:width="180"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
{{ record.request?.reportTime ? formatDate(record.request.reportTime) : '-' }}
|
{{
|
||||||
|
record.request?.reportTime
|
||||||
|
? formatDate(record.request.reportTime)
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="标识符" align="center" data-index="identifier" :width="160">
|
<a-table-column
|
||||||
|
title="标识符"
|
||||||
|
align="center"
|
||||||
|
data-index="identifier"
|
||||||
|
:width="160"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
<a-tag color="blue" size="small">
|
<a-tag color="blue" size="small">
|
||||||
{{ record.request?.identifier }}
|
{{ record.request?.identifier }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="事件名称" align="center" data-index="eventName" :width="160">
|
<a-table-column
|
||||||
|
title="事件名称"
|
||||||
|
align="center"
|
||||||
|
data-index="eventName"
|
||||||
|
:width="160"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
{{ getEventName(record.request?.identifier) }}
|
{{ getEventName(record.request?.identifier) }}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="事件类型" align="center" data-index="eventType" :width="100">
|
<a-table-column
|
||||||
|
title="事件类型"
|
||||||
|
align="center"
|
||||||
|
data-index="eventType"
|
||||||
|
:width="100"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
{{ getEventType(record.request?.identifier) }}
|
{{ getEventType(record.request?.identifier) }}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="输入参数" align="center" data-index="params">
|
<a-table-column title="输入参数" align="center" data-index="params">
|
||||||
<template #default="{ record }"> {{ parseParams(record.request.params) }} </template>
|
<template #default="{ record }">
|
||||||
|
{{ parseParams(record.request.params) }}
|
||||||
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
|
||||||
@@ -89,108 +224,3 @@
|
|||||||
/>
|
/>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
|
||||||
import { Pagination } from 'ant-design-vue'
|
|
||||||
import { ContentWrap } from '@vben/common-ui'
|
|
||||||
import { IconifyIcon } from '@vben/icons'
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import type { ThingModelData } from '#/api/iot/thingmodel'
|
|
||||||
import { formatDate } from '@vben/utils'
|
|
||||||
import {
|
|
||||||
getEventTypeLabel,
|
|
||||||
IotDeviceMessageMethodEnum,
|
|
||||||
IoTThingModelTypeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
deviceId: number
|
|
||||||
thingModelList: ThingModelData[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const loading = ref(false) // 列表的加载中
|
|
||||||
const total = ref(0) // 列表的总页数
|
|
||||||
const list = ref([] as any[]) // 列表的数据
|
|
||||||
const queryParams = reactive({
|
|
||||||
deviceId: props.deviceId,
|
|
||||||
method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 固定筛选事件消息
|
|
||||||
identifier: '',
|
|
||||||
times: [] as any[],
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 10
|
|
||||||
})
|
|
||||||
const queryFormRef = ref() // 搜索的表单
|
|
||||||
|
|
||||||
/** 事件类型的物模型数据 */
|
|
||||||
const eventThingModels = computed(() => {
|
|
||||||
return props.thingModelList.filter(
|
|
||||||
(item: ThingModelData) => String(item.type) === String(IoTThingModelTypeEnum.EVENT)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 查询列表 */
|
|
||||||
const getList = async () => {
|
|
||||||
if (!props.deviceId) return
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
|
|
||||||
list.value = data.list || []
|
|
||||||
total.value = data.total || 0
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 搜索按钮操作 */
|
|
||||||
const handleQuery = () => {
|
|
||||||
queryParams.pageNo = 1
|
|
||||||
getList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 重置按钮操作 */
|
|
||||||
const resetQuery = () => {
|
|
||||||
queryFormRef.value?.resetFields()
|
|
||||||
queryParams.identifier = ''
|
|
||||||
queryParams.times = []
|
|
||||||
handleQuery()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取事件名称 */
|
|
||||||
const getEventName = (identifier: string | undefined) => {
|
|
||||||
if (!identifier) return '-'
|
|
||||||
const event = eventThingModels.value.find(
|
|
||||||
(item: ThingModelData) => item.identifier === identifier
|
|
||||||
)
|
|
||||||
return event?.name || identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取事件类型 */
|
|
||||||
const getEventType = (identifier: string | undefined) => {
|
|
||||||
if (!identifier) return '-'
|
|
||||||
const event = eventThingModels.value.find(
|
|
||||||
(item: ThingModelData) => item.identifier === identifier
|
|
||||||
)
|
|
||||||
if (!event?.event?.type) return '-'
|
|
||||||
return getEventTypeLabel(event.event.type) || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 解析参数 */
|
|
||||||
const parseParams = (params: string) => {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(params)
|
|
||||||
if (parsed.params) {
|
|
||||||
return parsed.params
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
} catch (error) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(() => {
|
|
||||||
getList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,143 @@
|
|||||||
<!-- 设备属性管理 -->
|
<!-- 设备属性管理 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IotDevicePropertyDetailRespVO } from '#/api/iot/device/device';
|
||||||
|
|
||||||
|
import { onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { ContentWrap } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
|
import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ deviceId: number }>();
|
||||||
|
|
||||||
|
const loading = ref(true); // 列表的加载中
|
||||||
|
const list = ref<IotDevicePropertyDetailRespVO[]>([]); // 显示的列表数据
|
||||||
|
const filterList = ref<IotDevicePropertyDetailRespVO[]>([]); // 完整的数据列表
|
||||||
|
const queryParams = reactive({
|
||||||
|
keyword: '' as string,
|
||||||
|
});
|
||||||
|
const autoRefresh = ref(false); // 自动刷新开关
|
||||||
|
let autoRefreshTimer: any = null; // 定时器
|
||||||
|
const viewMode = ref<'card' | 'list'>('card'); // 视图模式状态
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
deviceId: props.deviceId,
|
||||||
|
identifier: undefined as string | undefined,
|
||||||
|
name: undefined as string | undefined,
|
||||||
|
};
|
||||||
|
filterList.value = await DeviceApi.getLatestDeviceProperties(params);
|
||||||
|
handleFilter();
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 前端筛选数据 */
|
||||||
|
const handleFilter = () => {
|
||||||
|
if (queryParams.keyword.trim()) {
|
||||||
|
const keyword = queryParams.keyword.toLowerCase();
|
||||||
|
list.value = filterList.value.filter(
|
||||||
|
(item: IotDevicePropertyDetailRespVO) =>
|
||||||
|
item.identifier?.toLowerCase().includes(keyword) ||
|
||||||
|
item.name?.toLowerCase().includes(keyword),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
list.value = filterList.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
handleFilter();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 历史操作 */
|
||||||
|
const historyRef = ref();
|
||||||
|
const openHistory = (
|
||||||
|
deviceId: number,
|
||||||
|
identifier: string,
|
||||||
|
dataType: string,
|
||||||
|
) => {
|
||||||
|
historyRef.value.open(deviceId, identifier, dataType);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 格式化属性值和单位 */
|
||||||
|
const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => {
|
||||||
|
if (item.value === null || item.value === undefined || item.value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
const unitName = item.dataSpecs?.unitName;
|
||||||
|
return unitName ? `${item.value} ${unitName}` : item.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听自动刷新 */
|
||||||
|
watch(autoRefresh, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
autoRefreshTimer = setInterval(() => {
|
||||||
|
getList();
|
||||||
|
}, 5000); // 每 5 秒刷新一次
|
||||||
|
} else {
|
||||||
|
clearInterval(autoRefreshTimer);
|
||||||
|
autoRefreshTimer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 组件卸载时清除定时器 */
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (autoRefreshTimer) {
|
||||||
|
clearInterval(autoRefreshTimer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
<div class="flex items-center justify-between" style="margin-bottom: 16px;">
|
<div class="flex items-center justify-between" style="margin-bottom: 16px">
|
||||||
<div class="flex items-center" style="gap: 16px;">
|
<div class="flex items-center" style="gap: 16px">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="queryParams.keyword"
|
v-model:value="queryParams.keyword"
|
||||||
placeholder="请输入属性名称、标识符"
|
placeholder="请输入属性名称、标识符"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 240px;"
|
style="width: 240px"
|
||||||
@pressEnter="handleQuery"
|
@press-enter="handleQuery"
|
||||||
/>
|
|
||||||
<div class="flex items-center" style="gap: 8px;">
|
|
||||||
<span style="font-size: 14px; color: #666;">自动刷新</span>
|
|
||||||
<a-switch
|
|
||||||
v-model:checked="autoRefresh"
|
|
||||||
size="small"
|
|
||||||
/>
|
/>
|
||||||
|
<div class="flex items-center" style="gap: 8px">
|
||||||
|
<span style="font-size: 14px; color: #666">自动刷新</span>
|
||||||
|
<a-switch v-model:checked="autoRefresh" size="small" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a-button-group>
|
<a-button-group>
|
||||||
<a-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
|
<a-button
|
||||||
|
:type="viewMode === 'card' ? 'primary' : 'default'"
|
||||||
|
@click="viewMode = 'card'"
|
||||||
|
>
|
||||||
<IconifyIcon icon="ep:grid" />
|
<IconifyIcon icon="ep:grid" />
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
|
<a-button
|
||||||
|
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
>
|
||||||
<IconifyIcon icon="ep:list" />
|
<IconifyIcon icon="ep:list" />
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-button-group>
|
</a-button-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分隔线 -->
|
<!-- 分隔线 -->
|
||||||
<a-divider style="margin: 16px 0;" />
|
<a-divider style="margin: 16px 0" />
|
||||||
|
|
||||||
<!-- 卡片视图 -->
|
<!-- 卡片视图 -->
|
||||||
<template v-if="viewMode === 'card'">
|
<template v-if="viewMode === 'card'">
|
||||||
@@ -45,53 +152,60 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
>
|
>
|
||||||
<a-card
|
<a-card
|
||||||
class="h-full transition-colors relative overflow-hidden"
|
class="relative h-full overflow-hidden transition-colors"
|
||||||
:body-style="{ padding: '0' }"
|
:body-style="{ padding: '0' }"
|
||||||
>
|
>
|
||||||
<!-- 添加渐变背景层 -->
|
<!-- 添加渐变背景层 -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none bg-gradient-to-b from-[#eefaff] to-transparent"
|
class="pointer-events-none absolute left-0 right-0 top-0 h-[50px] bg-gradient-to-b from-[#eefaff] to-transparent"
|
||||||
>
|
></div>
|
||||||
</div>
|
<div class="relative p-4">
|
||||||
<div class="p-4 relative">
|
|
||||||
<!-- 标题区域 -->
|
<!-- 标题区域 -->
|
||||||
<div class="flex items-center mb-3">
|
<div class="mb-3 flex items-center">
|
||||||
<div class="mr-2.5 flex items-center">
|
<div class="mr-2.5 flex items-center">
|
||||||
<IconifyIcon icon="ep:cpu" class="text-[18px] text-[#0070ff]" />
|
<IconifyIcon
|
||||||
|
icon="ep:cpu"
|
||||||
|
class="text-[18px] text-[#0070ff]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[16px] font-600 flex-1">{{ item.name }}</div>
|
<div class="font-600 flex-1 text-[16px]">{{ item.name }}</div>
|
||||||
<!-- 标识符 -->
|
<!-- 标识符 -->
|
||||||
<div class="inline-flex items-center mr-2">
|
<div class="mr-2 inline-flex items-center">
|
||||||
<a-tag size="small" color="blue">
|
<a-tag size="small" color="blue">
|
||||||
{{ item.identifier }}
|
{{ item.identifier }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
<!-- 数据类型标签 -->
|
<!-- 数据类型标签 -->
|
||||||
<div class="inline-flex items-center mr-2">
|
<div class="mr-2 inline-flex items-center">
|
||||||
<a-tag size="small">
|
<a-tag size="small">
|
||||||
{{ item.dataType }}
|
{{ item.dataType }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
<!-- 数据图标 - 可点击 -->
|
<!-- 数据图标 - 可点击 -->
|
||||||
<div
|
<div
|
||||||
class="cursor-pointer flex items-center justify-center w-8 h-8 rounded-full hover:bg-blue-50 transition-colors"
|
class="flex h-8 w-8 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-blue-50"
|
||||||
@click="openHistory(props.deviceId, item.identifier, item.dataType)"
|
@click="
|
||||||
|
openHistory(props.deviceId, item.identifier, item.dataType)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="ep:data-line" class="text-[18px] text-[#0070ff]" />
|
<IconifyIcon
|
||||||
|
icon="ep:data-line"
|
||||||
|
class="text-[18px] text-[#0070ff]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 信息区域 -->
|
<!-- 信息区域 -->
|
||||||
<div class="text-[14px]">
|
<div class="text-[14px]">
|
||||||
<div class="mb-2.5 last:mb-0">
|
<div class="mb-2.5 last:mb-0">
|
||||||
<span class="text-[#717c8e] mr-2.5">属性值</span>
|
<span class="mr-2.5 text-[#717c8e]">属性值</span>
|
||||||
<span class="text-[#0b1d30] font-600">
|
<span class="font-600 text-[#0b1d30]">
|
||||||
{{ formatValueWithUnit(item) }}
|
{{ formatValueWithUnit(item) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2.5 last:mb-0">
|
<div class="mb-2.5 last:mb-0">
|
||||||
<span class="text-[#717c8e] mr-2.5">更新时间</span>
|
<span class="mr-2.5 text-[#717c8e]">更新时间</span>
|
||||||
<span class="text-[#0b1d30] text-[12px]">
|
<span class="text-[12px] text-[#0b1d30]">
|
||||||
{{ item.updateTime ? formatDate(item.updateTime) : '-' }}
|
{{ item.updateTime ? formatDate(item.updateTime) : '-' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +218,11 @@
|
|||||||
|
|
||||||
<!-- 列表视图 -->
|
<!-- 列表视图 -->
|
||||||
<a-table v-else v-loading="loading" :data-source="list" :pagination="false">
|
<a-table v-else v-loading="loading" :data-source="list" :pagination="false">
|
||||||
<a-table-column title="属性标识符" align="center" data-index="identifier" />
|
<a-table-column
|
||||||
|
title="属性标识符"
|
||||||
|
align="center"
|
||||||
|
data-index="identifier"
|
||||||
|
/>
|
||||||
<a-table-column title="属性名称" align="center" data-index="name" />
|
<a-table-column title="属性名称" align="center" data-index="name" />
|
||||||
<a-table-column title="数据类型" align="center" data-index="dataType" />
|
<a-table-column title="数据类型" align="center" data-index="dataType" />
|
||||||
<a-table-column title="属性值" align="center" data-index="value">
|
<a-table-column title="属性值" align="center" data-index="value">
|
||||||
@@ -126,7 +244,9 @@
|
|||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
<a-button
|
<a-button
|
||||||
type="link"
|
type="link"
|
||||||
@click="openHistory(props.deviceId, record.identifier, record.dataType)"
|
@click="
|
||||||
|
openHistory(props.deviceId, record.identifier, record.dataType)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
查看数据
|
查看数据
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -135,10 +255,12 @@
|
|||||||
</a-table>
|
</a-table>
|
||||||
|
|
||||||
<!-- 表单弹窗:添加/修改 -->
|
<!-- 表单弹窗:添加/修改 -->
|
||||||
<DeviceDetailsThingModelPropertyHistory ref="historyRef" :deviceId="props.deviceId" />
|
<DeviceDetailsThingModelPropertyHistory
|
||||||
|
ref="historyRef"
|
||||||
|
:device-id="props.deviceId"
|
||||||
|
/>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 移除 a-row 的额外边距 */
|
/* 移除 a-row 的额外边距 */
|
||||||
:deep(.ant-row) {
|
:deep(.ant-row) {
|
||||||
@@ -146,98 +268,3 @@
|
|||||||
margin-right: -8px !important;
|
margin-right: -8px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
||||||
import { ContentWrap } from '@vben/common-ui'
|
|
||||||
import { IconifyIcon } from '@vben/icons'
|
|
||||||
import { DeviceApi, type IotDevicePropertyDetailRespVO } from '#/api/iot/device/device'
|
|
||||||
import { formatDate } from '@vben/utils'
|
|
||||||
import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{ deviceId: number }>()
|
|
||||||
|
|
||||||
const loading = ref(true) // 列表的加载中
|
|
||||||
const list = ref<IotDevicePropertyDetailRespVO[]>([]) // 显示的列表数据
|
|
||||||
const filterList = ref<IotDevicePropertyDetailRespVO[]>([]) // 完整的数据列表
|
|
||||||
const queryParams = reactive({
|
|
||||||
keyword: '' as string
|
|
||||||
})
|
|
||||||
const autoRefresh = ref(false) // 自动刷新开关
|
|
||||||
let autoRefreshTimer: any = null // 定时器
|
|
||||||
const viewMode = ref<'card' | 'list'>('card') // 视图模式状态
|
|
||||||
|
|
||||||
/** 查询列表 */
|
|
||||||
const getList = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const params = {
|
|
||||||
deviceId: props.deviceId,
|
|
||||||
identifier: undefined as string | undefined,
|
|
||||||
name: undefined as string | undefined
|
|
||||||
}
|
|
||||||
filterList.value = await DeviceApi.getLatestDeviceProperties(params)
|
|
||||||
handleFilter()
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 前端筛选数据 */
|
|
||||||
const handleFilter = () => {
|
|
||||||
if (!queryParams.keyword.trim()) {
|
|
||||||
list.value = filterList.value
|
|
||||||
} else {
|
|
||||||
const keyword = queryParams.keyword.toLowerCase()
|
|
||||||
list.value = filterList.value.filter(
|
|
||||||
(item: IotDevicePropertyDetailRespVO) =>
|
|
||||||
item.identifier?.toLowerCase().includes(keyword) ||
|
|
||||||
item.name?.toLowerCase().includes(keyword)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 搜索按钮操作 */
|
|
||||||
const handleQuery = () => {
|
|
||||||
handleFilter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 历史操作 */
|
|
||||||
const historyRef = ref()
|
|
||||||
const openHistory = (deviceId: number, identifier: string, dataType: string) => {
|
|
||||||
historyRef.value.open(deviceId, identifier, dataType)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 格式化属性值和单位 */
|
|
||||||
const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => {
|
|
||||||
if (item.value === null || item.value === undefined || item.value === '') {
|
|
||||||
return '-'
|
|
||||||
}
|
|
||||||
const unitName = item.dataSpecs?.unitName
|
|
||||||
return unitName ? `${item.value} ${unitName}` : item.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听自动刷新 */
|
|
||||||
watch(autoRefresh, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
autoRefreshTimer = setInterval(() => {
|
|
||||||
getList()
|
|
||||||
}, 5000) // 每 5 秒刷新一次
|
|
||||||
} else {
|
|
||||||
clearInterval(autoRefreshTimer)
|
|
||||||
autoRefreshTimer = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 组件卸载时清除定时器 */
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (autoRefreshTimer) {
|
|
||||||
clearInterval(autoRefreshTimer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 初始化 **/
|
|
||||||
onMounted(() => {
|
|
||||||
getList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,374 @@
|
|||||||
<!-- 设备物模型 -> 运行状态 -> 查看数据(设备的属性值历史)-->
|
<!-- 设备物模型 -> 运行状态 -> 查看数据(设备的属性值历史)-->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import type { IotDevicePropertyRespVO } from '#/api/iot/device/device';
|
||||||
|
|
||||||
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
import { beginOfDay, endOfDay, formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Empty, message, Modal } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** IoT 设备属性历史数据详情 */
|
||||||
|
defineOptions({ name: 'DeviceDetailsThingModelPropertyHistory' });
|
||||||
|
|
||||||
|
defineProps<{ deviceId: number }>();
|
||||||
|
|
||||||
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
|
const loading = ref(false);
|
||||||
|
const exporting = ref(false);
|
||||||
|
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||||
|
const list = ref<IotDevicePropertyRespVO[]>([]); // 列表的数据
|
||||||
|
const total = ref(0); // 总数据量
|
||||||
|
const thingModelDataType = ref<string>(''); // 物模型数据类型
|
||||||
|
const propertyIdentifier = ref<string>(''); // 属性标识符
|
||||||
|
const dateRange = ref<[Dayjs, Dayjs]>([
|
||||||
|
dayjs().subtract(7, 'day').startOf('day'),
|
||||||
|
dayjs().endOf('day'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
deviceId: -1,
|
||||||
|
identifier: '',
|
||||||
|
times: [
|
||||||
|
formatDate(beginOfDay(new Date(Date.now() - 3600 * 1000 * 24 * 7))),
|
||||||
|
formatDate(endOfDay(new Date())),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Echarts 相关
|
||||||
|
const chartRef = ref<EchartsUIType>();
|
||||||
|
const { renderEcharts } = useEcharts(chartRef);
|
||||||
|
|
||||||
|
// 判断是否为复杂数据类型(struct 或 array)
|
||||||
|
const isComplexDataType = computed(() => {
|
||||||
|
if (!thingModelDataType.value) return false;
|
||||||
|
return [
|
||||||
|
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||||
|
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||||
|
].includes(thingModelDataType.value as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const maxValue = computed(() => {
|
||||||
|
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||||
|
const values = list.value
|
||||||
|
.map((item) => Number(item.value))
|
||||||
|
.filter((v) => !isNaN(v));
|
||||||
|
return values.length > 0 ? Math.max(...values).toFixed(2) : '-';
|
||||||
|
});
|
||||||
|
|
||||||
|
const minValue = computed(() => {
|
||||||
|
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||||
|
const values = list.value
|
||||||
|
.map((item) => Number(item.value))
|
||||||
|
.filter((v) => !isNaN(v));
|
||||||
|
return values.length > 0 ? Math.min(...values).toFixed(2) : '-';
|
||||||
|
});
|
||||||
|
|
||||||
|
const avgValue = computed(() => {
|
||||||
|
if (isComplexDataType.value || list.value.length === 0) return '-';
|
||||||
|
const values = list.value
|
||||||
|
.map((item) => Number(item.value))
|
||||||
|
.filter((v) => !isNaN(v));
|
||||||
|
if (values.length === 0) return '-';
|
||||||
|
const sum = values.reduce((acc, val) => acc + val, 0);
|
||||||
|
return (sum / values.length).toFixed(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const tableColumns = computed(() => [
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
key: 'index',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
customRender: ({ index }: { index: number }) => index + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
key: 'updateTime',
|
||||||
|
dataIndex: 'updateTime',
|
||||||
|
width: 200,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '属性值',
|
||||||
|
key: 'value',
|
||||||
|
dataIndex: 'value',
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const paginationConfig = computed(() => ({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
total: total.value,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
|
showTotal: (total: number) => `共 ${total} 条数据`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
/** 获得设备历史数据 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await DeviceApi.getHistoryDevicePropertyList(queryParams);
|
||||||
|
list.value = data?.list || [];
|
||||||
|
total.value = list.value.length;
|
||||||
|
|
||||||
|
// 如果是图表模式且不是复杂数据类型,渲染图表
|
||||||
|
if (
|
||||||
|
viewMode.value === 'chart' &&
|
||||||
|
!isComplexDataType.value &&
|
||||||
|
list.value.length > 0
|
||||||
|
) {
|
||||||
|
await nextTick();
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
message.error('获取数据失败');
|
||||||
|
list.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 渲染图表 */
|
||||||
|
const renderChart = () => {
|
||||||
|
if (!list.value || list.value.length === 0) return;
|
||||||
|
|
||||||
|
const chartData = list.value.map((item) => [item.updateTime, item.value]);
|
||||||
|
|
||||||
|
renderEcharts({
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
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')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${param.color};"></span>
|
||||||
|
<span>属性值: <strong>${param.value[1]}</strong></span>
|
||||||
|
</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',
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (deviceId: number, identifier: string, dataType: string) => {
|
||||||
|
dialogVisible.value = true;
|
||||||
|
queryParams.deviceId = deviceId;
|
||||||
|
queryParams.identifier = identifier;
|
||||||
|
propertyIdentifier.value = identifier;
|
||||||
|
thingModelDataType.value = dataType;
|
||||||
|
|
||||||
|
// 如果物模型是 struct、array,需要默认使用 list 模式
|
||||||
|
viewMode.value = isComplexDataType.value ? 'list' : 'chart';
|
||||||
|
|
||||||
|
// 等待弹窗完全渲染后再获取数据
|
||||||
|
await nextTick();
|
||||||
|
await getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 时间变化处理 */
|
||||||
|
const handleTimeChange = () => {
|
||||||
|
if (!dateRange.value || dateRange.value.length !== 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams.times = [
|
||||||
|
formatDate(dateRange.value[0].toDate()),
|
||||||
|
formatDate(dateRange.value[1].toDate()),
|
||||||
|
];
|
||||||
|
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 刷新数据 */
|
||||||
|
const handleRefresh = () => {
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 导出数据 */
|
||||||
|
const handleExport = async () => {
|
||||||
|
if (list.value.length === 0) {
|
||||||
|
message.warning('暂无数据可导出');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
exporting.value = true;
|
||||||
|
try {
|
||||||
|
// 构建CSV内容
|
||||||
|
const headers = ['序号', '时间', '属性值'];
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...list.value.map((item, index) => {
|
||||||
|
return [
|
||||||
|
index + 1,
|
||||||
|
formatDate(new Date(item.updateTime)),
|
||||||
|
isComplexDataType.value
|
||||||
|
? `"${JSON.stringify(item.value)}"`
|
||||||
|
: item.value,
|
||||||
|
].join(',');
|
||||||
|
}),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
// 创建 BOM 头,解决中文乱码
|
||||||
|
const BOM = '\uFEFF';
|
||||||
|
const blob = new Blob([BOM + csvContent], {
|
||||||
|
type: 'text/csv;charset=utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `设备属性历史_${propertyIdentifier.value}_${formatDate(new Date(), 'YYYYMMDDHHmmss')}.csv`;
|
||||||
|
document.body.append(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
message.success('导出成功');
|
||||||
|
} catch {
|
||||||
|
message.error('导出失败');
|
||||||
|
} finally {
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 关闭弹窗 */
|
||||||
|
const handleClose = () => {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
list.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 格式化复杂数据类型 */
|
||||||
|
const formatComplexValue = (value: any) => {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听视图模式变化,重新渲染图表 */
|
||||||
|
watch(viewMode, async (newMode) => {
|
||||||
|
if (
|
||||||
|
newMode === 'chart' &&
|
||||||
|
!isComplexDataType.value &&
|
||||||
|
list.value.length > 0
|
||||||
|
) {
|
||||||
|
await nextTick();
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
v-model:open="dialogVisible"
|
v-model:open="dialogVisible"
|
||||||
@@ -30,7 +400,11 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<!-- 导出按钮 -->
|
<!-- 导出按钮 -->
|
||||||
<a-button @click="handleExport" :loading="exporting" :disabled="list.length === 0">
|
<a-button
|
||||||
|
@click="handleExport"
|
||||||
|
:loading="exporting"
|
||||||
|
:disabled="list.length === 0"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Icon icon="ant-design:export-outlined" />
|
<Icon icon="ant-design:export-outlined" />
|
||||||
</template>
|
</template>
|
||||||
@@ -49,7 +423,10 @@
|
|||||||
</template>
|
</template>
|
||||||
图表
|
图表
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
|
<a-button
|
||||||
|
:type="viewMode === 'list' ? 'primary' : 'default'"
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<Icon icon="ant-design:table-outlined" />
|
<Icon icon="ant-design:table-outlined" />
|
||||||
</template>
|
</template>
|
||||||
@@ -63,7 +440,8 @@
|
|||||||
<a-space :size="16">
|
<a-space :size="16">
|
||||||
<span>共 {{ total }} 条数据</span>
|
<span>共 {{ total }} 条数据</span>
|
||||||
<span v-if="viewMode === 'chart' && !isComplexDataType">
|
<span v-if="viewMode === 'chart' && !isComplexDataType">
|
||||||
最大值: {{ maxValue }} | 最小值: {{ minValue }} | 平均值: {{ avgValue }}
|
最大值: {{ maxValue }} | 最小值: {{ minValue }} | 平均值:
|
||||||
|
{{ avgValue }}
|
||||||
</span>
|
</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,7 +463,7 @@
|
|||||||
<!-- 表格模式 -->
|
<!-- 表格模式 -->
|
||||||
<div v-else class="table-container">
|
<div v-else class="table-container">
|
||||||
<a-table
|
<a-table
|
||||||
:dataSource="list"
|
:data-source="list"
|
||||||
:columns="tableColumns"
|
:columns="tableColumns"
|
||||||
:pagination="paginationConfig"
|
:pagination="paginationConfig"
|
||||||
:scroll="{ y: 500 }"
|
:scroll="{ y: 500 }"
|
||||||
@@ -113,358 +491,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
|
||||||
import type { EchartsUIType } from '@vben/plugins/echarts'
|
|
||||||
import type { IotDevicePropertyRespVO } from '#/api/iot/device/device'
|
|
||||||
|
|
||||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts'
|
|
||||||
import { beginOfDay, endOfDay, formatDate } from '@vben/utils'
|
|
||||||
|
|
||||||
import { Empty, message, Modal } from 'ant-design-vue'
|
|
||||||
import dayjs, { type Dayjs } from 'dayjs'
|
|
||||||
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
defineProps<{ deviceId: number }>()
|
|
||||||
|
|
||||||
/** IoT 设备属性历史数据详情 */
|
|
||||||
defineOptions({ name: 'DeviceDetailsThingModelPropertyHistory' })
|
|
||||||
|
|
||||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
|
||||||
const loading = ref(false)
|
|
||||||
const exporting = ref(false)
|
|
||||||
const viewMode = ref<'chart' | 'list'>('chart') // 视图模式状态
|
|
||||||
const list = ref<IotDevicePropertyRespVO[]>([]) // 列表的数据
|
|
||||||
const total = ref(0) // 总数据量
|
|
||||||
const thingModelDataType = ref<string>('') // 物模型数据类型
|
|
||||||
const propertyIdentifier = ref<string>('') // 属性标识符
|
|
||||||
const dateRange = ref<[Dayjs, Dayjs]>([
|
|
||||||
dayjs().subtract(7, 'day').startOf('day'),
|
|
||||||
dayjs().endOf('day')
|
|
||||||
])
|
|
||||||
|
|
||||||
const queryParams = reactive({
|
|
||||||
deviceId: -1,
|
|
||||||
identifier: '',
|
|
||||||
times: [
|
|
||||||
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
|
|
||||||
formatDate(endOfDay(new Date()))
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Echarts 相关
|
|
||||||
const chartRef = ref<EchartsUIType>()
|
|
||||||
const { renderEcharts } = useEcharts(chartRef)
|
|
||||||
|
|
||||||
// 判断是否为复杂数据类型(struct 或 array)
|
|
||||||
const isComplexDataType = computed(() => {
|
|
||||||
if (!thingModelDataType.value) return false
|
|
||||||
return [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY].includes(
|
|
||||||
thingModelDataType.value as any
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 统计数据
|
|
||||||
const maxValue = computed(() => {
|
|
||||||
if (isComplexDataType.value || list.value.length === 0) return '-'
|
|
||||||
const values = list.value.map((item) => Number(item.value)).filter((v) => !isNaN(v))
|
|
||||||
return values.length > 0 ? Math.max(...values).toFixed(2) : '-'
|
|
||||||
})
|
|
||||||
|
|
||||||
const minValue = computed(() => {
|
|
||||||
if (isComplexDataType.value || list.value.length === 0) return '-'
|
|
||||||
const values = list.value.map((item) => Number(item.value)).filter((v) => !isNaN(v))
|
|
||||||
return values.length > 0 ? Math.min(...values).toFixed(2) : '-'
|
|
||||||
})
|
|
||||||
|
|
||||||
const avgValue = computed(() => {
|
|
||||||
if (isComplexDataType.value || list.value.length === 0) return '-'
|
|
||||||
const values = list.value.map((item) => Number(item.value)).filter((v) => !isNaN(v))
|
|
||||||
if (values.length === 0) return '-'
|
|
||||||
const sum = values.reduce((acc, val) => acc + val, 0)
|
|
||||||
return (sum / values.length).toFixed(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 表格列配置
|
|
||||||
const tableColumns = computed(() => [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
key: 'index',
|
|
||||||
width: 80,
|
|
||||||
align: 'center',
|
|
||||||
customRender: ({ index }: { index: number }) => index + 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '时间',
|
|
||||||
key: 'updateTime',
|
|
||||||
dataIndex: 'updateTime',
|
|
||||||
width: 200,
|
|
||||||
align: 'center'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '属性值',
|
|
||||||
key: 'value',
|
|
||||||
dataIndex: 'value',
|
|
||||||
align: 'center'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 分页配置
|
|
||||||
const paginationConfig = computed(() => ({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
total: total.value,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showQuickJumper: true,
|
|
||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
|
||||||
showTotal: (total: number) => `共 ${total} 条数据`
|
|
||||||
}))
|
|
||||||
|
|
||||||
/** 获得设备历史数据 */
|
|
||||||
const getList = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await DeviceApi.getHistoryDevicePropertyList(queryParams)
|
|
||||||
list.value = data?.list || []
|
|
||||||
total.value = list.value.length
|
|
||||||
|
|
||||||
// 如果是图表模式且不是复杂数据类型,渲染图表
|
|
||||||
if (viewMode.value === 'chart' && !isComplexDataType.value && list.value.length > 0) {
|
|
||||||
await nextTick()
|
|
||||||
renderChart()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取数据失败')
|
|
||||||
list.value = []
|
|
||||||
total.value = 0
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 渲染图表 */
|
|
||||||
const renderChart = () => {
|
|
||||||
if (!list.value || list.value.length === 0) return
|
|
||||||
|
|
||||||
const chartData = list.value.map((item) => [item.updateTime, item.value])
|
|
||||||
|
|
||||||
renderEcharts({
|
|
||||||
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'
|
|
||||||
},
|
|
||||||
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')}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${param.color};"></span>
|
|
||||||
<span>属性值: <strong>${param.value[1]}</strong></span>
|
|
||||||
</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'
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打开弹窗 */
|
|
||||||
const open = async (deviceId: number, identifier: string, dataType: string) => {
|
|
||||||
dialogVisible.value = true
|
|
||||||
queryParams.deviceId = deviceId
|
|
||||||
queryParams.identifier = identifier
|
|
||||||
propertyIdentifier.value = identifier
|
|
||||||
thingModelDataType.value = dataType
|
|
||||||
|
|
||||||
// 如果物模型是 struct、array,需要默认使用 list 模式
|
|
||||||
if (isComplexDataType.value) {
|
|
||||||
viewMode.value = 'list'
|
|
||||||
} else {
|
|
||||||
viewMode.value = 'chart'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待弹窗完全渲染后再获取数据
|
|
||||||
await nextTick()
|
|
||||||
await getList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 时间变化处理 */
|
|
||||||
const handleTimeChange = () => {
|
|
||||||
if (!dateRange.value || dateRange.value.length !== 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
queryParams.times = [
|
|
||||||
formatDate(dateRange.value[0].toDate()),
|
|
||||||
formatDate(dateRange.value[1].toDate())
|
|
||||||
]
|
|
||||||
|
|
||||||
getList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 刷新数据 */
|
|
||||||
const handleRefresh = () => {
|
|
||||||
getList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 导出数据 */
|
|
||||||
const handleExport = async () => {
|
|
||||||
if (list.value.length === 0) {
|
|
||||||
message.warning('暂无数据可导出')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exporting.value = true
|
|
||||||
try {
|
|
||||||
// 构建CSV内容
|
|
||||||
const headers = ['序号', '时间', '属性值']
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...list.value.map((item, index) => {
|
|
||||||
return [
|
|
||||||
index + 1,
|
|
||||||
formatDate(new Date(item.updateTime)),
|
|
||||||
isComplexDataType.value ? `"${JSON.stringify(item.value)}"` : item.value
|
|
||||||
].join(',')
|
|
||||||
})
|
|
||||||
].join('\n')
|
|
||||||
|
|
||||||
// 创建 BOM 头,解决中文乱码
|
|
||||||
const BOM = '\uFEFF'
|
|
||||||
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8' })
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
const url = window.URL.createObjectURL(blob)
|
|
||||||
const link = document.createElement('a')
|
|
||||||
link.href = url
|
|
||||||
link.download = `设备属性历史_${propertyIdentifier.value}_${formatDate(new Date(), 'YYYYMMDDHHmmss')}.csv`
|
|
||||||
document.body.appendChild(link)
|
|
||||||
link.click()
|
|
||||||
document.body.removeChild(link)
|
|
||||||
window.URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
message.success('导出成功')
|
|
||||||
} catch (error) {
|
|
||||||
message.error('导出失败')
|
|
||||||
} finally {
|
|
||||||
exporting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 关闭弹窗 */
|
|
||||||
const handleClose = () => {
|
|
||||||
dialogVisible.value = false
|
|
||||||
list.value = []
|
|
||||||
total.value = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 格式化复杂数据类型 */
|
|
||||||
const formatComplexValue = (value: any) => {
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
return JSON.stringify(value)
|
|
||||||
}
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听视图模式变化,重新渲染图表 */
|
|
||||||
watch(viewMode, async (newMode) => {
|
|
||||||
if (newMode === 'chart' && !isComplexDataType.value && list.value.length > 0) {
|
|
||||||
await nextTick()
|
|
||||||
renderChart()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.property-history-container {
|
.property-history-container {
|
||||||
@@ -485,4 +511,3 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,114 @@
|
|||||||
<!-- 设备服务调用 -->
|
<!-- 设备服务调用 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { ContentWrap } from '@vben/common-ui';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Pagination } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
import {
|
||||||
|
getThingModelServiceCallTypeLabel,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
deviceId: number;
|
||||||
|
thingModelList: ThingModelData[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const loading = ref(false); // 列表的加载中
|
||||||
|
const total = ref(0); // 列表的总页数
|
||||||
|
const list = ref([] as any[]); // 列表的数据
|
||||||
|
const queryParams = reactive({
|
||||||
|
deviceId: props.deviceId,
|
||||||
|
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method, // 固定筛选服务调用消息
|
||||||
|
identifier: '',
|
||||||
|
times: [] as any[],
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
});
|
||||||
|
const queryFormRef = ref(); // 搜索的表单
|
||||||
|
|
||||||
|
/** 服务类型的物模型数据 */
|
||||||
|
const serviceThingModels = computed(() => {
|
||||||
|
return props.thingModelList.filter(
|
||||||
|
(item: ThingModelData) =>
|
||||||
|
String(item.type) === String(IoTThingModelTypeEnum.SERVICE),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
if (!props.deviceId) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await DeviceApi.getDeviceMessagePairPage(queryParams);
|
||||||
|
list.value = data.list;
|
||||||
|
total.value = data.total;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 搜索按钮操作 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1;
|
||||||
|
getList();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置按钮操作 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields();
|
||||||
|
queryParams.identifier = '';
|
||||||
|
queryParams.times = [];
|
||||||
|
handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取服务名称 */
|
||||||
|
const getServiceName = (identifier: string | undefined) => {
|
||||||
|
if (!identifier) return '-';
|
||||||
|
const service = serviceThingModels.value.find(
|
||||||
|
(item: ThingModelData) => item.identifier === identifier,
|
||||||
|
);
|
||||||
|
return service?.name || identifier;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取调用方式 */
|
||||||
|
const getCallType = (identifier: string | undefined) => {
|
||||||
|
if (!identifier) return '-';
|
||||||
|
const service = serviceThingModels.value.find(
|
||||||
|
(item: ThingModelData) => item.identifier === identifier,
|
||||||
|
);
|
||||||
|
if (!service?.service?.callType) return '-';
|
||||||
|
return getThingModelServiceCallTypeLabel(service.service.callType) || '-';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 解析参数 */
|
||||||
|
const parseParams = (params: string) => {
|
||||||
|
if (!params) return '-';
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(params);
|
||||||
|
if (parsed.params) {
|
||||||
|
return JSON.stringify(parsed.params, null, 2);
|
||||||
|
}
|
||||||
|
return JSON.stringify(parsed, null, 2);
|
||||||
|
} catch {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
getList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContentWrap>
|
<ContentWrap>
|
||||||
<!-- 搜索工作栏 -->
|
<!-- 搜索工作栏 -->
|
||||||
@@ -7,14 +117,14 @@
|
|||||||
ref="queryFormRef"
|
ref="queryFormRef"
|
||||||
layout="inline"
|
layout="inline"
|
||||||
@submit.prevent
|
@submit.prevent
|
||||||
style="margin-bottom: 16px;"
|
style="margin-bottom: 16px"
|
||||||
>
|
>
|
||||||
<a-form-item label="标识符" name="identifier">
|
<a-form-item label="标识符" name="identifier">
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="queryParams.identifier"
|
v-model:value="queryParams.identifier"
|
||||||
placeholder="请选择服务标识符"
|
placeholder="请选择服务标识符"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 240px;"
|
style="width: 240px"
|
||||||
>
|
>
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-for="service in serviceThingModels"
|
v-for="service in serviceThingModels"
|
||||||
@@ -30,7 +140,7 @@
|
|||||||
v-model:value="queryParams.times"
|
v-model:value="queryParams.times"
|
||||||
show-time
|
show-time
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
style="width: 360px;"
|
style="width: 360px"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
@@ -40,7 +150,7 @@
|
|||||||
</template>
|
</template>
|
||||||
搜索
|
搜索
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="resetQuery" style="margin-left: 8px;">
|
<a-button @click="resetQuery" style="margin-left: 8px">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconifyIcon icon="ep:refresh" />
|
<IconifyIcon icon="ep:refresh" />
|
||||||
</template>
|
</template>
|
||||||
@@ -49,39 +159,72 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<a-divider style="margin: 16px 0;" />
|
<a-divider style="margin: 16px 0" />
|
||||||
|
|
||||||
<!-- 服务调用列表 -->
|
<!-- 服务调用列表 -->
|
||||||
<a-table v-loading="loading" :data-source="list" :pagination="false">
|
<a-table v-loading="loading" :data-source="list" :pagination="false">
|
||||||
<a-table-column title="调用时间" align="center" data-index="requestTime" :width="180">
|
<a-table-column
|
||||||
|
title="调用时间"
|
||||||
|
align="center"
|
||||||
|
data-index="requestTime"
|
||||||
|
:width="180"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
{{ record.request?.reportTime ? formatDate(record.request.reportTime) : '-' }}
|
{{
|
||||||
|
record.request?.reportTime
|
||||||
|
? formatDate(record.request.reportTime)
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="响应时间" align="center" data-index="responseTime" :width="180">
|
<a-table-column
|
||||||
|
title="响应时间"
|
||||||
|
align="center"
|
||||||
|
data-index="responseTime"
|
||||||
|
:width="180"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
{{ record.reply?.reportTime ? formatDate(record.reply.reportTime) : '-' }}
|
{{
|
||||||
|
record.reply?.reportTime ? formatDate(record.reply.reportTime) : '-'
|
||||||
|
}}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="标识符" align="center" data-index="identifier" :width="160">
|
<a-table-column
|
||||||
|
title="标识符"
|
||||||
|
align="center"
|
||||||
|
data-index="identifier"
|
||||||
|
:width="160"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
<a-tag color="blue" size="small">
|
<a-tag color="blue" size="small">
|
||||||
{{ record.request?.identifier }}
|
{{ record.request?.identifier }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="服务名称" align="center" data-index="serviceName" :width="160">
|
<a-table-column
|
||||||
|
title="服务名称"
|
||||||
|
align="center"
|
||||||
|
data-index="serviceName"
|
||||||
|
:width="160"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
{{ getServiceName(record.request?.identifier) }}
|
{{ getServiceName(record.request?.identifier) }}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="调用方式" align="center" data-index="callType" :width="100">
|
<a-table-column
|
||||||
|
title="调用方式"
|
||||||
|
align="center"
|
||||||
|
data-index="callType"
|
||||||
|
:width="100"
|
||||||
|
>
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
{{ getCallType(record.request?.identifier) }}
|
{{ getCallType(record.request?.identifier) }}
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="输入参数" align="center" data-index="inputParams">
|
<a-table-column title="输入参数" align="center" data-index="inputParams">
|
||||||
<template #default="{ record }"> {{ parseParams(record.request?.params) }} </template>
|
<template #default="{ record }">
|
||||||
|
{{ parseParams(record.request?.params) }}
|
||||||
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="输出参数" align="center" data-index="outputParams">
|
<a-table-column title="输出参数" align="center" data-index="outputParams">
|
||||||
<template #default="{ record }">
|
<template #default="{ record }">
|
||||||
@@ -104,109 +247,3 @@
|
|||||||
/>
|
/>
|
||||||
</ContentWrap>
|
</ContentWrap>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
|
||||||
import { Pagination } from 'ant-design-vue'
|
|
||||||
import { ContentWrap } from '@vben/common-ui'
|
|
||||||
import { IconifyIcon } from '@vben/icons'
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import type { ThingModelData } from '#/api/iot/thingmodel'
|
|
||||||
import { formatDate } from '@vben/utils'
|
|
||||||
import {
|
|
||||||
getThingModelServiceCallTypeLabel,
|
|
||||||
IotDeviceMessageMethodEnum,
|
|
||||||
IoTThingModelTypeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
deviceId: number
|
|
||||||
thingModelList: ThingModelData[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const loading = ref(false) // 列表的加载中
|
|
||||||
const total = ref(0) // 列表的总页数
|
|
||||||
const list = ref([] as any[]) // 列表的数据
|
|
||||||
const queryParams = reactive({
|
|
||||||
deviceId: props.deviceId,
|
|
||||||
method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method, // 固定筛选服务调用消息
|
|
||||||
identifier: '',
|
|
||||||
times: [] as any[],
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 10
|
|
||||||
})
|
|
||||||
const queryFormRef = ref() // 搜索的表单
|
|
||||||
|
|
||||||
/** 服务类型的物模型数据 */
|
|
||||||
const serviceThingModels = computed(() => {
|
|
||||||
return props.thingModelList.filter(
|
|
||||||
(item: ThingModelData) => String(item.type) === String(IoTThingModelTypeEnum.SERVICE)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 查询列表 */
|
|
||||||
const getList = async () => {
|
|
||||||
if (!props.deviceId) return
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
|
|
||||||
list.value = data.list
|
|
||||||
total.value = data.total
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 搜索按钮操作 */
|
|
||||||
const handleQuery = () => {
|
|
||||||
queryParams.pageNo = 1
|
|
||||||
getList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 重置按钮操作 */
|
|
||||||
const resetQuery = () => {
|
|
||||||
queryFormRef.value?.resetFields()
|
|
||||||
queryParams.identifier = ''
|
|
||||||
queryParams.times = []
|
|
||||||
handleQuery()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取服务名称 */
|
|
||||||
const getServiceName = (identifier: string | undefined) => {
|
|
||||||
if (!identifier) return '-'
|
|
||||||
const service = serviceThingModels.value.find(
|
|
||||||
(item: ThingModelData) => item.identifier === identifier
|
|
||||||
)
|
|
||||||
return service?.name || identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取调用方式 */
|
|
||||||
const getCallType = (identifier: string | undefined) => {
|
|
||||||
if (!identifier) return '-'
|
|
||||||
const service = serviceThingModels.value.find(
|
|
||||||
(item: ThingModelData) => item.identifier === identifier
|
|
||||||
)
|
|
||||||
if (!service?.service?.callType) return '-'
|
|
||||||
return getThingModelServiceCallTypeLabel(service.service.callType) || '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 解析参数 */
|
|
||||||
const parseParams = (params: string) => {
|
|
||||||
if (!params) return '-'
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(params)
|
|
||||||
if (parsed.params) {
|
|
||||||
return JSON.stringify(parsed.params, null, 2)
|
|
||||||
}
|
|
||||||
return JSON.stringify(parsed, null, 2)
|
|
||||||
} catch (error) {
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
onMounted(() => {
|
|
||||||
getList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,79 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DeviceVO } from '#/api/iot/device/device';
|
||||||
|
import type { ProductVO } from '#/api/iot/product/product';
|
||||||
|
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { onMounted, ref, unref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useTabbarStore } from '@vben/stores';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
import { DeviceTypeEnum, ProductApi } from '#/api/iot/product/product';
|
||||||
|
import { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import DeviceDetailConfig from './DeviceDetailConfig.vue';
|
||||||
|
import DeviceDetailsHeader from './DeviceDetailsHeader.vue';
|
||||||
|
import DeviceDetailsInfo from './DeviceDetailsInfo.vue';
|
||||||
|
import DeviceDetailsMessage from './DeviceDetailsMessage.vue';
|
||||||
|
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue';
|
||||||
|
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'IoTDeviceDetail' });
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const id = Number(route.params.id); // 将字符串转换为数字
|
||||||
|
const loading = ref(true); // 加载中
|
||||||
|
const product = ref<ProductVO>({} as ProductVO); // 产品详情
|
||||||
|
const device = ref<DeviceVO>({} as DeviceVO); // 设备详情
|
||||||
|
const activeTab = ref('info'); // 默认激活的标签页
|
||||||
|
const thingModelList = ref<ThingModelData[]>([]); // 物模型列表数据
|
||||||
|
|
||||||
|
/** 获取设备详情 */
|
||||||
|
const getDeviceData = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
device.value = await DeviceApi.getDevice(id);
|
||||||
|
await getProductData(device.value.productId);
|
||||||
|
await getThingModelList(device.value.productId);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取产品详情 */
|
||||||
|
const getProductData = async (id: number) => {
|
||||||
|
product.value = await ProductApi.getProduct(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取物模型列表 */
|
||||||
|
const getThingModelList = async (productId: number) => {
|
||||||
|
try {
|
||||||
|
const data = await ThingModelApi.getThingModelList(productId);
|
||||||
|
thingModelList.value = data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取物模型列表失败:', error);
|
||||||
|
thingModelList.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
const tabbarStore = useTabbarStore(); // 视图操作
|
||||||
|
const router = useRouter(); // 路由
|
||||||
|
const { currentRoute } = router;
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!id) {
|
||||||
|
message.warning({ content: '参数错误,产品不能为空!' });
|
||||||
|
await tabbarStore.closeTab(unref(currentRoute), router);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getDeviceData();
|
||||||
|
activeTab.value = (route.query.tab as string) || 'info';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page>
|
||||||
<DeviceDetailsHeader
|
<DeviceDetailsHeader
|
||||||
@@ -9,7 +85,11 @@
|
|||||||
|
|
||||||
<a-tabs v-model:active-key="activeTab" class="device-detail-tabs mt-4">
|
<a-tabs v-model:active-key="activeTab" class="device-detail-tabs mt-4">
|
||||||
<a-tab-pane key="info" tab="设备信息">
|
<a-tab-pane key="info" tab="设备信息">
|
||||||
<DeviceDetailsInfo v-if="activeTab === 'info'" :product="product" :device="device" />
|
<DeviceDetailsInfo
|
||||||
|
v-if="activeTab === 'info'"
|
||||||
|
:product="product"
|
||||||
|
:device="device"
|
||||||
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="model" tab="物模型数据">
|
<a-tab-pane key="model" tab="物模型数据">
|
||||||
<DeviceDetailsThingModel
|
<DeviceDetailsThingModel
|
||||||
@@ -18,9 +98,16 @@
|
|||||||
:thing-model-list="thingModelList"
|
:thing-model-list="thingModelList"
|
||||||
/>
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane v-if="product.deviceType === DeviceTypeEnum.GATEWAY" key="sub-device" tab="子设备管理" />
|
<a-tab-pane
|
||||||
|
v-if="product.deviceType === DeviceTypeEnum.GATEWAY"
|
||||||
|
key="sub-device"
|
||||||
|
tab="子设备管理"
|
||||||
|
/>
|
||||||
<a-tab-pane key="log" tab="设备消息">
|
<a-tab-pane key="log" tab="设备消息">
|
||||||
<DeviceDetailsMessage v-if="activeTab === 'log' && device.id" :device-id="device.id" />
|
<DeviceDetailsMessage
|
||||||
|
v-if="activeTab === 'log' && device.id"
|
||||||
|
:device-id="device.id"
|
||||||
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="simulator" tab="模拟设备">
|
<a-tab-pane key="simulator" tab="模拟设备">
|
||||||
<DeviceDetailsSimulator
|
<DeviceDetailsSimulator
|
||||||
@@ -40,75 +127,3 @@
|
|||||||
</a-tabs>
|
</a-tabs>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, unref, onMounted } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { useTabbarStore } from '@vben/stores'
|
|
||||||
import { Page } from '@vben/common-ui'
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import type { DeviceVO } from '#/api/iot/device/device'
|
|
||||||
import { DeviceTypeEnum, ProductApi } from '#/api/iot/product/product'
|
|
||||||
import type { ProductVO } from '#/api/iot/product/product'
|
|
||||||
import { ThingModelApi } from '#/api/iot/thingmodel'
|
|
||||||
import type { ThingModelData } from '#/api/iot/thingmodel'
|
|
||||||
import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
|
|
||||||
import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
|
|
||||||
import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
|
|
||||||
import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
|
|
||||||
import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
|
|
||||||
import DeviceDetailConfig from './DeviceDetailConfig.vue'
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTDeviceDetail' })
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const id = Number(route.params.id) // 将字符串转换为数字
|
|
||||||
const loading = ref(true) // 加载中
|
|
||||||
const product = ref<ProductVO>({} as ProductVO) // 产品详情
|
|
||||||
const device = ref<DeviceVO>({} as DeviceVO) // 设备详情
|
|
||||||
const activeTab = ref('info') // 默认激活的标签页
|
|
||||||
const thingModelList = ref<ThingModelData[]>([]) // 物模型列表数据
|
|
||||||
|
|
||||||
/** 获取设备详情 */
|
|
||||||
const getDeviceData = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
device.value = await DeviceApi.getDevice(id)
|
|
||||||
await getProductData(device.value.productId)
|
|
||||||
await getThingModelList(device.value.productId)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取产品详情 */
|
|
||||||
const getProductData = async (id: number) => {
|
|
||||||
product.value = await ProductApi.getProduct(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取物模型列表 */
|
|
||||||
const getThingModelList = async (productId: number) => {
|
|
||||||
try {
|
|
||||||
const data = await ThingModelApi.getThingModelList(productId)
|
|
||||||
thingModelList.value = data || []
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取物模型列表失败:', error)
|
|
||||||
thingModelList.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 */
|
|
||||||
const tabbarStore = useTabbarStore() // 视图操作
|
|
||||||
const router = useRouter() // 路由
|
|
||||||
const { currentRoute } = router
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!id) {
|
|
||||||
message.warning({ content: '参数错误,产品不能为空!' })
|
|
||||||
await tabbarStore.closeTab(unref(currentRoute), router)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await getDeviceData()
|
|
||||||
activeTab.value = (route.query.tab as string) || 'info'
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -52,14 +52,12 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
try {
|
try {
|
||||||
const values = await formApi.getValues();
|
const values = await formApi.getValues();
|
||||||
|
|
||||||
if (formData.value?.id) {
|
await (formData.value?.id
|
||||||
await updateDeviceGroup({
|
? updateDeviceGroup({
|
||||||
...values,
|
...values,
|
||||||
id: formData.value.id,
|
id: formData.value.id,
|
||||||
} as IotDeviceGroupApi.DeviceGroup);
|
} as IotDeviceGroupApi.DeviceGroup)
|
||||||
} else {
|
: createDeviceGroup(values as IotDeviceGroupApi.DeviceGroup));
|
||||||
await createDeviceGroup(values as IotDeviceGroupApi.DeviceGroup);
|
|
||||||
}
|
|
||||||
|
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* 设备数量饼图配置
|
* 设备数量饼图配置
|
||||||
*/
|
*/
|
||||||
export function getDeviceCountChartOptions(productCategoryDeviceCounts: Record<string, number>): any {
|
export function getDeviceCountChartOptions(
|
||||||
|
productCategoryDeviceCounts: Record<string, number>,
|
||||||
|
): any {
|
||||||
const data = Object.entries(productCategoryDeviceCounts).map(
|
const data = Object.entries(productCategoryDeviceCounts).map(
|
||||||
([name, value]) => ({ name, value })
|
([name, value]) => ({ name, value }),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -22,7 +24,7 @@ export function getDeviceCountChartOptions(productCategoryDeviceCounts: Record<s
|
|||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['50%', '80%'],
|
radius: ['50%', '80%'],
|
||||||
center: ['30%', '50%'],
|
center: ['30%', '50%'],
|
||||||
data: data,
|
data,
|
||||||
emphasis: {
|
emphasis: {
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
shadowBlur: 10,
|
shadowBlur: 10,
|
||||||
@@ -42,7 +44,12 @@ export function getDeviceCountChartOptions(productCategoryDeviceCounts: Record<s
|
|||||||
/**
|
/**
|
||||||
* 仪表盘图表配置
|
* 仪表盘图表配置
|
||||||
*/
|
*/
|
||||||
export function getGaugeChartOptions(value: number, max: number, color: string, title: string): any {
|
export function getGaugeChartOptions(
|
||||||
|
value: number,
|
||||||
|
max: number,
|
||||||
|
color: string,
|
||||||
|
title: string,
|
||||||
|
): any {
|
||||||
return {
|
return {
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
@@ -50,14 +57,14 @@ export function getGaugeChartOptions(value: number, max: number, color: string,
|
|||||||
startAngle: 180,
|
startAngle: 180,
|
||||||
endAngle: 0,
|
endAngle: 0,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: max,
|
max,
|
||||||
center: ['50%', '70%'],
|
center: ['50%', '70%'],
|
||||||
radius: '120%',
|
radius: '120%',
|
||||||
progress: {
|
progress: {
|
||||||
show: true,
|
show: true,
|
||||||
width: 12,
|
width: 12,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: color,
|
color,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
@@ -74,7 +81,7 @@ export function getGaugeChartOptions(value: number, max: number, color: string,
|
|||||||
valueAnimation: true,
|
valueAnimation: true,
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: color,
|
color,
|
||||||
offsetCenter: [0, '-20%'],
|
offsetCenter: [0, '-20%'],
|
||||||
formatter: '{value}',
|
formatter: '{value}',
|
||||||
},
|
},
|
||||||
@@ -84,10 +91,8 @@ export function getGaugeChartOptions(value: number, max: number, color: string,
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#666',
|
color: '#666',
|
||||||
},
|
},
|
||||||
data: [{ value: value, name: title }],
|
data: [{ value, name: title }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
* - 工具函数
|
* - 工具函数
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ref, onMounted } from 'vue';
|
|
||||||
|
|
||||||
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { getStatisticsSummary } from '#/api/iot/statistics';
|
import { getStatisticsSummary } from '#/api/iot/statistics';
|
||||||
|
|
||||||
/** 统计数据接口 - 使用 API 定义的类型 */
|
/** 统计数据接口 - 使用 API 定义的类型 */
|
||||||
@@ -49,7 +49,7 @@ export async function loadStatisticsData(): Promise<StatsData> {
|
|||||||
productCategoryCount: 12,
|
productCategoryCount: 12,
|
||||||
productCount: 45,
|
productCount: 45,
|
||||||
deviceCount: 328,
|
deviceCount: 328,
|
||||||
deviceMessageCount: 15678,
|
deviceMessageCount: 15_678,
|
||||||
productCategoryTodayCount: 2,
|
productCategoryTodayCount: 2,
|
||||||
productTodayCount: 5,
|
productTodayCount: 5,
|
||||||
deviceTodayCount: 23,
|
deviceTodayCount: 23,
|
||||||
@@ -58,10 +58,10 @@ export async function loadStatisticsData(): Promise<StatsData> {
|
|||||||
deviceOfflineCount: 48,
|
deviceOfflineCount: 48,
|
||||||
deviceInactiveCount: 24,
|
deviceInactiveCount: 24,
|
||||||
productCategoryDeviceCounts: {
|
productCategoryDeviceCounts: {
|
||||||
'智能家居': 120,
|
智能家居: 120,
|
||||||
'工业设备': 98,
|
工业设备: 98,
|
||||||
'环境监测': 65,
|
环境监测: 65,
|
||||||
'智能穿戴': 45,
|
智能穿戴: 45,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -103,12 +103,11 @@ export function useIotHome() {
|
|||||||
|
|
||||||
/** 格式化数字 - 大数字显示为 K/M */
|
/** 格式化数字 - 大数字显示为 K/M */
|
||||||
export const formatNumber = (num: number): string => {
|
export const formatNumber = (num: number): string => {
|
||||||
if (num >= 1000000) {
|
if (num >= 1_000_000) {
|
||||||
return (num / 1000000).toFixed(1) + 'M';
|
return `${(num / 1_000_000).toFixed(1)}M`;
|
||||||
}
|
}
|
||||||
if (num >= 1000) {
|
if (num >= 1000) {
|
||||||
return (num / 1000).toFixed(1) + 'K';
|
return `${(num / 1000).toFixed(1)}K`;
|
||||||
}
|
}
|
||||||
return num.toString();
|
return num.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Row, Col } from 'ant-design-vue';
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Col, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
|
// 导入业务逻辑
|
||||||
|
import { useIotHome } from './data';
|
||||||
// 导入组件
|
// 导入组件
|
||||||
import ComparisonCard from './modules/ComparisonCard.vue';
|
import ComparisonCard from './modules/ComparisonCard.vue';
|
||||||
import DeviceCountCard from './modules/DeviceCountCard.vue';
|
import DeviceCountCard from './modules/DeviceCountCard.vue';
|
||||||
import DeviceStateCountCard from './modules/DeviceStateCountCard.vue';
|
import DeviceStateCountCard from './modules/DeviceStateCountCard.vue';
|
||||||
import MessageTrendCard from './modules/MessageTrendCard.vue';
|
import MessageTrendCard from './modules/MessageTrendCard.vue';
|
||||||
|
|
||||||
// 导入业务逻辑
|
|
||||||
import { useIotHome } from './data';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTHome' });
|
defineOptions({ name: 'IoTHome' });
|
||||||
|
|
||||||
// 使用业务逻辑 Hook
|
// 使用业务逻辑 Hook
|
||||||
@@ -25,9 +25,9 @@ const { loading, statsData } = useIotHome();
|
|||||||
<ComparisonCard
|
<ComparisonCard
|
||||||
title="分类数量"
|
title="分类数量"
|
||||||
:value="statsData.productCategoryCount"
|
:value="statsData.productCategoryCount"
|
||||||
:todayCount="statsData.productCategoryTodayCount"
|
:today-count="statsData.productCategoryTodayCount"
|
||||||
icon="menu"
|
icon="menu"
|
||||||
iconColor="text-blue-500"
|
icon-color="text-blue-500"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -35,9 +35,9 @@ const { loading, statsData } = useIotHome();
|
|||||||
<ComparisonCard
|
<ComparisonCard
|
||||||
title="产品数量"
|
title="产品数量"
|
||||||
:value="statsData.productCount"
|
:value="statsData.productCount"
|
||||||
:todayCount="statsData.productTodayCount"
|
:today-count="statsData.productTodayCount"
|
||||||
icon="box"
|
icon="box"
|
||||||
iconColor="text-orange-500"
|
icon-color="text-orange-500"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -45,9 +45,9 @@ const { loading, statsData } = useIotHome();
|
|||||||
<ComparisonCard
|
<ComparisonCard
|
||||||
title="设备数量"
|
title="设备数量"
|
||||||
:value="statsData.deviceCount"
|
:value="statsData.deviceCount"
|
||||||
:todayCount="statsData.deviceTodayCount"
|
:today-count="statsData.deviceTodayCount"
|
||||||
icon="cpu"
|
icon="cpu"
|
||||||
iconColor="text-purple-500"
|
icon-color="text-purple-500"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -55,9 +55,9 @@ const { loading, statsData } = useIotHome();
|
|||||||
<ComparisonCard
|
<ComparisonCard
|
||||||
title="设备消息数"
|
title="设备消息数"
|
||||||
:value="statsData.deviceMessageCount"
|
:value="statsData.deviceMessageCount"
|
||||||
:todayCount="statsData.deviceMessageTodayCount"
|
:today-count="statsData.deviceMessageTodayCount"
|
||||||
icon="message"
|
icon="message"
|
||||||
iconColor="text-teal-500"
|
icon-color="text-teal-500"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -66,10 +66,10 @@ const { loading, statsData } = useIotHome();
|
|||||||
<!-- 第二行:图表 -->
|
<!-- 第二行:图表 -->
|
||||||
<Row :gutter="16" class="mb-4">
|
<Row :gutter="16" class="mb-4">
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<DeviceCountCard :statsData="statsData" :loading="loading" />
|
<DeviceCountCard :stats-data="statsData" :loading="loading" />
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="12">
|
<Col :span="12">
|
||||||
<DeviceStateCountCard :statsData="statsData" :loading="loading" />
|
<DeviceStateCountCard :stats-data="statsData" :loading="loading" />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { CountTo } from '@vben/common-ui';
|
||||||
|
import { createIconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'ComparisonCard' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
icon: string;
|
||||||
|
iconColor?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
title: string;
|
||||||
|
todayCount: number;
|
||||||
|
value: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const iconMap: Record<string, any> = {
|
||||||
|
menu: createIconifyIcon('ant-design:appstore-outlined'),
|
||||||
|
box: createIconifyIcon('ant-design:box-plot-outlined'),
|
||||||
|
cpu: createIconifyIcon('ant-design:cluster-outlined'),
|
||||||
|
message: createIconifyIcon('ant-design:message-outlined'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const IconComponent = computed(() => iconMap[props.icon] || iconMap.menu);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Card class="stat-card" :loading="loading">
|
<Card class="stat-card" :loading="loading">
|
||||||
<div class="flex flex-col h-full">
|
<div class="flex h-full flex-col">
|
||||||
<div class="flex justify-between items-start mb-4">
|
<div class="mb-4 flex items-start justify-between">
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-1 flex-col">
|
||||||
<span class="text-gray-500 text-sm font-medium mb-2">{{ title }}</span>
|
<span class="mb-2 text-sm font-medium text-gray-500">{{
|
||||||
|
title
|
||||||
|
}}</span>
|
||||||
<span class="text-3xl font-bold text-gray-800">
|
<span class="text-3xl font-bold text-gray-800">
|
||||||
<span v-if="value === -1">--</span>
|
<span v-if="value === -1">--</span>
|
||||||
<CountTo v-else :end-val="value" :duration="1000" />
|
<CountTo v-else :end-val="value" :duration="1000" />
|
||||||
@@ -14,44 +45,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-auto pt-3 border-t border-gray-100">
|
<div class="mt-auto border-t border-gray-100 pt-3">
|
||||||
<div class="flex justify-between items-center text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-gray-400">今日新增</span>
|
<span class="text-gray-400">今日新增</span>
|
||||||
<span v-if="todayCount === -1" class="text-gray-400">--</span>
|
<span v-if="todayCount === -1" class="text-gray-400">--</span>
|
||||||
<span v-else class="text-green-500 font-medium">+{{ todayCount }}</span>
|
<span v-else class="font-medium text-green-500"
|
||||||
|
>+{{ todayCount }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Card } from 'ant-design-vue';
|
|
||||||
import { CountTo } from '@vben/common-ui';
|
|
||||||
import { createIconifyIcon } from '@vben/icons';
|
|
||||||
import { computed } from 'vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ComparisonCard' });
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
title: string;
|
|
||||||
value: number;
|
|
||||||
todayCount: number;
|
|
||||||
icon: string;
|
|
||||||
iconColor?: string;
|
|
||||||
loading?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const iconMap: Record<string, any> = {
|
|
||||||
'menu': createIconifyIcon('ant-design:appstore-outlined'),
|
|
||||||
'box': createIconifyIcon('ant-design:box-plot-outlined'),
|
|
||||||
'cpu': createIconifyIcon('ant-design:cluster-outlined'),
|
|
||||||
'message': createIconifyIcon('ant-design:message-outlined'),
|
|
||||||
};
|
|
||||||
|
|
||||||
const IconComponent = computed(() => iconMap[props.icon] || iconMap.menu);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stat-card {
|
.stat-card {
|
||||||
height: 160px;
|
height: 160px;
|
||||||
|
|||||||
@@ -1,28 +1,17 @@
|
|||||||
<template>
|
|
||||||
<Card title="设备数量统计" :loading="loading" class="chart-card">
|
|
||||||
<div v-if="loading && !hasData" class="h-[300px] flex justify-center items-center">
|
|
||||||
<Empty description="加载中..." />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!hasData" class="h-[300px] flex justify-center items-center">
|
|
||||||
<Empty description="暂无数据" />
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<EchartsUI ref="deviceCountChartRef" class="h-[400px] w-full" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, nextTick } from 'vue';
|
|
||||||
import { Card, Empty } from 'ant-design-vue';
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { Card, Empty } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceCountCard' });
|
defineOptions({ name: 'DeviceCountCard' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
statsData: IotStatisticsApi.StatisticsSummary;
|
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
statsData: IotStatisticsApi.StatisticsSummary;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const deviceCountChartRef = ref();
|
const deviceCountChartRef = ref();
|
||||||
@@ -31,7 +20,9 @@ const { renderEcharts } = useEcharts(deviceCountChartRef);
|
|||||||
/** 是否有数据 */
|
/** 是否有数据 */
|
||||||
const hasData = computed(() => {
|
const hasData = computed(() => {
|
||||||
if (!props.statsData) return false;
|
if (!props.statsData) return false;
|
||||||
const categories = Object.entries(props.statsData.productCategoryDeviceCounts || {});
|
const categories = Object.entries(
|
||||||
|
props.statsData.productCategoryDeviceCounts || {},
|
||||||
|
);
|
||||||
return categories.length > 0 && props.statsData.deviceCount !== 0;
|
return categories.length > 0 && props.statsData.deviceCount !== 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,9 +31,9 @@ const initChart = () => {
|
|||||||
if (!hasData.value) return;
|
if (!hasData.value) return;
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const data = Object.entries(props.statsData.productCategoryDeviceCounts).map(
|
const data = Object.entries(
|
||||||
([name, value]) => ({ name, value })
|
props.statsData.productCategoryDeviceCounts,
|
||||||
);
|
).map(([name, value]) => ({ name, value }));
|
||||||
|
|
||||||
renderEcharts({
|
renderEcharts({
|
||||||
tooltip: {
|
tooltip: {
|
||||||
@@ -98,7 +89,7 @@ const initChart = () => {
|
|||||||
labelLine: {
|
labelLine: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
data: data,
|
data,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -111,7 +102,7 @@ watch(
|
|||||||
() => {
|
() => {
|
||||||
initChart();
|
initChart();
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 组件挂载时初始化图表 */
|
/** 组件挂载时初始化图表 */
|
||||||
@@ -120,6 +111,26 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card title="设备数量统计" :loading="loading" class="chart-card">
|
||||||
|
<div
|
||||||
|
v-if="loading && !hasData"
|
||||||
|
class="flex h-[300px] items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty description="加载中..." />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!hasData"
|
||||||
|
class="flex h-[300px] items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty description="暂无数据" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<EchartsUI ref="deviceCountChartRef" class="h-[400px] w-full" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-card {
|
.chart-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,36 +1,17 @@
|
|||||||
<template>
|
|
||||||
<Card title="设备状态统计" :loading="loading" class="chart-card">
|
|
||||||
<div v-if="loading && !hasData" class="h-[300px] flex justify-center items-center">
|
|
||||||
<Empty description="加载中..." />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!hasData" class="h-[300px] flex justify-center items-center">
|
|
||||||
<Empty description="暂无数据" />
|
|
||||||
</div>
|
|
||||||
<Row v-else class="h-[280px]">
|
|
||||||
<Col :span="8" class="flex items-center justify-center">
|
|
||||||
<EchartsUI ref="deviceOnlineChartRef" class="h-[250px] w-full" />
|
|
||||||
</Col>
|
|
||||||
<Col :span="8" class="flex items-center justify-center">
|
|
||||||
<EchartsUI ref="deviceOfflineChartRef" class="h-[250px] w-full" />
|
|
||||||
</Col>
|
|
||||||
<Col :span="8" class="flex items-center justify-center">
|
|
||||||
<EchartsUI ref="deviceInactiveChartRef" class="h-[250px] w-full" />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, nextTick } from 'vue';
|
|
||||||
import { Card, Empty, Row, Col } from 'ant-design-vue';
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
import type { IotStatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { Card, Col, Empty, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceStateCountCard' });
|
defineOptions({ name: 'DeviceStateCountCard' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
statsData: IotStatisticsApi.StatisticsSummary;
|
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
statsData: IotStatisticsApi.StatisticsSummary;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const deviceOnlineChartRef = ref();
|
const deviceOnlineChartRef = ref();
|
||||||
@@ -39,7 +20,9 @@ const deviceInactiveChartRef = ref();
|
|||||||
|
|
||||||
const { renderEcharts: renderOnlineChart } = useEcharts(deviceOnlineChartRef);
|
const { renderEcharts: renderOnlineChart } = useEcharts(deviceOnlineChartRef);
|
||||||
const { renderEcharts: renderOfflineChart } = useEcharts(deviceOfflineChartRef);
|
const { renderEcharts: renderOfflineChart } = useEcharts(deviceOfflineChartRef);
|
||||||
const { renderEcharts: renderInactiveChart } = useEcharts(deviceInactiveChartRef);
|
const { renderEcharts: renderInactiveChart } = useEcharts(
|
||||||
|
deviceInactiveChartRef,
|
||||||
|
);
|
||||||
|
|
||||||
/** 是否有数据 */
|
/** 是否有数据 */
|
||||||
const hasData = computed(() => {
|
const hasData = computed(() => {
|
||||||
@@ -63,7 +46,7 @@ const getGaugeOption = (value: number, color: string, title: string): any => {
|
|||||||
show: true,
|
show: true,
|
||||||
width: 12,
|
width: 12,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: color,
|
color,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
axisLine: {
|
axisLine: {
|
||||||
@@ -86,11 +69,11 @@ const getGaugeOption = (value: number, color: string, title: string): any => {
|
|||||||
valueAnimation: true,
|
valueAnimation: true,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: color,
|
color,
|
||||||
offsetCenter: [0, '10%'],
|
offsetCenter: [0, '10%'],
|
||||||
formatter: (val: number) => `${val} 个`,
|
formatter: (val: number) => `${val} 个`,
|
||||||
},
|
},
|
||||||
data: [{ value: value, name: title }],
|
data: [{ value, name: title }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -103,15 +86,19 @@ const initCharts = () => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
// 在线设备
|
// 在线设备
|
||||||
renderOnlineChart(
|
renderOnlineChart(
|
||||||
getGaugeOption(props.statsData.deviceOnlineCount, '#52c41a', '在线设备')
|
getGaugeOption(props.statsData.deviceOnlineCount, '#52c41a', '在线设备'),
|
||||||
);
|
);
|
||||||
// 离线设备
|
// 离线设备
|
||||||
renderOfflineChart(
|
renderOfflineChart(
|
||||||
getGaugeOption(props.statsData.deviceOfflineCount, '#ff4d4f', '离线设备')
|
getGaugeOption(props.statsData.deviceOfflineCount, '#ff4d4f', '离线设备'),
|
||||||
);
|
);
|
||||||
// 待激活设备
|
// 待激活设备
|
||||||
renderInactiveChart(
|
renderInactiveChart(
|
||||||
getGaugeOption(props.statsData.deviceInactiveCount, '#1890ff', '待激活设备')
|
getGaugeOption(
|
||||||
|
props.statsData.deviceInactiveCount,
|
||||||
|
'#1890ff',
|
||||||
|
'待激活设备',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -122,7 +109,7 @@ watch(
|
|||||||
() => {
|
() => {
|
||||||
initCharts();
|
initCharts();
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 组件挂载时初始化图表 */
|
/** 组件挂载时初始化图表 */
|
||||||
@@ -131,6 +118,34 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card title="设备状态统计" :loading="loading" class="chart-card">
|
||||||
|
<div
|
||||||
|
v-if="loading && !hasData"
|
||||||
|
class="flex h-[300px] items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty description="加载中..." />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!hasData"
|
||||||
|
class="flex h-[300px] items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty description="暂无数据" />
|
||||||
|
</div>
|
||||||
|
<Row v-else class="h-[280px]">
|
||||||
|
<Col :span="8" class="flex items-center justify-center">
|
||||||
|
<EchartsUI ref="deviceOnlineChartRef" class="h-[250px] w-full" />
|
||||||
|
</Col>
|
||||||
|
<Col :span="8" class="flex items-center justify-center">
|
||||||
|
<EchartsUI ref="deviceOfflineChartRef" class="h-[250px] w-full" />
|
||||||
|
</Col>
|
||||||
|
<Col :span="8" class="flex items-center justify-center">
|
||||||
|
<EchartsUI ref="deviceInactiveChartRef" class="h-[250px] w-full" />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-card {
|
.chart-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,70 +1,24 @@
|
|||||||
<template>
|
|
||||||
<Card class="chart-card" :loading="loading">
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center justify-between flex-wrap gap-2">
|
|
||||||
<span class="text-base font-medium">上下行消息量统计</span>
|
|
||||||
<Space :size="8">
|
|
||||||
<Button
|
|
||||||
:type="activeTimeRange === '1h' ? 'primary' : 'default'"
|
|
||||||
size="small"
|
|
||||||
@click="setTimeRange('1h')"
|
|
||||||
>
|
|
||||||
最近1小时
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
:type="activeTimeRange === '24h' ? 'primary' : 'default'"
|
|
||||||
size="small"
|
|
||||||
@click="setTimeRange('24h')"
|
|
||||||
>
|
|
||||||
最近24小时
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
:type="activeTimeRange === '7d' ? 'primary' : 'default'"
|
|
||||||
size="small"
|
|
||||||
@click="setTimeRange('7d')"
|
|
||||||
>
|
|
||||||
近一周
|
|
||||||
</Button>
|
|
||||||
<RangePicker
|
|
||||||
v-model:value="dateRange"
|
|
||||||
format="YYYY-MM-DD"
|
|
||||||
:placeholder="['开始时间', '结束时间']"
|
|
||||||
@change="handleDateChange"
|
|
||||||
size="small"
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-if="loading" class="h-[350px] flex justify-center items-center">
|
|
||||||
<Empty description="加载中..." />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!hasData" class="h-[350px] flex justify-center items-center">
|
|
||||||
<Empty description="暂无数据" />
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<EchartsUI ref="messageChartRef" class="h-[350px] w-full" />
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue';
|
|
||||||
import { Card, Empty, Space, DatePicker, Button } from 'ant-design-vue';
|
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import {
|
import type {
|
||||||
StatisticsApi,
|
IotStatisticsDeviceMessageReqVO,
|
||||||
type IotStatisticsDeviceMessageSummaryByDateRespVO,
|
IotStatisticsDeviceMessageSummaryByDateRespVO,
|
||||||
type IotStatisticsDeviceMessageReqVO,
|
|
||||||
} from '#/api/iot/statistics';
|
} from '#/api/iot/statistics';
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { Button, Card, DatePicker, Empty, Space } from 'ant-design-vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import { StatisticsApi } from '#/api/iot/statistics';
|
||||||
|
|
||||||
defineOptions({ name: 'MessageTrendCard' });
|
defineOptions({ name: 'MessageTrendCard' });
|
||||||
|
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
const messageChartRef = ref();
|
const messageChartRef = ref();
|
||||||
const { renderEcharts } = useEcharts(messageChartRef);
|
const { renderEcharts } = useEcharts(messageChartRef);
|
||||||
|
|
||||||
@@ -89,25 +43,29 @@ const setTimeRange = (range: string) => {
|
|||||||
dateRange.value = undefined; // 清空自定义时间选择
|
dateRange.value = undefined; // 清空自定义时间选择
|
||||||
|
|
||||||
let start: Dayjs;
|
let start: Dayjs;
|
||||||
let end = dayjs();
|
const end = dayjs();
|
||||||
|
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case '1h':
|
case '1h': {
|
||||||
start = dayjs().subtract(1, 'hour');
|
start = dayjs().subtract(1, 'hour');
|
||||||
queryParams.interval = 1; // 按分钟
|
queryParams.interval = 1; // 按分钟
|
||||||
break;
|
break;
|
||||||
case '24h':
|
}
|
||||||
start = dayjs().subtract(24, 'hour');
|
case '7d': {
|
||||||
queryParams.interval = 1; // 按小时
|
|
||||||
break;
|
|
||||||
case '7d':
|
|
||||||
start = dayjs().subtract(7, 'day');
|
start = dayjs().subtract(7, 'day');
|
||||||
queryParams.interval = 1; // 按天
|
queryParams.interval = 1; // 按天
|
||||||
break;
|
break;
|
||||||
default:
|
}
|
||||||
|
case '24h': {
|
||||||
|
start = dayjs().subtract(24, 'hour');
|
||||||
|
queryParams.interval = 1; // 按小时
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
start = dayjs().subtract(7, 'day');
|
start = dayjs().subtract(7, 'day');
|
||||||
queryParams.interval = 1;
|
queryParams.interval = 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
queryParams.times = [
|
queryParams.times = [
|
||||||
start.format('YYYY-MM-DD HH:mm:ss'),
|
start.format('YYYY-MM-DD HH:mm:ss'),
|
||||||
@@ -136,7 +94,8 @@ const fetchMessageData = async () => {
|
|||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
messageData.value = await StatisticsApi.getDeviceMessageSummaryByDate(queryParams);
|
messageData.value =
|
||||||
|
await StatisticsApi.getDeviceMessageSummaryByDate(queryParams);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
initChart();
|
initChart();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -230,6 +189,60 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card class="chart-card" :loading="loading">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<span class="text-base font-medium">上下行消息量统计</span>
|
||||||
|
<Space :size="8">
|
||||||
|
<Button
|
||||||
|
:type="activeTimeRange === '1h' ? 'primary' : 'default'"
|
||||||
|
size="small"
|
||||||
|
@click="setTimeRange('1h')"
|
||||||
|
>
|
||||||
|
最近1小时
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:type="activeTimeRange === '24h' ? 'primary' : 'default'"
|
||||||
|
size="small"
|
||||||
|
@click="setTimeRange('24h')"
|
||||||
|
>
|
||||||
|
最近24小时
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
:type="activeTimeRange === '7d' ? 'primary' : 'default'"
|
||||||
|
size="small"
|
||||||
|
@click="setTimeRange('7d')"
|
||||||
|
>
|
||||||
|
近一周
|
||||||
|
</Button>
|
||||||
|
<RangePicker
|
||||||
|
v-model:value="dateRange"
|
||||||
|
format="YYYY-MM-DD"
|
||||||
|
:placeholder="['开始时间', '结束时间']"
|
||||||
|
@change="handleDateChange"
|
||||||
|
size="small"
|
||||||
|
style="width: 240px"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex h-[350px] items-center justify-center">
|
||||||
|
<Empty description="加载中..." />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="!hasData"
|
||||||
|
class="flex h-[350px] items-center justify-center"
|
||||||
|
>
|
||||||
|
<Empty description="暂无数据" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<EchartsUI ref="messageChartRef" class="h-[350px] w-full" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-card {
|
.chart-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import { Page, useVbenModal } from '@vben/common-ui';
|
|||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import Form from '../modules/OtaFirmwareForm.vue';
|
||||||
import {
|
import {
|
||||||
handleDeleteFirmware,
|
handleDeleteFirmware,
|
||||||
useGridFormSchema,
|
useGridFormSchema,
|
||||||
useGridOptions,
|
useGridOptions,
|
||||||
} from './data';
|
} from './data';
|
||||||
import Form from '../modules/OtaFirmwareForm.vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTOtaFirmware' });
|
defineOptions({ name: 'IoTOtaFirmware' });
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
// 提交表单
|
// 提交表单
|
||||||
const data = (await formApi.getValues()) as IoTOtaFirmwareApi.Firmware;
|
const data = (await formApi.getValues()) as IoTOtaFirmwareApi.Firmware;
|
||||||
try {
|
try {
|
||||||
await (formData.value?.id ? updateOtaFirmware(data) : createOtaFirmware(data));
|
await (formData.value?.id
|
||||||
|
? updateOtaFirmware(data)
|
||||||
|
: createOtaFirmware(data));
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
|
||||||
import { formatDate } from '@vben/utils';
|
import { formatDate } from '@vben/utils';
|
||||||
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
|
||||||
|
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
import * as IoTOtaFirmwareApi from '#/api/iot/ota/firmware';
|
import * as IoTOtaFirmwareApi from '#/api/iot/ota/firmware';
|
||||||
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
import OtaTaskList from '../task/OtaTaskList.vue';
|
import OtaTaskList from '../task/OtaTaskList.vue';
|
||||||
|
|
||||||
/** IoT OTA 固件详情 */
|
/** IoT OTA 固件详情 */
|
||||||
@@ -35,7 +40,8 @@ const getFirmwareInfo = async () => {
|
|||||||
const getStatistics = async () => {
|
const getStatistics = async () => {
|
||||||
firmwareStatisticsLoading.value = true;
|
firmwareStatisticsLoading.value = true;
|
||||||
try {
|
try {
|
||||||
firmwareStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
|
firmwareStatistics.value =
|
||||||
|
await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
|
||||||
firmwareId.value,
|
firmwareId.value,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -65,7 +71,11 @@ onMounted(() => {
|
|||||||
{{ firmware?.version }}
|
{{ firmware?.version }}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="创建时间">
|
<Descriptions.Item label="创建时间">
|
||||||
{{ firmware?.createTime ? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss') : '-' }}
|
{{
|
||||||
|
firmware?.createTime
|
||||||
|
? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="固件描述" :span="2">
|
<Descriptions.Item label="固件描述" :span="2">
|
||||||
{{ firmware?.description }}
|
{{ firmware?.description }}
|
||||||
@@ -74,63 +84,101 @@ onMounted(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 升级设备统计 -->
|
<!-- 升级设备统计 -->
|
||||||
<Card title="升级设备统计" class="mb-5" :loading="firmwareStatisticsLoading">
|
<Card
|
||||||
|
title="升级设备统计"
|
||||||
|
class="mb-5"
|
||||||
|
:loading="firmwareStatisticsLoading"
|
||||||
|
>
|
||||||
<Row :gutter="20" class="py-5">
|
<Row :gutter="20" class="py-5">
|
||||||
<Col :span="6">
|
<Col :span="6">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-blue-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-blue-500">
|
||||||
{{
|
{{
|
||||||
Object.values(firmwareStatistics).reduce((sum: number, count) => sum + (count || 0), 0) ||
|
Object.values(firmwareStatistics).reduce(
|
||||||
0
|
(sum: number, count) => sum + (count || 0),
|
||||||
|
0,
|
||||||
|
) || 0
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级设备总数</div>
|
<div class="text-sm text-gray-600">升级设备总数</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">待推送</div>
|
<div class="text-sm text-gray-600">待推送</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-blue-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-blue-400">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">已推送</div>
|
<div class="text-sm text-gray-600">已推送</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-yellow-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-yellow-500">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[
|
||||||
|
IoTOtaTaskRecordStatusEnum.UPGRADING.value
|
||||||
|
] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">正在升级</div>
|
<div class="text-sm text-gray-600">正在升级</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-green-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-green-500">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级成功</div>
|
<div class="text-sm text-gray-600">升级成功</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-red-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-red-500">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级失败</div>
|
<div class="text-sm text-gray-600">升级失败</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级取消</div>
|
<div class="text-sm text-gray-600">升级取消</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
|
||||||
import { formatDate } from '@vben/utils';
|
import { formatDate } from '@vben/utils';
|
||||||
import type { IoTOtaFirmware } from '#/api/iot/ota/firmware';
|
|
||||||
|
import { Card, Col, Descriptions, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
import * as IoTOtaFirmwareApi from '#/api/iot/ota/firmware';
|
import * as IoTOtaFirmwareApi from '#/api/iot/ota/firmware';
|
||||||
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
import OtaTaskList from '../task/OtaTaskList.vue';
|
import OtaTaskList from '../task/OtaTaskList.vue';
|
||||||
|
|
||||||
/** IoT OTA 固件详情 */
|
/** IoT OTA 固件详情 */
|
||||||
@@ -35,7 +40,8 @@ const getFirmwareInfo = async () => {
|
|||||||
const getStatistics = async () => {
|
const getStatistics = async () => {
|
||||||
firmwareStatisticsLoading.value = true;
|
firmwareStatisticsLoading.value = true;
|
||||||
try {
|
try {
|
||||||
firmwareStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
|
firmwareStatistics.value =
|
||||||
|
await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
|
||||||
firmwareId.value,
|
firmwareId.value,
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -65,7 +71,11 @@ onMounted(() => {
|
|||||||
{{ firmware?.version }}
|
{{ firmware?.version }}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="创建时间">
|
<Descriptions.Item label="创建时间">
|
||||||
{{ firmware?.createTime ? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss') : '-' }}
|
{{
|
||||||
|
firmware?.createTime
|
||||||
|
? formatDate(firmware.createTime, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="固件描述" :span="2">
|
<Descriptions.Item label="固件描述" :span="2">
|
||||||
{{ firmware?.description }}
|
{{ firmware?.description }}
|
||||||
@@ -74,63 +84,101 @@ onMounted(() => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 升级设备统计 -->
|
<!-- 升级设备统计 -->
|
||||||
<Card title="升级设备统计" class="mb-5" :loading="firmwareStatisticsLoading">
|
<Card
|
||||||
|
title="升级设备统计"
|
||||||
|
class="mb-5"
|
||||||
|
:loading="firmwareStatisticsLoading"
|
||||||
|
>
|
||||||
<Row :gutter="20" class="py-5">
|
<Row :gutter="20" class="py-5">
|
||||||
<Col :span="6">
|
<Col :span="6">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-blue-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-blue-500">
|
||||||
{{
|
{{
|
||||||
Object.values(firmwareStatistics).reduce((sum: number, count) => sum + (count || 0), 0) ||
|
Object.values(firmwareStatistics).reduce(
|
||||||
0
|
(sum: number, count) => sum + (count || 0),
|
||||||
|
0,
|
||||||
|
) || 0
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级设备总数</div>
|
<div class="text-sm text-gray-600">升级设备总数</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">待推送</div>
|
<div class="text-sm text-gray-600">待推送</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-blue-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-blue-400">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">已推送</div>
|
<div class="text-sm text-gray-600">已推送</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-yellow-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-yellow-500">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[
|
||||||
|
IoTOtaTaskRecordStatusEnum.UPGRADING.value
|
||||||
|
] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">正在升级</div>
|
<div class="text-sm text-gray-600">正在升级</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-green-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-green-500">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级成功</div>
|
<div class="text-sm text-gray-600">升级成功</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-red-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-red-500">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级失败</div>
|
<div class="text-sm text-gray-600">升级失败</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||||
|
{{
|
||||||
|
firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级取消</div>
|
<div class="text-sm text-gray-600">升级取消</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref } from 'vue';
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
|
||||||
import { Card, Col, Descriptions, Modal, Row, Table, Tabs, Tag, message } from 'ant-design-vue';
|
|
||||||
import type { TableColumnsType } from 'ant-design-vue';
|
import type { TableColumnsType } from 'ant-design-vue';
|
||||||
|
|
||||||
import type { OtaTask } from '#/api/iot/ota/task';
|
import type { OtaTask } from '#/api/iot/ota/task';
|
||||||
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
|
||||||
import type { OtaTaskRecord } from '#/api/iot/ota/task/record';
|
import type { OtaTaskRecord } from '#/api/iot/ota/task/record';
|
||||||
|
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Descriptions,
|
||||||
|
message,
|
||||||
|
Modal,
|
||||||
|
Row,
|
||||||
|
Table,
|
||||||
|
Tabs,
|
||||||
|
Tag,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
|
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
||||||
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
import * as IoTOtaTaskRecordApi from '#/api/iot/ota/task/record';
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||||
import { formatDate } from '@vben/utils';
|
|
||||||
|
|
||||||
/** OTA 任务详情组件 */
|
/** OTA 任务详情组件 */
|
||||||
defineOptions({ name: 'OtaTaskDetail' });
|
defineOptions({ name: 'OtaTaskDetail' });
|
||||||
@@ -117,7 +132,8 @@ const getStatistics = async () => {
|
|||||||
}
|
}
|
||||||
taskStatisticsLoading.value = true;
|
taskStatisticsLoading.value = true;
|
||||||
try {
|
try {
|
||||||
taskStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
|
taskStatistics.value =
|
||||||
|
await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
|
||||||
undefined,
|
undefined,
|
||||||
taskId.value,
|
taskId.value,
|
||||||
);
|
);
|
||||||
@@ -143,10 +159,11 @@ const getRecordList = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 切换标签 */
|
/** 切换标签 */
|
||||||
const handleTabChange = (tabKey: string | number) => {
|
const handleTabChange = (tabKey: number | string) => {
|
||||||
activeTab.value = String(tabKey);
|
activeTab.value = String(tabKey);
|
||||||
queryParams.pageNo = 1;
|
queryParams.pageNo = 1;
|
||||||
queryParams.status = activeTab.value === '' ? undefined : parseInt(String(tabKey));
|
queryParams.status =
|
||||||
|
activeTab.value === '' ? undefined : Number.parseInt(String(tabKey));
|
||||||
getRecordList();
|
getRecordList();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,7 +219,9 @@ defineExpose({ open });
|
|||||||
<Card title="任务信息" class="mb-5" :loading="taskLoading">
|
<Card title="任务信息" class="mb-5" :loading="taskLoading">
|
||||||
<Descriptions :column="3" bordered>
|
<Descriptions :column="3" bordered>
|
||||||
<Descriptions.Item label="任务编号">{{ task.id }}</Descriptions.Item>
|
<Descriptions.Item label="任务编号">{{ task.id }}</Descriptions.Item>
|
||||||
<Descriptions.Item label="任务名称">{{ task.name }}</Descriptions.Item>
|
<Descriptions.Item label="任务名称">
|
||||||
|
{{ task.name }}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="升级范围">
|
<Descriptions.Item label="升级范围">
|
||||||
<Tag v-if="task.deviceScope === 1" color="blue">全部设备</Tag>
|
<Tag v-if="task.deviceScope === 1" color="blue">全部设备</Tag>
|
||||||
<Tag v-else-if="task.deviceScope === 2" color="green">指定设备</Tag>
|
<Tag v-else-if="task.deviceScope === 2" color="green">指定设备</Tag>
|
||||||
@@ -216,7 +235,11 @@ defineExpose({ open });
|
|||||||
<Tag v-else>{{ task.status }}</Tag>
|
<Tag v-else>{{ task.status }}</Tag>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="创建时间">
|
<Descriptions.Item label="创建时间">
|
||||||
{{ task.createTime ? formatDate(task.createTime, 'YYYY-MM-DD HH:mm:ss') : '-' }}
|
{{
|
||||||
|
task.createTime
|
||||||
|
? formatDate(task.createTime, 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="任务描述" :span="3">
|
<Descriptions.Item label="任务描述" :span="3">
|
||||||
{{ task.description }}
|
{{ task.description }}
|
||||||
@@ -228,59 +251,89 @@ defineExpose({ open });
|
|||||||
<Card title="升级设备统计" class="mb-5" :loading="taskStatisticsLoading">
|
<Card title="升级设备统计" class="mb-5" :loading="taskStatisticsLoading">
|
||||||
<Row :gutter="20" class="py-5">
|
<Row :gutter="20" class="py-5">
|
||||||
<Col :span="6">
|
<Col :span="6">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-blue-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-blue-500">
|
||||||
{{
|
{{
|
||||||
Object.values(taskStatistics).reduce((sum, count) => sum + (count || 0), 0) || 0
|
Object.values(taskStatistics).reduce(
|
||||||
|
(sum, count) => sum + (count || 0),
|
||||||
|
0,
|
||||||
|
) || 0
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级设备总数</div>
|
<div class="text-sm text-gray-600">升级设备总数</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||||
|
{{
|
||||||
|
taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">待推送</div>
|
<div class="text-sm text-gray-600">待推送</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-blue-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-blue-400">
|
||||||
|
{{
|
||||||
|
taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">已推送</div>
|
<div class="text-sm text-gray-600">已推送</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-yellow-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-yellow-500">
|
||||||
|
{{
|
||||||
|
taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] ||
|
||||||
|
0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">正在升级</div>
|
<div class="text-sm text-gray-600">正在升级</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-green-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-green-500">
|
||||||
|
{{
|
||||||
|
taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级成功</div>
|
<div class="text-sm text-gray-600">升级成功</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-red-500">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-red-500">
|
||||||
|
{{
|
||||||
|
taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级失败</div>
|
<div class="text-sm text-gray-600">升级失败</div>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col :span="3">
|
<Col :span="3">
|
||||||
<div class="text-center p-5 border border-solid border-gray-200 rounded bg-gray-50">
|
<div
|
||||||
<div class="text-3xl font-bold mb-2 text-gray-400">
|
class="rounded border border-solid border-gray-200 bg-gray-50 p-5 text-center"
|
||||||
{{ taskStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
|
>
|
||||||
|
<div class="mb-2 text-3xl font-bold text-gray-400">
|
||||||
|
{{
|
||||||
|
taskStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-600">升级取消</div>
|
<div class="text-sm text-gray-600">升级取消</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,8 +343,16 @@ defineExpose({ open });
|
|||||||
|
|
||||||
<!-- 设备管理 -->
|
<!-- 设备管理 -->
|
||||||
<Card title="升级设备记录">
|
<Card title="升级设备记录">
|
||||||
<Tabs v-model:activeKey="activeTab" @change="handleTabChange" class="mb-4">
|
<Tabs
|
||||||
<Tabs.TabPane v-for="tab in statusTabs" :key="tab.key" :tab="tab.label" />
|
v-model:active-key="activeTab"
|
||||||
|
@change="handleTabChange"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<Tabs.TabPane
|
||||||
|
v-for="tab in statusTabs"
|
||||||
|
:key="tab.key"
|
||||||
|
:tab="tab.label"
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
@@ -313,7 +374,9 @@ defineExpose({ open });
|
|||||||
<template v-if="column.key === 'status'">
|
<template v-if="column.key === 'status'">
|
||||||
<Tag v-if="record.status === 0" color="default">待推送</Tag>
|
<Tag v-if="record.status === 0" color="default">待推送</Tag>
|
||||||
<Tag v-else-if="record.status === 1" color="blue">已推送</Tag>
|
<Tag v-else-if="record.status === 1" color="blue">已推送</Tag>
|
||||||
<Tag v-else-if="record.status === 2" color="processing">升级中</Tag>
|
<Tag v-else-if="record.status === 2" color="processing">
|
||||||
|
升级中
|
||||||
|
</Tag>
|
||||||
<Tag v-else-if="record.status === 3" color="success">成功</Tag>
|
<Tag v-else-if="record.status === 3" color="success">成功</Tag>
|
||||||
<Tag v-else-if="record.status === 4" color="error">失败</Tag>
|
<Tag v-else-if="record.status === 4" color="error">失败</Tag>
|
||||||
<Tag v-else-if="record.status === 5" color="warning">已取消</Tag>
|
<Tag v-else-if="record.status === 5" color="warning">已取消</Tag>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import type { DeviceVO } from '#/api/iot/device/device';
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
|
||||||
import { message, Form, Input, Select, Spin } from 'ant-design-vue';
|
|
||||||
import type { OtaTask } from '#/api/iot/ota/task';
|
import type { OtaTask } from '#/api/iot/ota/task';
|
||||||
|
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { Form, Input, message, Select, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import * as DeviceApi from '#/api/iot/device/device';
|
||||||
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
||||||
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
||||||
import type { DeviceVO } from '#/api/iot/device/device';
|
|
||||||
import * as DeviceApi from '#/api/iot/device/device';
|
|
||||||
|
|
||||||
/** IoT OTA 升级任务表单 */
|
/** IoT OTA 升级任务表单 */
|
||||||
defineOptions({ name: 'OtaTaskForm' });
|
defineOptions({ name: 'OtaTaskForm' });
|
||||||
@@ -28,9 +32,30 @@ const formData = ref<OtaTask>({
|
|||||||
});
|
});
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const formRules = {
|
const formRules = {
|
||||||
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' as const, type: 'string' as const }],
|
name: [
|
||||||
deviceScope: [{ required: true, message: '请选择升级范围', trigger: 'change' as const, type: 'number' as const }],
|
{
|
||||||
deviceIds: [{ required: true, message: '请至少选择一个设备', trigger: 'change' as const, type: 'array' as const }],
|
required: true,
|
||||||
|
message: '请输入任务名称',
|
||||||
|
trigger: 'blur' as const,
|
||||||
|
type: 'string' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deviceScope: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请选择升级范围',
|
||||||
|
trigger: 'change' as const,
|
||||||
|
type: 'number' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deviceIds: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '请至少选择一个设备',
|
||||||
|
trigger: 'change' as const,
|
||||||
|
type: 'array' as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
const devices = ref<DeviceVO[]>([]);
|
const devices = ref<DeviceVO[]>([]);
|
||||||
|
|
||||||
@@ -73,7 +98,8 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
// 加载设备列表
|
// 加载设备列表
|
||||||
formLoading.value = true;
|
formLoading.value = true;
|
||||||
try {
|
try {
|
||||||
devices.value = (await DeviceApi.getDeviceListByProductId(props.productId)) || [];
|
devices.value =
|
||||||
|
(await DeviceApi.getDeviceListByProductId(props.productId)) || [];
|
||||||
} finally {
|
} finally {
|
||||||
formLoading.value = false;
|
formLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import type { TableColumnsType } from 'ant-design-vue';
|
||||||
import { formatDate } from '@vben/utils';
|
|
||||||
import type { OtaTask } from '#/api/iot/ota/task';
|
import type { OtaTask } from '#/api/iot/ota/task';
|
||||||
|
|
||||||
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import { VbenButton } from '@vben/common-ui';
|
||||||
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Card, Input, message, Modal, Space, Table, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
import * as IoTOtaTaskApi from '#/api/iot/ota/task';
|
||||||
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
||||||
import OtaTaskForm from './OtaTaskForm.vue';
|
|
||||||
import OtaTaskDetail from './OtaTaskDetail.vue';
|
import OtaTaskDetail from './OtaTaskDetail.vue';
|
||||||
import { Card, Input, Table, Space, Modal, message, Tag } from 'ant-design-vue';
|
import OtaTaskForm from './OtaTaskForm.vue';
|
||||||
import type { TableColumnsType } from 'ant-design-vue';
|
|
||||||
import { VbenButton } from '@vben/common-ui';
|
|
||||||
|
|
||||||
/** IoT OTA 任务列表 */
|
/** IoT OTA 任务列表 */
|
||||||
defineOptions({ name: 'OtaTaskList' });
|
defineOptions({ name: 'OtaTaskList' });
|
||||||
@@ -160,7 +166,7 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<Card title="升级任务管理" class="mb-5">
|
<Card title="升级任务管理" class="mb-5">
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<div class="mb-4 flex justify-between items-center">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<VbenButton type="primary" @click="openTaskForm">
|
<VbenButton type="primary" @click="openTaskForm">
|
||||||
<Icon icon="ant-design:plus-outlined" class="mr-1" />
|
<Icon icon="ant-design:plus-outlined" class="mr-1" />
|
||||||
新增
|
新增
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IotPlugin' });
|
defineOptions({ name: 'IotPlugin' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page
|
<Page description="物聯網插件管理" title="插件管理">
|
||||||
description="物聯網插件管理"
|
|
||||||
title="插件管理"
|
|
||||||
>
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { z } from '#/adapter/form';
|
|||||||
import {
|
import {
|
||||||
deleteProductCategory,
|
deleteProductCategory,
|
||||||
getProductCategoryPage,
|
getProductCategoryPage,
|
||||||
getSimpleProductCategoryList
|
getSimpleProductCategoryList,
|
||||||
} from '#/api/iot/product/category';
|
} from '#/api/iot/product/category';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
@@ -162,7 +162,10 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 删除分类 */
|
/** 删除分类 */
|
||||||
export async function handleDeleteCategory(row: IotProductCategoryApi.ProductCategory, onSuccess?: () => void) {
|
export async function handleDeleteCategory(
|
||||||
|
row: IotProductCategoryApi.ProductCategory,
|
||||||
|
onSuccess?: () => void,
|
||||||
|
) {
|
||||||
const hideLoading = message.loading({
|
const hideLoading = message.loading({
|
||||||
content: $t('ui.actionMessage.deleting', [row.name]),
|
content: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
duration: 0,
|
duration: 0,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
handleDeleteCategory,
|
handleDeleteCategory,
|
||||||
queryProductCategoryList,
|
queryProductCategoryList,
|
||||||
useGridColumns,
|
useGridColumns,
|
||||||
useGridFormSchema
|
useGridFormSchema,
|
||||||
} from './data';
|
} from './data';
|
||||||
import Form from './modules/ProductCategoryForm.vue';
|
import Form from './modules/ProductCategoryForm.vue';
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
},
|
},
|
||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
refresh: true,
|
refresh: true,
|
||||||
search:true,
|
search: true,
|
||||||
},
|
},
|
||||||
treeConfig: {
|
treeConfig: {
|
||||||
parentField: 'parentId',
|
parentField: 'parentId',
|
||||||
@@ -95,7 +95,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
icon: ACTION_ICON.ADD,
|
icon: ACTION_ICON.ADD,
|
||||||
onClick: handleCreate,
|
onClick: handleCreate,
|
||||||
},
|
},
|
||||||
|
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { getSimpleProductCategoryList } from '#/api/iot/product/category';
|
|||||||
import {
|
import {
|
||||||
deleteProduct,
|
deleteProduct,
|
||||||
exportProduct,
|
exportProduct,
|
||||||
getProductPage
|
getProductPage,
|
||||||
} from '#/api/iot/product/product';
|
} from '#/api/iot/product/product';
|
||||||
|
|
||||||
/** 新增/修改产品的表单 */
|
/** 新增/修改产品的表单 */
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
|||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Button, Card, Image, Input, Space } from 'ant-design-vue';
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { Button, Card, Image, Input, Space } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import ProductForm from './modules/ProductForm.vue';
|
|
||||||
// @ts-ignore
|
|
||||||
import ProductCardView from './modules/ProductCardView.vue';
|
|
||||||
import {
|
import {
|
||||||
getCategoryName,
|
getCategoryName,
|
||||||
handleDeleteProduct,
|
handleDeleteProduct,
|
||||||
@@ -23,12 +21,15 @@ import {
|
|||||||
useGridColumns,
|
useGridColumns,
|
||||||
useImagePreview,
|
useImagePreview,
|
||||||
} from './data';
|
} from './data';
|
||||||
|
// @ts-ignore
|
||||||
|
import ProductCardView from './modules/ProductCardView.vue';
|
||||||
|
import ProductForm from './modules/ProductForm.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProduct' });
|
defineOptions({ name: 'IoTProduct' });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const categoryList = ref<any[]>([]);
|
const categoryList = ref<any[]>([]);
|
||||||
const viewMode = ref<'list' | 'card'>('card');
|
const viewMode = ref<'card' | 'list'>('card');
|
||||||
const cardViewRef = ref();
|
const cardViewRef = ref();
|
||||||
|
|
||||||
// 搜索参数
|
// 搜索参数
|
||||||
@@ -154,13 +155,13 @@ onMounted(() => {
|
|||||||
<!-- 统一搜索工具栏 -->
|
<!-- 统一搜索工具栏 -->
|
||||||
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
<Card :body-style="{ padding: '16px' }" class="mb-4">
|
||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<div class="flex items-center gap-3 mb-3">
|
<div class="mb-3 flex items-center gap-3">
|
||||||
<Input
|
<Input
|
||||||
v-model:value="searchParams.name"
|
v-model:value="searchParams.name"
|
||||||
placeholder="请输入产品名称"
|
placeholder="请输入产品名称"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@pressEnter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<span class="text-gray-400">产品名称</span>
|
<span class="text-gray-400">产品名称</span>
|
||||||
@@ -171,7 +172,7 @@ onMounted(() => {
|
|||||||
placeholder="请输入产品标识"
|
placeholder="请输入产品标识"
|
||||||
allow-clear
|
allow-clear
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@pressEnter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<span class="text-gray-400">ProductKey</span>
|
<span class="text-gray-400">ProductKey</span>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -12,14 +16,21 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
|
||||||
import { getDictLabel } from '@vben/hooks';
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
import { getProductPage } from '#/api/iot/product/product';
|
||||||
|
|
||||||
defineOptions({ name: 'ProductCardView' });
|
defineOptions({ name: 'ProductCardView' });
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
create: [];
|
||||||
|
delete: [row: any];
|
||||||
|
detail: [productId: number];
|
||||||
|
edit: [row: any];
|
||||||
|
thingModel: [productId: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
categoryList: any[];
|
categoryList: any[];
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
@@ -28,16 +39,6 @@ interface Props {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
create: [];
|
|
||||||
edit: [row: any];
|
|
||||||
delete: [row: any];
|
|
||||||
detail: [productId: number];
|
|
||||||
thingModel: [productId: number];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const list = ref<any[]>([]);
|
const list = ref<any[]>([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -111,42 +112,57 @@ defineExpose({
|
|||||||
:lg="6"
|
:lg="6"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
>
|
>
|
||||||
<Card
|
<Card :body-style="{ padding: '20px' }" class="product-card h-full">
|
||||||
:body-style="{ padding: '20px' }"
|
|
||||||
class="product-card h-full"
|
|
||||||
>
|
|
||||||
<!-- 顶部标题区域 -->
|
<!-- 顶部标题区域 -->
|
||||||
<div class="flex items-start mb-4">
|
<div class="mb-4 flex items-start">
|
||||||
<div class="product-icon">
|
<div class="product-icon">
|
||||||
<IconifyIcon icon="ant-design:inbox-outlined" class="text-[32px]" />
|
<IconifyIcon
|
||||||
|
icon="ant-design:inbox-outlined"
|
||||||
|
class="text-[32px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 flex-1 min-w-0">
|
<div class="ml-3 min-w-0 flex-1">
|
||||||
<div class="product-title">{{ item.name }}</div>
|
<div class="product-title">{{ item.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="flex items-start mb-4">
|
<div class="mb-4 flex items-start">
|
||||||
<div class="flex-1 info-list">
|
<div class="info-list flex-1">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">产品分类</span>
|
<span class="info-label">产品分类</span>
|
||||||
<span class="info-value text-primary">{{ getCategoryName(item.categoryId) }}</span>
|
<span class="info-value text-primary">{{
|
||||||
|
getCategoryName(item.categoryId)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">产品类型</span>
|
<span class="info-label">产品类型</span>
|
||||||
<Tag :color="getDeviceTypeColor(item.deviceType)" class="m-0 info-tag">
|
<Tag
|
||||||
{{ getDictLabel(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE, item.deviceType) }}
|
:color="getDeviceTypeColor(item.deviceType)"
|
||||||
|
class="info-tag m-0"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
getDictLabel(
|
||||||
|
DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE,
|
||||||
|
item.deviceType,
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">产品标识</span>
|
<span class="info-label">产品标识</span>
|
||||||
<Tooltip :title="item.productKey || item.id" placement="top">
|
<Tooltip :title="item.productKey || item.id" placement="top">
|
||||||
<span class="info-value product-key">{{ item.productKey || item.id }}</span>
|
<span class="info-value product-key">{{
|
||||||
|
item.productKey || item.id
|
||||||
|
}}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-3d-icon">
|
<div class="product-3d-icon">
|
||||||
<IconifyIcon icon="ant-design:box-plot-outlined" class="text-[80px]" />
|
<IconifyIcon
|
||||||
|
icon="ant-design:box-plot-outlined"
|
||||||
|
class="text-[80px]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -173,7 +189,10 @@ defineExpose({
|
|||||||
class="action-btn action-btn-model"
|
class="action-btn action-btn-model"
|
||||||
@click="emit('thingModel', item.id)"
|
@click="emit('thingModel', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="ant-design:apartment-outlined" class="mr-1" />
|
<IconifyIcon
|
||||||
|
icon="ant-design:apartment-outlined"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
物模型
|
物模型
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -374,4 +393,3 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
import { createProduct, getProduct, updateProduct, type IotProductApi } from '#/api/iot/product/product';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createProduct,
|
||||||
|
getProduct,
|
||||||
|
updateProduct,
|
||||||
|
} from '#/api/iot/product/product';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
<!-- IoT 产品选择器,使用弹窗展示 -->
|
<!-- IoT 产品选择器,使用弹窗展示 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getProductPage } from '#/api/iot/product/product';
|
import { getProductPage } from '#/api/iot/product/product';
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductTableSelect' });
|
defineOptions({ name: 'IoTProductTableSelect' });
|
||||||
|
|
||||||
interface Props {
|
|
||||||
multiple?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
multiple: false,
|
multiple: false,
|
||||||
});
|
});
|
||||||
@@ -23,6 +21,10 @@ const emit = defineEmits<{
|
|||||||
success: [product: IotProductApi.Product | IotProductApi.Product[]];
|
success: [product: IotProductApi.Product | IotProductApi.Product[]];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
multiple?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
title: '产品选择器',
|
title: '产品选择器',
|
||||||
onConfirm: handleConfirm,
|
onConfirm: handleConfirm,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { updateProductStatus } from '#/api/iot/product/product';
|
import { updateProductStatus } from '#/api/iot/product/product';
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
|
|
||||||
import ProductForm from '../ProductForm.vue';
|
import ProductForm from '../ProductForm.vue';
|
||||||
|
|
||||||
@@ -37,7 +38,10 @@ const copyToClipboard = async (text: string) => {
|
|||||||
|
|
||||||
/** 跳转到设备管理 */
|
/** 跳转到设备管理 */
|
||||||
const goToDeviceList = (productId: number) => {
|
const goToDeviceList = (productId: number) => {
|
||||||
router.push({ path: '/iot/device/device', query: { productId: String(productId) } });
|
router.push({
|
||||||
|
path: '/iot/device/device',
|
||||||
|
query: { productId: String(productId) },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 打开编辑表单 */
|
/** 打开编辑表单 */
|
||||||
@@ -102,12 +106,18 @@ const confirmUnpublish = async (id: number) => {
|
|||||||
<a-descriptions :column="1">
|
<a-descriptions :column="1">
|
||||||
<a-descriptions-item label="ProductKey">
|
<a-descriptions-item label="ProductKey">
|
||||||
{{ product.productKey }}
|
{{ product.productKey }}
|
||||||
<a-button size="small" class="ml-2" @click="copyToClipboard(product.productKey || '')">
|
<a-button
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
@click="copyToClipboard(product.productKey || '')"
|
||||||
|
>
|
||||||
复制
|
复制
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="设备总数">
|
<a-descriptions-item label="设备总数">
|
||||||
<span class="ml-5 mr-2">{{ product.deviceCount ?? '加载中...' }}</span>
|
<span class="ml-5 mr-2">{{
|
||||||
|
product.deviceCount ?? '加载中...'
|
||||||
|
}}</span>
|
||||||
<a-button size="small" @click="goToDeviceList(product.id!)">
|
<a-button size="small" @click="goToDeviceList(product.id!)">
|
||||||
前往管理
|
前往管理
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
|
||||||
import { DictTag } from '#/components/dict-tag';
|
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
import { DeviceTypeEnum } from '#/api/iot/product/product';
|
import { DeviceTypeEnum } from '#/api/iot/product/product';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
product: IotProductApi.Product;
|
product: IotProductApi.Product;
|
||||||
@@ -28,7 +29,10 @@ const formatDate = (date?: Date | string) => {
|
|||||||
{{ product.categoryName || '-' }}
|
{{ product.categoryName || '-' }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="设备类型">
|
<a-descriptions-item label="设备类型">
|
||||||
<DictTag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
|
<DictTag
|
||||||
|
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||||
|
:value="product.deviceType"
|
||||||
|
/>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="定位类型">
|
<a-descriptions-item label="定位类型">
|
||||||
{{ product.locationType ?? '-' }}
|
{{ product.locationType ?? '-' }}
|
||||||
@@ -43,7 +47,11 @@ const formatDate = (date?: Date | string) => {
|
|||||||
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="product.status" />
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item
|
<a-descriptions-item
|
||||||
v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(product.deviceType!)"
|
v-if="
|
||||||
|
[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(
|
||||||
|
product.deviceType!,
|
||||||
|
)
|
||||||
|
"
|
||||||
label="联网方式"
|
label="联网方式"
|
||||||
>
|
>
|
||||||
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
|
<DictTag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
import { onMounted, provide, ref } from 'vue';
|
import { onMounted, provide, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { getProduct } from '#/api/iot/product/product';
|
import { message } from 'ant-design-vue';
|
||||||
import type { IotProductApi } from '#/api/iot/product/product';
|
|
||||||
import { getDeviceCount } from '#/api/iot/device/device';
|
import { getDeviceCount } from '#/api/iot/device/device';
|
||||||
|
import { getProduct } from '#/api/iot/product/product';
|
||||||
|
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||||
|
|
||||||
import ProductDetailsHeader from './ProductDetailsHeader.vue';
|
import ProductDetailsHeader from './ProductDetailsHeader.vue';
|
||||||
import ProductDetailsInfo from './ProductDetailsInfo.vue';
|
import ProductDetailsInfo from './ProductDetailsInfo.vue';
|
||||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
|
||||||
|
|
||||||
defineOptions({ name: 'IoTProductDetail' });
|
defineOptions({ name: 'IoTProductDetail' });
|
||||||
|
|
||||||
@@ -43,7 +45,12 @@ const getDeviceCountData = async (productId: number) => {
|
|||||||
try {
|
try {
|
||||||
return await getDeviceCount(productId);
|
return await getDeviceCount(productId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching device count:', error, 'productId:', productId);
|
console.error(
|
||||||
|
'Error fetching device count:',
|
||||||
|
error,
|
||||||
|
'productId:',
|
||||||
|
productId,
|
||||||
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -84,7 +91,10 @@ onMounted(async () => {
|
|||||||
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
|
<ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="thingModel" tab="物模型(功能定义)">
|
<a-tab-pane key="thingModel" tab="物模型(功能定义)">
|
||||||
<IoTProductThingModel v-if="activeTab === 'thingModel'" :product-id="id" />
|
<IoTProductThingModel
|
||||||
|
v-if="activeTab === 'thingModel'"
|
||||||
|
:product-id="id"
|
||||||
|
/>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
import { getDataSinkSimpleList } from '#/api/iot/rule/data/sink';
|
import { getDataSinkSimpleList } from '#/api/iot/rule/data/sink';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useRuleFormSchema } from './data';
|
|
||||||
import SourceConfigForm from './components/SourceConfigForm.vue';
|
import SourceConfigForm from './components/SourceConfigForm.vue';
|
||||||
|
import { useRuleFormSchema } from './data';
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<any>();
|
const formData = ref<any>();
|
||||||
@@ -110,7 +110,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
<Modal class="w-4/5" :title="getTitle">
|
<Modal class="w-4/5" :title="getTitle">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
<div class="mx-4 mt-4">
|
<div class="mx-4 mt-4">
|
||||||
<div class="text-sm font-medium mb-2">数据源配置</div>
|
<div class="mb-2 text-sm font-medium">数据源配置</div>
|
||||||
<SourceConfigForm ref="sourceConfigRef" />
|
<SourceConfigForm ref="sourceConfigRef" />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { Table, Select, Button, Form, FormItem } from 'ant-design-vue';
|
import { Button, Form, FormItem, Select, Table } from 'ant-design-vue';
|
||||||
|
|
||||||
import { getSimpleProductList } from '#/api/iot/product/product';
|
|
||||||
import { getSimpleDeviceList } from '#/api/iot/device/device';
|
import { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||||
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||||
import {
|
import {
|
||||||
IotDeviceMessageMethodEnum,
|
IotDeviceMessageMethodEnum,
|
||||||
@@ -25,13 +25,17 @@ const formRef = ref(); // 表单 Ref
|
|||||||
|
|
||||||
// 获取上行消息方法列表
|
// 获取上行消息方法列表
|
||||||
const upstreamMethods = computed(() => {
|
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,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 判断是否需要显示标识符选择器 */
|
/** 判断是否需要显示标识符选择器 */
|
||||||
|
|||||||
@@ -145,4 +145,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
} from '#/api/iot/rule/data';
|
} from '#/api/iot/rule/data';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import { useSinkFormSchema } from './data';
|
|
||||||
import {
|
import {
|
||||||
HttpConfigForm,
|
HttpConfigForm,
|
||||||
KafkaMQConfigForm,
|
KafkaMQConfigForm,
|
||||||
@@ -22,6 +21,9 @@ import {
|
|||||||
RedisStreamConfigForm,
|
RedisStreamConfigForm,
|
||||||
RocketMQConfigForm,
|
RocketMQConfigForm,
|
||||||
} from './config';
|
} from './config';
|
||||||
|
import { useSinkFormSchema } from './data';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
const IotDataSinkTypeEnum = {
|
const IotDataSinkTypeEnum = {
|
||||||
HTTP: 1,
|
HTTP: 1,
|
||||||
@@ -32,7 +34,6 @@ const IotDataSinkTypeEnum = {
|
|||||||
REDIS_STREAM: 6,
|
REDIS_STREAM: 6,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
|
||||||
const formData = ref<any>();
|
const formData = ref<any>();
|
||||||
|
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
@@ -118,7 +119,7 @@ watch(
|
|||||||
<Modal class="w-3/5" :title="getTitle">
|
<Modal class="w-3/5" :title="getTitle">
|
||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
<div v-if="formData" class="mx-4 mt-4">
|
<div v-if="formData" class="mx-4 mt-4">
|
||||||
<div class="text-sm font-medium mb-2">配置信息</div>
|
<div class="mb-2 text-sm font-medium">配置信息</div>
|
||||||
<HttpConfigForm
|
<HttpConfigForm
|
||||||
v-if="IotDataSinkTypeEnum.HTTP === formData.type"
|
v-if="IotDataSinkTypeEnum.HTTP === formData.type"
|
||||||
v-model="formData.config"
|
v-model="formData.config"
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Input, Select, FormItem } from 'ant-design-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { FormItem, Input, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import KeyValueEditor from './components/KeyValueEditor.vue';
|
import KeyValueEditor from './components/KeyValueEditor.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'HttpConfigForm' });
|
defineOptions({ name: 'HttpConfigForm' });
|
||||||
@@ -33,10 +34,10 @@ onMounted(() => {
|
|||||||
if (config.value.url) {
|
if (config.value.url) {
|
||||||
if (config.value.url.startsWith('https://')) {
|
if (config.value.url.startsWith('https://')) {
|
||||||
urlPrefix.value = 'https://';
|
urlPrefix.value = 'https://';
|
||||||
urlPath.value = config.value.url.substring(8);
|
urlPath.value = config.value.url.slice(8);
|
||||||
} else if (config.value.url.startsWith('http://')) {
|
} else if (config.value.url.startsWith('http://')) {
|
||||||
urlPrefix.value = 'http://';
|
urlPrefix.value = 'http://';
|
||||||
urlPath.value = config.value.url.substring(7);
|
urlPath.value = config.value.url.slice(7);
|
||||||
} else {
|
} else {
|
||||||
urlPath.value = config.value.url;
|
urlPath.value = config.value.url;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Input, Switch, FormItem } from 'ant-design-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { FormItem, Input, Switch } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'KafkaMQConfigForm' });
|
defineOptions({ name: 'KafkaMQConfigForm' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Input, FormItem } from 'ant-design-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { FormItem, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'MqttConfigForm' });
|
defineOptions({ name: 'MqttConfigForm' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Input, InputNumber, FormItem } from 'ant-design-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { FormItem, Input, InputNumber } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'RabbitMQConfigForm' });
|
defineOptions({ name: 'RabbitMQConfigForm' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Input, InputNumber, FormItem } from 'ant-design-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { FormItem, Input, InputNumber } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'RedisStreamConfigForm' });
|
defineOptions({ name: 'RedisStreamConfigForm' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Input, FormItem } from 'ant-design-vue';
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { FormItem, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'RocketMQConfigForm' });
|
defineOptions({ name: 'RocketMQConfigForm' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,5 +1,66 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Delete, Plus } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'KeyValueEditor' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
addButtonText: string;
|
||||||
|
modelValue: Record<string, string>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
interface KeyValueItem {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = ref<KeyValueItem[]>([]); // 内部 key-value 项列表
|
||||||
|
|
||||||
|
/** 添加项目 */
|
||||||
|
const addItem = () => {
|
||||||
|
items.value.push({ key: '', value: '' });
|
||||||
|
updateModelValue();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 移除项目 */
|
||||||
|
const removeItem = (index: number) => {
|
||||||
|
items.value.splice(index, 1);
|
||||||
|
updateModelValue();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 更新 modelValue */
|
||||||
|
const updateModelValue = () => {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
items.value.forEach((item) => {
|
||||||
|
if (item.key) {
|
||||||
|
result[item.key] = item.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emit('update:modelValue', result);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听项目变化 */
|
||||||
|
watch(items, updateModelValue, { deep: true });
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
// 列表有值后以列表中的值为准
|
||||||
|
if (isEmpty(val) || !isEmpty(items.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
items.value = Object.entries(props.modelValue).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-for="(item, index) in items" :key="index" class="flex mb-2 w-full">
|
<div v-for="(item, index) in items" :key="index" class="mb-2 flex w-full">
|
||||||
<el-input v-model="item.key" class="mr-2" placeholder="键" />
|
<el-input v-model="item.key" class="mr-2" placeholder="键" />
|
||||||
<el-input v-model="item.value" placeholder="值" />
|
<el-input v-model="item.value" placeholder="值" />
|
||||||
<el-button class="ml-2" text type="danger" @click="removeItem(index)">
|
<el-button class="ml-2" text type="danger" @click="removeItem(index)">
|
||||||
@@ -16,58 +77,3 @@
|
|||||||
{{ addButtonText }}
|
{{ addButtonText }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
|
||||||
import { isEmpty } from '@vben/utils'
|
|
||||||
|
|
||||||
defineOptions({ name: 'KeyValueEditor' })
|
|
||||||
|
|
||||||
interface KeyValueItem {
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: Record<string, string>
|
|
||||||
addButtonText: string
|
|
||||||
}>()
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
|
||||||
const items = ref<KeyValueItem[]>([]) // 内部 key-value 项列表
|
|
||||||
|
|
||||||
/** 添加项目 */
|
|
||||||
const addItem = () => {
|
|
||||||
items.value.push({ key: '', value: '' })
|
|
||||||
updateModelValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 移除项目 */
|
|
||||||
const removeItem = (index: number) => {
|
|
||||||
items.value.splice(index, 1)
|
|
||||||
updateModelValue()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 更新 modelValue */
|
|
||||||
const updateModelValue = () => {
|
|
||||||
const result: Record<string, string> = {}
|
|
||||||
items.value.forEach((item) => {
|
|
||||||
if (item.key) {
|
|
||||||
result[item.key] = item.value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
emit('update:modelValue', result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听项目变化 */
|
|
||||||
watch(items, updateModelValue, { deep: true })
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(val) => {
|
|
||||||
// 列表有值后以列表中的值为准
|
|
||||||
if (isEmpty(val) || !isEmpty(items.value)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
items.value = Object.entries(props.modelValue).map(([key, value]) => ({ key, value }))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import HttpConfigForm from './HttpConfigForm.vue'
|
export { default as HttpConfigForm } from './HttpConfigForm.vue';
|
||||||
import MqttConfigForm from './MqttConfigForm.vue'
|
export { default as KafkaMQConfigForm } from './KafkaMQConfigForm.vue';
|
||||||
import RocketMQConfigForm from './RocketMQConfigForm.vue'
|
export { default as MqttConfigForm } from './MqttConfigForm.vue';
|
||||||
import KafkaMQConfigForm from './KafkaMQConfigForm.vue'
|
export { default as RabbitMQConfigForm } from './RabbitMQConfigForm.vue';
|
||||||
import RabbitMQConfigForm from './RabbitMQConfigForm.vue'
|
export { default as RedisStreamConfigForm } from './RedisStreamConfigForm.vue';
|
||||||
import RedisStreamConfigForm from './RedisStreamConfigForm.vue'
|
export { default as RocketMQConfigForm } from './RocketMQConfigForm.vue';
|
||||||
|
|
||||||
export {
|
|
||||||
HttpConfigForm,
|
|
||||||
MqttConfigForm,
|
|
||||||
RocketMQConfigForm,
|
|
||||||
KafkaMQConfigForm,
|
|
||||||
RabbitMQConfigForm,
|
|
||||||
RedisStreamConfigForm
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -150,4 +150,3 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import { Button } from 'ant-design-vue';
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
defineOptions({ name: 'IotRuleDataBridge' });
|
defineOptions({ name: 'IotRuleDataBridge' });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page
|
<Page description="物聯網規則引擎 - 數據橋接" title="數據橋接">
|
||||||
description="物聯網規則引擎 - 數據橋接"
|
|
||||||
title="數據橋接"
|
|
||||||
>
|
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
|
|||||||
@@ -1,69 +1,37 @@
|
|||||||
<template>
|
|
||||||
<el-drawer
|
|
||||||
v-model="drawerVisible"
|
|
||||||
:title="drawerTitle"
|
|
||||||
size="80%"
|
|
||||||
direction="rtl"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
:close-on-press-escape="false"
|
|
||||||
@close="handleClose"
|
|
||||||
>
|
|
||||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px">
|
|
||||||
<!-- 基础信息配置 -->
|
|
||||||
<BasicInfoSection v-model="formData" :rules="formRules" />
|
|
||||||
<!-- 触发器配置 -->
|
|
||||||
<TriggerSection v-model:triggers="formData.triggers" />
|
|
||||||
<!-- 执行器配置 -->
|
|
||||||
<ActionSection v-model:actions="formData.actions" />
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<div class="drawer-footer">
|
|
||||||
<el-button :disabled="submitLoading" type="primary" @click="handleSubmit">
|
|
||||||
<Icon icon="ep:check" />
|
|
||||||
确 定
|
|
||||||
</el-button>
|
|
||||||
<el-button @click="handleClose">
|
|
||||||
<Icon icon="ep:close" />
|
|
||||||
取 消
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-drawer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { CommonStatusEnum } from '@/utils/constants';
|
||||||
import BasicInfoSection from './sections/BasicInfoSection.vue'
|
import { useVModel } from '@vueuse/core';
|
||||||
import TriggerSection from './sections/TriggerSection.vue'
|
import { ElMessage } from 'element-plus';
|
||||||
import ActionSection from './sections/ActionSection.vue'
|
|
||||||
import { IotSceneRule } from '#/api/iot/rule/scene'
|
import { IotSceneRule, RuleSceneApi } from '#/api/iot/rule/scene';
|
||||||
import { RuleSceneApi } from '#/api/iot/rule/scene'
|
|
||||||
import {
|
import {
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
IotRuleSceneActionTypeEnum,
|
IotRuleSceneActionTypeEnum,
|
||||||
isDeviceTrigger
|
IotRuleSceneTriggerTypeEnum,
|
||||||
} from '#/views/iot/utils/constants'
|
isDeviceTrigger,
|
||||||
import { ElMessage } from 'element-plus'
|
} from '#/views/iot/utils/constants';
|
||||||
import { CommonStatusEnum } from '@/utils/constants'
|
|
||||||
|
import ActionSection from './sections/ActionSection.vue';
|
||||||
|
import BasicInfoSection from './sections/BasicInfoSection.vue';
|
||||||
|
import TriggerSection from './sections/TriggerSection.vue';
|
||||||
|
|
||||||
/** IoT 场景联动规则表单 - 主表单组件 */
|
/** IoT 场景联动规则表单 - 主表单组件 */
|
||||||
defineOptions({ name: 'RuleSceneForm' })
|
defineOptions({ name: 'RuleSceneForm' });
|
||||||
|
|
||||||
/** 组件属性定义 */
|
/** 组件属性定义 */
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 抽屉显示状态 */
|
/** 抽屉显示状态 */
|
||||||
modelValue: boolean
|
modelValue: boolean;
|
||||||
/** 编辑的场景联动规则数据 */
|
/** 编辑的场景联动规则数据 */
|
||||||
ruleScene?: IotSceneRule
|
ruleScene?: IotSceneRule;
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
/** 组件事件定义 */
|
/** 组件事件定义 */
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: boolean): void
|
(e: 'update:modelValue', value: boolean): void;
|
||||||
(e: 'success'): void
|
(e: 'success'): void;
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const drawerVisible = useVModel(props, 'modelValue', emit) // 抽屉显示状态
|
const drawerVisible = useVModel(props, 'modelValue', emit); // 抽屉显示状态
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建默认的表单数据
|
* 创建默认的表单数据
|
||||||
@@ -83,15 +51,15 @@ const createDefaultFormData = (): IotSceneRule => {
|
|||||||
operator: undefined,
|
operator: undefined,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
cronExpression: undefined,
|
cronExpression: undefined,
|
||||||
conditionGroups: [] // 空的条件组数组
|
conditionGroups: [], // 空的条件组数组
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
actions: []
|
actions: [],
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const formRef = ref() // 表单引用
|
const formRef = ref(); // 表单引用
|
||||||
const formData = ref<IotSceneRule>(createDefaultFormData()) // 表单数据
|
const formData = ref<IotSceneRule>(createDefaultFormData()); // 表单数据
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 触发器校验器
|
* 触发器校验器
|
||||||
@@ -101,54 +69,57 @@ const formData = ref<IotSceneRule>(createDefaultFormData()) // 表单数据
|
|||||||
*/
|
*/
|
||||||
const validateTriggers = (_rule: any, value: any, callback: any) => {
|
const validateTriggers = (_rule: any, value: any, callback: any) => {
|
||||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
callback(new Error('至少需要一个触发器'))
|
callback(new Error('至少需要一个触发器'));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
for (const [i, trigger] of value.entries()) {
|
||||||
const trigger = value[i]
|
|
||||||
|
|
||||||
// 校验触发器类型
|
// 校验触发器类型
|
||||||
if (!trigger.type) {
|
if (!trigger.type) {
|
||||||
callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`))
|
callback(new Error(`触发器 ${i + 1}: 触发器类型不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验设备触发器
|
// 校验设备触发器
|
||||||
if (isDeviceTrigger(trigger.type)) {
|
if (isDeviceTrigger(trigger.type)) {
|
||||||
if (!trigger.productId) {
|
if (!trigger.productId) {
|
||||||
callback(new Error(`触发器 ${i + 1}: 产品不能为空`))
|
callback(new Error(`触发器 ${i + 1}: 产品不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!trigger.deviceId) {
|
if (!trigger.deviceId) {
|
||||||
callback(new Error(`触发器 ${i + 1}: 设备不能为空`))
|
callback(new Error(`触发器 ${i + 1}: 设备不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!trigger.identifier) {
|
if (!trigger.identifier) {
|
||||||
callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`))
|
callback(new Error(`触发器 ${i + 1}: 物模型标识符不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!trigger.operator) {
|
if (!trigger.operator) {
|
||||||
callback(new Error(`触发器 ${i + 1}: 操作符不能为空`))
|
callback(new Error(`触发器 ${i + 1}: 操作符不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
|
if (
|
||||||
callback(new Error(`触发器 ${i + 1}: 参数值不能为空`))
|
trigger.value === undefined ||
|
||||||
return
|
trigger.value === null ||
|
||||||
|
trigger.value === ''
|
||||||
|
) {
|
||||||
|
callback(new Error(`触发器 ${i + 1}: 参数值不能为空`));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验定时触发器
|
// 校验定时触发器
|
||||||
if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
if (
|
||||||
if (!trigger.cronExpression) {
|
trigger.type === IotRuleSceneTriggerTypeEnum.TIMER &&
|
||||||
callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`))
|
!trigger.cronExpression
|
||||||
return
|
) {
|
||||||
}
|
callback(new Error(`触发器 ${i + 1}: CRON表达式不能为空`));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback()
|
callback();
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行器校验器
|
* 执行器校验器
|
||||||
@@ -158,17 +129,15 @@ const validateTriggers = (_rule: any, value: any, callback: any) => {
|
|||||||
*/
|
*/
|
||||||
const validateActions = (_rule: any, value: any, callback: any) => {
|
const validateActions = (_rule: any, value: any, callback: any) => {
|
||||||
if (!value || !Array.isArray(value) || value.length === 0) {
|
if (!value || !Array.isArray(value) || value.length === 0) {
|
||||||
callback(new Error('至少需要一个执行器'))
|
callback(new Error('至少需要一个执行器'));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < value.length; i++) {
|
for (const [i, action] of value.entries()) {
|
||||||
const action = value[i]
|
|
||||||
|
|
||||||
// 校验执行器类型
|
// 校验执行器类型
|
||||||
if (!action.type) {
|
if (!action.type) {
|
||||||
callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`))
|
callback(new Error(`执行器 ${i + 1}: 执行器类型不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验设备控制执行器
|
// 校验设备控制执行器
|
||||||
@@ -177,47 +146,53 @@ const validateActions = (_rule: any, value: any, callback: any) => {
|
|||||||
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
) {
|
) {
|
||||||
if (!action.productId) {
|
if (!action.productId) {
|
||||||
callback(new Error(`执行器 ${i + 1}: 产品不能为空`))
|
callback(new Error(`执行器 ${i + 1}: 产品不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (!action.deviceId) {
|
if (!action.deviceId) {
|
||||||
callback(new Error(`执行器 ${i + 1}: 设备不能为空`))
|
callback(new Error(`执行器 ${i + 1}: 设备不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务调用需要验证服务标识符
|
// 服务调用需要验证服务标识符
|
||||||
if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
|
if (
|
||||||
if (!action.identifier) {
|
action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE &&
|
||||||
callback(new Error(`执行器 ${i + 1}: 服务不能为空`))
|
!action.identifier
|
||||||
return
|
) {
|
||||||
}
|
callback(new Error(`执行器 ${i + 1}: 服务不能为空`));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!action.params || Object.keys(action.params).length === 0) {
|
if (!action.params || Object.keys(action.params).length === 0) {
|
||||||
callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`))
|
callback(new Error(`执行器 ${i + 1}: 参数配置不能为空`));
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验告警执行器
|
// 校验告警执行器
|
||||||
if (
|
if (
|
||||||
action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
|
(action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
|
||||||
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
|
action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER) &&
|
||||||
|
!action.alertConfigId
|
||||||
) {
|
) {
|
||||||
if (!action.alertConfigId) {
|
callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`));
|
||||||
callback(new Error(`执行器 ${i + 1}: 告警配置不能为空`))
|
return;
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback()
|
callback();
|
||||||
}
|
};
|
||||||
|
|
||||||
const formRules = reactive({
|
const formRules = reactive({
|
||||||
name: [
|
name: [
|
||||||
{ required: true, message: '场景名称不能为空', trigger: 'blur' },
|
{ required: true, message: '场景名称不能为空', trigger: 'blur' },
|
||||||
{ type: 'string', min: 1, max: 50, message: '场景名称长度应在1-50个字符之间', trigger: 'blur' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
min: 1,
|
||||||
|
max: 50,
|
||||||
|
message: '场景名称长度应在1-50个字符之间',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
status: [
|
status: [
|
||||||
{ required: true, message: '场景状态不能为空', trigger: 'change' },
|
{ required: true, message: '场景状态不能为空', trigger: 'change' },
|
||||||
@@ -225,61 +200,70 @@ const formRules = reactive({
|
|||||||
type: 'enum',
|
type: 'enum',
|
||||||
enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
|
enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
|
||||||
message: '状态值必须为启用或禁用',
|
message: '状态值必须为启用或禁用',
|
||||||
trigger: 'change'
|
trigger: 'change',
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
description: [
|
description: [
|
||||||
{ type: 'string', max: 200, message: '场景描述不能超过200个字符', trigger: 'blur' }
|
{
|
||||||
|
type: 'string',
|
||||||
|
max: 200,
|
||||||
|
message: '场景描述不能超过200个字符',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
triggers: [{ required: true, validator: validateTriggers, trigger: 'change' }],
|
triggers: [
|
||||||
actions: [{ required: true, validator: validateActions, trigger: 'change' }]
|
{ required: true, validator: validateTriggers, trigger: 'change' },
|
||||||
}) // 表单校验规则
|
],
|
||||||
|
actions: [{ required: true, validator: validateActions, trigger: 'change' }],
|
||||||
|
}); // 表单校验规则
|
||||||
|
|
||||||
const submitLoading = ref(false) // 提交加载状态
|
const submitLoading = ref(false); // 提交加载状态
|
||||||
const isEdit = ref(false) // 是否为编辑模式
|
const isEdit = ref(false); // 是否为编辑模式
|
||||||
const drawerTitle = computed(() => (isEdit.value ? '编辑场景联动规则' : '新增场景联动规则')) // 抽屉标题
|
const drawerTitle = computed(() =>
|
||||||
|
isEdit.value ? '编辑场景联动规则' : '新增场景联动规则',
|
||||||
|
); // 抽屉标题
|
||||||
|
|
||||||
/** 提交表单 */
|
/** 提交表单 */
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// 校验表单
|
// 校验表单
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return;
|
||||||
const valid = await formRef.value.validate()
|
const valid = await formRef.value.validate();
|
||||||
if (!valid) return
|
if (!valid) return;
|
||||||
|
|
||||||
// 提交请求
|
// 提交请求
|
||||||
submitLoading.value = true
|
submitLoading.value = true;
|
||||||
try {
|
try {
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
// 更新场景联动规则
|
// 更新场景联动规则
|
||||||
await RuleSceneApi.updateRuleScene(formData.value)
|
await RuleSceneApi.updateRuleScene(formData.value);
|
||||||
ElMessage.success('更新成功')
|
ElMessage.success('更新成功');
|
||||||
} else {
|
} else {
|
||||||
// 创建场景联动规则
|
// 创建场景联动规则
|
||||||
await RuleSceneApi.createRuleScene(formData.value)
|
await RuleSceneApi.createRuleScene(formData.value);
|
||||||
ElMessage.success('创建成功')
|
ElMessage.success('创建成功');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关闭抽屉并触发成功事件
|
// 关闭抽屉并触发成功事件
|
||||||
drawerVisible.value = false
|
drawerVisible.value = false;
|
||||||
emit('success')
|
emit('success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('保存失败:', error)
|
console.error('保存失败:', error);
|
||||||
ElMessage.error(isEdit.value ? '更新失败' : '创建失败')
|
ElMessage.error(isEdit.value ? '更新失败' : '创建失败');
|
||||||
} finally {
|
} finally {
|
||||||
submitLoading.value = false
|
submitLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/** 处理抽屉关闭事件 */
|
/** 处理抽屉关闭事件 */
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
drawerVisible.value = false
|
drawerVisible.value = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
/** 初始化表单数据 */
|
/** 初始化表单数据 */
|
||||||
const initFormData = () => {
|
const initFormData = () => {
|
||||||
if (props.ruleScene) {
|
if (props.ruleScene) {
|
||||||
// 编辑模式:数据结构已对齐,直接使用后端数据
|
// 编辑模式:数据结构已对齐,直接使用后端数据
|
||||||
isEdit.value = true
|
isEdit.value = true;
|
||||||
formData.value = {
|
formData.value = {
|
||||||
...props.ruleScene,
|
...props.ruleScene,
|
||||||
// 确保触发器数组不为空
|
// 确保触发器数组不为空
|
||||||
@@ -294,37 +278,79 @@ const initFormData = () => {
|
|||||||
operator: undefined,
|
operator: undefined,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
cronExpression: undefined,
|
cronExpression: undefined,
|
||||||
conditionGroups: []
|
conditionGroups: [],
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
// 确保执行器数组不为空
|
// 确保执行器数组不为空
|
||||||
actions: props.ruleScene.actions || []
|
actions: props.ruleScene.actions || [],
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
// 新增模式:使用默认数据
|
// 新增模式:使用默认数据
|
||||||
isEdit.value = false
|
isEdit.value = false;
|
||||||
formData.value = createDefaultFormData()
|
formData.value = createDefaultFormData();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/** 监听抽屉显示 */
|
/** 监听抽屉显示 */
|
||||||
watch(drawerVisible, async (visible) => {
|
watch(drawerVisible, async (visible) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
initFormData()
|
initFormData();
|
||||||
// 重置表单验证状态
|
// 重置表单验证状态
|
||||||
await nextTick()
|
await nextTick();
|
||||||
formRef.value?.clearValidate()
|
formRef.value?.clearValidate();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
/** 监听编辑数据变化 */
|
/** 监听编辑数据变化 */
|
||||||
watch(
|
watch(
|
||||||
() => props.ruleScene,
|
() => props.ruleScene,
|
||||||
() => {
|
() => {
|
||||||
if (drawerVisible.value) {
|
if (drawerVisible.value) {
|
||||||
initFormData()
|
initFormData();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true },
|
||||||
)
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawerVisible"
|
||||||
|
:title="drawerTitle"
|
||||||
|
size="80%"
|
||||||
|
direction="rtl"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
:close-on-press-escape="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="110px"
|
||||||
|
>
|
||||||
|
<!-- 基础信息配置 -->
|
||||||
|
<BasicInfoSection v-model="formData" :rules="formRules" />
|
||||||
|
<!-- 触发器配置 -->
|
||||||
|
<TriggerSection v-model:triggers="formData.triggers" />
|
||||||
|
<!-- 执行器配置 -->
|
||||||
|
<ActionSection v-model:actions="formData.actions" />
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="drawer-footer">
|
||||||
|
<el-button
|
||||||
|
:disabled="submitLoading"
|
||||||
|
type="primary"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:check" />
|
||||||
|
确 定
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleClose">
|
||||||
|
<Icon icon="ep:close" />
|
||||||
|
取 消
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,56 @@
|
|||||||
<!-- 告警配置组件 -->
|
<!-- 告警配置组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { AlertConfigApi } from '#/api/iot/alert/config';
|
||||||
|
|
||||||
|
/** 告警配置组件 */
|
||||||
|
defineOptions({ name: 'AlertConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value?: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const localValue = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const loading = ref(false); // 加载状态
|
||||||
|
const alertConfigs = ref<any[]>([]); // 告警配置列表
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理选择变化事件
|
||||||
|
* @param value 选中的值
|
||||||
|
*/
|
||||||
|
const handleChange = (value?: number) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载告警配置列表
|
||||||
|
*/
|
||||||
|
const loadAlertConfigs = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const data = await AlertConfigApi.getAlertConfigPage({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
enabled: true, // 只加载启用的配置
|
||||||
|
});
|
||||||
|
alertConfigs.value = data.list || [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadAlertConfigs();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<el-form-item label="告警配置" required>
|
<el-form-item label="告警配置" required>
|
||||||
@@ -28,54 +80,3 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { AlertConfigApi } from '#/api/iot/alert/config'
|
|
||||||
|
|
||||||
/** 告警配置组件 */
|
|
||||||
defineOptions({ name: 'AlertConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value?: number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
const loading = ref(false) // 加载状态
|
|
||||||
const alertConfigs = ref<any[]>([]) // 告警配置列表
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理选择变化事件
|
|
||||||
* @param value 选中的值
|
|
||||||
*/
|
|
||||||
const handleChange = (value?: number) => {
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载告警配置列表
|
|
||||||
*/
|
|
||||||
const loadAlertConfigs = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const data = await AlertConfigApi.getAlertConfigPage({
|
|
||||||
pageNo: 1,
|
|
||||||
pageSize: 100,
|
|
||||||
enabled: true // 只加载启用的配置
|
|
||||||
})
|
|
||||||
alertConfigs.value = data.list || []
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时加载数据
|
|
||||||
onMounted(() => {
|
|
||||||
loadAlertConfigs()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,6 +1,158 @@
|
|||||||
<!-- 单个条件配置组件 -->
|
<!-- 单个条件配置组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConditionTypeOptions,
|
||||||
|
IoTDeviceStatusEnum,
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import ValueInput from '../inputs/ValueInput.vue';
|
||||||
|
import DeviceSelector from '../selectors/DeviceSelector.vue';
|
||||||
|
import OperatorSelector from '../selectors/OperatorSelector.vue';
|
||||||
|
import ProductSelector from '../selectors/ProductSelector.vue';
|
||||||
|
import PropertySelector from '../selectors/PropertySelector.vue';
|
||||||
|
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue';
|
||||||
|
|
||||||
|
/** 单个条件配置组件 */
|
||||||
|
defineOptions({ name: 'ConditionConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: TriggerCondition;
|
||||||
|
triggerType: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: TriggerCondition): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 获取设备状态选项 */
|
||||||
|
const deviceStatusOptions = [
|
||||||
|
{
|
||||||
|
value: IoTDeviceStatusEnum.ONLINE.value,
|
||||||
|
label: IoTDeviceStatusEnum.ONLINE.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IoTDeviceStatusEnum.OFFLINE.value,
|
||||||
|
label: IoTDeviceStatusEnum.OFFLINE.label,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 获取状态操作符选项 */
|
||||||
|
const statusOperatorOptions = [
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
|
||||||
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
|
||||||
|
label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const condition = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const propertyType = ref<string>('string'); // 属性类型
|
||||||
|
const propertyConfig = ref<any>(null); // 属性配置
|
||||||
|
const isDeviceCondition = computed(() => {
|
||||||
|
return (
|
||||||
|
condition.value.type ===
|
||||||
|
IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
|
||||||
|
condition.value.type ===
|
||||||
|
IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
|
||||||
|
);
|
||||||
|
}); // 计算属性:判断是否为设备相关条件
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新条件字段
|
||||||
|
* @param field 字段名
|
||||||
|
* @param value 字段值
|
||||||
|
*/
|
||||||
|
const updateConditionField = (field: any, value: any) => {
|
||||||
|
(condition.value as any)[field] = value;
|
||||||
|
emit('update:modelValue', condition.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新整个条件对象
|
||||||
|
* @param newCondition 新的条件对象
|
||||||
|
*/
|
||||||
|
const updateCondition = (newCondition: TriggerCondition) => {
|
||||||
|
condition.value = newCondition;
|
||||||
|
emit('update:modelValue', condition.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理条件类型变化事件
|
||||||
|
* @param type 条件类型
|
||||||
|
*/
|
||||||
|
const handleConditionTypeChange = (type: number) => {
|
||||||
|
// 根据条件类型清理字段
|
||||||
|
const isCurrentTime =
|
||||||
|
type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME;
|
||||||
|
const isDeviceStatus =
|
||||||
|
type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS;
|
||||||
|
|
||||||
|
// 清理标识符字段(时间条件和设备状态条件都不需要)
|
||||||
|
if (isCurrentTime || isDeviceStatus) {
|
||||||
|
condition.value.identifier = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理设备相关字段(仅时间条件需要)
|
||||||
|
if (isCurrentTime) {
|
||||||
|
condition.value.productId = undefined;
|
||||||
|
condition.value.deviceId = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置默认操作符
|
||||||
|
condition.value.operator = isCurrentTime
|
||||||
|
? 'at_time'
|
||||||
|
: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value;
|
||||||
|
|
||||||
|
// 清空参数值
|
||||||
|
condition.value.param = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理产品变化事件 */
|
||||||
|
const handleProductChange = (_: number) => {
|
||||||
|
// 产品变化时清空设备和属性
|
||||||
|
condition.value.deviceId = undefined;
|
||||||
|
condition.value.identifier = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理设备变化事件 */
|
||||||
|
const handleDeviceChange = (_: number) => {
|
||||||
|
// 设备变化时清空属性
|
||||||
|
condition.value.identifier = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理属性变化事件
|
||||||
|
* @param propertyInfo 属性信息对象
|
||||||
|
*/
|
||||||
|
const handlePropertyChange = (propertyInfo: { config: any; type: string }) => {
|
||||||
|
propertyType.value = propertyInfo.type;
|
||||||
|
propertyConfig.value = propertyInfo.config;
|
||||||
|
|
||||||
|
// 重置操作符和值
|
||||||
|
condition.value.operator =
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value;
|
||||||
|
condition.value.param = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理操作符变化事件 */
|
||||||
|
const handleOperatorChange = () => {
|
||||||
|
// 重置值
|
||||||
|
condition.value.param = '';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="gap-16px flex flex-col">
|
||||||
<!-- 条件类型选择 -->
|
<!-- 条件类型选择 -->
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
@@ -29,7 +181,9 @@
|
|||||||
<el-form-item label="产品" required>
|
<el-form-item label="产品" required>
|
||||||
<ProductSelector
|
<ProductSelector
|
||||||
:model-value="condition.productId"
|
:model-value="condition.productId"
|
||||||
@update:model-value="(value) => updateConditionField('productId', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('productId', value)
|
||||||
|
"
|
||||||
@change="handleProductChange"
|
@change="handleProductChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -38,7 +192,9 @@
|
|||||||
<el-form-item label="设备" required>
|
<el-form-item label="设备" required>
|
||||||
<DeviceSelector
|
<DeviceSelector
|
||||||
:model-value="condition.deviceId"
|
:model-value="condition.deviceId"
|
||||||
@update:model-value="(value) => updateConditionField('deviceId', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('deviceId', value)
|
||||||
|
"
|
||||||
:product-id="condition.productId"
|
:product-id="condition.productId"
|
||||||
@change="handleDeviceChange"
|
@change="handleDeviceChange"
|
||||||
/>
|
/>
|
||||||
@@ -48,8 +204,10 @@
|
|||||||
|
|
||||||
<!-- 设备状态条件配置 -->
|
<!-- 设备状态条件配置 -->
|
||||||
<div
|
<div
|
||||||
v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS"
|
v-if="
|
||||||
class="flex flex-col gap-16px"
|
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
|
||||||
|
"
|
||||||
|
class="gap-16px flex flex-col"
|
||||||
>
|
>
|
||||||
<!-- 状态和操作符选择 -->
|
<!-- 状态和操作符选择 -->
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
@@ -58,7 +216,9 @@
|
|||||||
<el-form-item label="操作符" required>
|
<el-form-item label="操作符" required>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="condition.operator"
|
:model-value="condition.operator"
|
||||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('operator', value)
|
||||||
|
"
|
||||||
placeholder="请选择操作符"
|
placeholder="请选择操作符"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
@@ -77,7 +237,9 @@
|
|||||||
<el-form-item label="设备状态" required>
|
<el-form-item label="设备状态" required>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="condition.param"
|
:model-value="condition.param"
|
||||||
@update:model-value="(value) => updateConditionField('param', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('param', value)
|
||||||
|
"
|
||||||
placeholder="请选择设备状态"
|
placeholder="请选择设备状态"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
@@ -95,7 +257,9 @@
|
|||||||
|
|
||||||
<!-- 设备属性条件配置 -->
|
<!-- 设备属性条件配置 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY"
|
v-else-if="
|
||||||
|
condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
|
||||||
|
"
|
||||||
class="space-y-16px"
|
class="space-y-16px"
|
||||||
>
|
>
|
||||||
<!-- 属性配置 -->
|
<!-- 属性配置 -->
|
||||||
@@ -105,7 +269,9 @@
|
|||||||
<el-form-item label="监控项" required>
|
<el-form-item label="监控项" required>
|
||||||
<PropertySelector
|
<PropertySelector
|
||||||
:model-value="condition.identifier"
|
:model-value="condition.identifier"
|
||||||
@update:model-value="(value) => updateConditionField('identifier', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('identifier', value)
|
||||||
|
"
|
||||||
:trigger-type="triggerType"
|
:trigger-type="triggerType"
|
||||||
:product-id="condition.productId"
|
:product-id="condition.productId"
|
||||||
:device-id="condition.deviceId"
|
:device-id="condition.deviceId"
|
||||||
@@ -119,7 +285,9 @@
|
|||||||
<el-form-item label="操作符" required>
|
<el-form-item label="操作符" required>
|
||||||
<OperatorSelector
|
<OperatorSelector
|
||||||
:model-value="condition.operator"
|
:model-value="condition.operator"
|
||||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('operator', value)
|
||||||
|
"
|
||||||
:property-type="propertyType"
|
:property-type="propertyType"
|
||||||
@change="handleOperatorChange"
|
@change="handleOperatorChange"
|
||||||
/>
|
/>
|
||||||
@@ -131,7 +299,9 @@
|
|||||||
<el-form-item label="比较值" required>
|
<el-form-item label="比较值" required>
|
||||||
<ValueInput
|
<ValueInput
|
||||||
:model-value="condition.param"
|
:model-value="condition.param"
|
||||||
@update:model-value="(value) => updateConditionField('param', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('param', value)
|
||||||
|
"
|
||||||
:property-type="propertyType"
|
:property-type="propertyType"
|
||||||
:operator="condition.operator"
|
:operator="condition.operator"
|
||||||
:property-config="propertyConfig"
|
:property-config="propertyConfig"
|
||||||
@@ -143,157 +313,15 @@
|
|||||||
|
|
||||||
<!-- 当前时间条件配置 -->
|
<!-- 当前时间条件配置 -->
|
||||||
<CurrentTimeConditionConfig
|
<CurrentTimeConditionConfig
|
||||||
v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME"
|
v-else-if="
|
||||||
|
condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME
|
||||||
|
"
|
||||||
:model-value="condition"
|
:model-value="condition"
|
||||||
@update:model-value="updateCondition"
|
@update:model-value="updateCondition"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
|
|
||||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
|
||||||
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
|
||||||
import PropertySelector from '../selectors/PropertySelector.vue'
|
|
||||||
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
|
||||||
import ValueInput from '../inputs/ValueInput.vue'
|
|
||||||
import type { TriggerCondition } from '#/api/iot/rule/scene'
|
|
||||||
import {
|
|
||||||
IotRuleSceneTriggerConditionTypeEnum,
|
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
|
||||||
getConditionTypeOptions,
|
|
||||||
IoTDeviceStatusEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 单个条件配置组件 */
|
|
||||||
defineOptions({ name: 'ConditionConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: TriggerCondition
|
|
||||||
triggerType: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: TriggerCondition): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
/** 获取设备状态选项 */
|
|
||||||
const deviceStatusOptions = [
|
|
||||||
{
|
|
||||||
value: IoTDeviceStatusEnum.ONLINE.value,
|
|
||||||
label: IoTDeviceStatusEnum.ONLINE.label
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IoTDeviceStatusEnum.OFFLINE.value,
|
|
||||||
label: IoTDeviceStatusEnum.OFFLINE.label
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
/** 获取状态操作符选项 */
|
|
||||||
const statusOperatorOptions = [
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
|
|
||||||
label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
|
|
||||||
label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const condition = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
const propertyType = ref<string>('string') // 属性类型
|
|
||||||
const propertyConfig = ref<any>(null) // 属性配置
|
|
||||||
const isDeviceCondition = computed(() => {
|
|
||||||
return (
|
|
||||||
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
|
|
||||||
condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
|
|
||||||
)
|
|
||||||
}) // 计算属性:判断是否为设备相关条件
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新条件字段
|
|
||||||
* @param field 字段名
|
|
||||||
* @param value 字段值
|
|
||||||
*/
|
|
||||||
const updateConditionField = (field: any, value: any) => {
|
|
||||||
;(condition.value as any)[field] = value
|
|
||||||
emit('update:modelValue', condition.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新整个条件对象
|
|
||||||
* @param newCondition 新的条件对象
|
|
||||||
*/
|
|
||||||
const updateCondition = (newCondition: TriggerCondition) => {
|
|
||||||
condition.value = newCondition
|
|
||||||
emit('update:modelValue', condition.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理条件类型变化事件
|
|
||||||
* @param type 条件类型
|
|
||||||
*/
|
|
||||||
const handleConditionTypeChange = (type: number) => {
|
|
||||||
// 根据条件类型清理字段
|
|
||||||
const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME
|
|
||||||
const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
|
|
||||||
|
|
||||||
// 清理标识符字段(时间条件和设备状态条件都不需要)
|
|
||||||
if (isCurrentTime || isDeviceStatus) {
|
|
||||||
condition.value.identifier = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理设备相关字段(仅时间条件需要)
|
|
||||||
if (isCurrentTime) {
|
|
||||||
condition.value.productId = undefined
|
|
||||||
condition.value.deviceId = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认操作符
|
|
||||||
condition.value.operator = isCurrentTime
|
|
||||||
? 'at_time'
|
|
||||||
: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
|
||||||
|
|
||||||
// 清空参数值
|
|
||||||
condition.value.param = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理产品变化事件 */
|
|
||||||
const handleProductChange = (_: number) => {
|
|
||||||
// 产品变化时清空设备和属性
|
|
||||||
condition.value.deviceId = undefined
|
|
||||||
condition.value.identifier = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理设备变化事件 */
|
|
||||||
const handleDeviceChange = (_: number) => {
|
|
||||||
// 设备变化时清空属性
|
|
||||||
condition.value.identifier = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理属性变化事件
|
|
||||||
* @param propertyInfo 属性信息对象
|
|
||||||
*/
|
|
||||||
const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
|
|
||||||
propertyType.value = propertyInfo.type
|
|
||||||
propertyConfig.value = propertyInfo.config
|
|
||||||
|
|
||||||
// 重置操作符和值
|
|
||||||
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
|
||||||
condition.value.param = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理操作符变化事件 */
|
|
||||||
const handleOperatorChange = () => {
|
|
||||||
// 重置值
|
|
||||||
condition.value.param = ''
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|||||||
@@ -1,13 +1,172 @@
|
|||||||
<!-- 当前时间条件配置组件 -->
|
<!-- 当前时间条件配置组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** 当前时间条件配置组件 */
|
||||||
|
defineOptions({ name: 'CurrentTimeConditionConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: TriggerCondition;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: TriggerCondition): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const condition = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
// 时间操作符选项
|
||||||
|
const timeOperatorOptions = [
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
|
||||||
|
label: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.name,
|
||||||
|
icon: 'ep:arrow-left',
|
||||||
|
iconClass: 'text-blue-500',
|
||||||
|
tag: 'primary',
|
||||||
|
category: '时间点',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
|
||||||
|
label: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.name,
|
||||||
|
icon: 'ep:arrow-right',
|
||||||
|
iconClass: 'text-green-500',
|
||||||
|
tag: 'success',
|
||||||
|
category: '时间点',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
|
||||||
|
label: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.name,
|
||||||
|
icon: 'ep:sort',
|
||||||
|
iconClass: 'text-orange-500',
|
||||||
|
tag: 'warning',
|
||||||
|
category: '时间段',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value,
|
||||||
|
label: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.name,
|
||||||
|
icon: 'ep:position',
|
||||||
|
iconClass: 'text-purple-500',
|
||||||
|
tag: 'info',
|
||||||
|
category: '时间点',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: IotRuleSceneTriggerTimeOperatorEnum.TODAY.value,
|
||||||
|
label: IotRuleSceneTriggerTimeOperatorEnum.TODAY.name,
|
||||||
|
icon: 'ep:calendar',
|
||||||
|
iconClass: 'text-red-500',
|
||||||
|
tag: 'danger',
|
||||||
|
category: '日期',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 计算属性:是否需要时间输入
|
||||||
|
const needsTimeInput = computed(() => {
|
||||||
|
const timeOnlyOperators = [
|
||||||
|
IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
|
||||||
|
IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
|
||||||
|
IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
|
||||||
|
IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value,
|
||||||
|
];
|
||||||
|
return timeOnlyOperators.includes(condition.value.operator as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:是否需要日期输入
|
||||||
|
const needsDateInput = computed(() => {
|
||||||
|
return false; // 暂时不支持日期输入,只支持时间
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:是否需要第二个时间输入
|
||||||
|
const needsSecondTimeInput = computed(() => {
|
||||||
|
return (
|
||||||
|
condition.value.operator ===
|
||||||
|
IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:从 param 中解析时间值
|
||||||
|
const timeValue = computed(() => {
|
||||||
|
if (!condition.value.param) return '';
|
||||||
|
const params = condition.value.param.split(',');
|
||||||
|
return params[0] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:从 param 中解析第二个时间值
|
||||||
|
const timeValue2 = computed(() => {
|
||||||
|
if (!condition.value.param) return '';
|
||||||
|
const params = condition.value.param.split(',');
|
||||||
|
return params[1] || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新条件字段
|
||||||
|
* @param field 字段名
|
||||||
|
* @param value 字段值
|
||||||
|
*/
|
||||||
|
const updateConditionField = (field: any, value: any) => {
|
||||||
|
condition.value[field] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理第一个时间值变化
|
||||||
|
* @param value 时间值
|
||||||
|
*/
|
||||||
|
const handleTimeValueChange = (value: string) => {
|
||||||
|
const currentParams = condition.value.param
|
||||||
|
? condition.value.param.split(',')
|
||||||
|
: [];
|
||||||
|
currentParams[0] = value || '';
|
||||||
|
|
||||||
|
// 如果是范围条件,保留第二个值;否则只保留第一个值
|
||||||
|
condition.value.param = needsSecondTimeInput.value
|
||||||
|
? currentParams.slice(0, 2).join(',')
|
||||||
|
: currentParams[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理第二个时间值变化
|
||||||
|
* @param value 时间值
|
||||||
|
*/
|
||||||
|
const handleTimeValue2Change = (value: string) => {
|
||||||
|
const currentParams = condition.value.param
|
||||||
|
? condition.value.param.split(',')
|
||||||
|
: [''];
|
||||||
|
currentParams[1] = value || '';
|
||||||
|
condition.value.param = currentParams.slice(0, 2).join(',');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听操作符变化,清理不相关的时间值 */
|
||||||
|
watch(
|
||||||
|
() => condition.value.operator,
|
||||||
|
(newOperator) => {
|
||||||
|
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
||||||
|
// 今日条件不需要时间参数
|
||||||
|
condition.value.param = '';
|
||||||
|
} else if (!needsSecondTimeInput.value) {
|
||||||
|
// 非范围条件只保留第一个时间值
|
||||||
|
const currentParams = condition.value.param
|
||||||
|
? condition.value.param.split(',')
|
||||||
|
: [];
|
||||||
|
condition.value.param = currentParams[0] || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="gap-16px flex flex-col">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<!-- 时间操作符选择 -->
|
<!-- 时间操作符选择 -->
|
||||||
<el-col :span="8">
|
<el-col :span="8">
|
||||||
<el-form-item label="时间条件" required>
|
<el-form-item label="时间条件" required>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="condition.operator"
|
:model-value="condition.operator"
|
||||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('operator', value)
|
||||||
|
"
|
||||||
placeholder="请选择时间条件"
|
placeholder="请选择时间条件"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
@@ -17,12 +176,14 @@
|
|||||||
:label="option.label"
|
:label="option.label"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full">
|
<div class="flex w-full items-center justify-between">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<Icon :icon="option.icon" :class="option.iconClass" />
|
<Icon :icon="option.icon" :class="option.iconClass" />
|
||||||
<span>{{ option.label }}</span>
|
<span>{{ option.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag :type="option.tag as any" size="small">{{ option.category }}</el-tag>
|
<el-tag :type="option.tag as any" size="small">
|
||||||
|
{{ option.category }}
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -51,7 +212,7 @@
|
|||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<div v-else class="text-[var(--el-text-color-placeholder)] text-14px">
|
<div v-else class="text-14px text-[var(--el-text-color-placeholder)]">
|
||||||
无需设置时间值
|
无需设置时间值
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -84,151 +245,3 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants'
|
|
||||||
import type { TriggerCondition } from '#/api/iot/rule/scene'
|
|
||||||
|
|
||||||
/** 当前时间条件配置组件 */
|
|
||||||
defineOptions({ name: 'CurrentTimeConditionConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: TriggerCondition
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: TriggerCondition): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const condition = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
// 时间操作符选项
|
|
||||||
const timeOperatorOptions = [
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
|
|
||||||
label: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.name,
|
|
||||||
icon: 'ep:arrow-left',
|
|
||||||
iconClass: 'text-blue-500',
|
|
||||||
tag: 'primary',
|
|
||||||
category: '时间点'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
|
|
||||||
label: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.name,
|
|
||||||
icon: 'ep:arrow-right',
|
|
||||||
iconClass: 'text-green-500',
|
|
||||||
tag: 'success',
|
|
||||||
category: '时间点'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
|
|
||||||
label: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.name,
|
|
||||||
icon: 'ep:sort',
|
|
||||||
iconClass: 'text-orange-500',
|
|
||||||
tag: 'warning',
|
|
||||||
category: '时间段'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value,
|
|
||||||
label: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.name,
|
|
||||||
icon: 'ep:position',
|
|
||||||
iconClass: 'text-purple-500',
|
|
||||||
tag: 'info',
|
|
||||||
category: '时间点'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTimeOperatorEnum.TODAY.value,
|
|
||||||
label: IotRuleSceneTriggerTimeOperatorEnum.TODAY.name,
|
|
||||||
icon: 'ep:calendar',
|
|
||||||
iconClass: 'text-red-500',
|
|
||||||
tag: 'danger',
|
|
||||||
category: '日期'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 计算属性:是否需要时间输入
|
|
||||||
const needsTimeInput = computed(() => {
|
|
||||||
const timeOnlyOperators = [
|
|
||||||
IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
|
|
||||||
IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
|
|
||||||
IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
|
|
||||||
IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value
|
|
||||||
]
|
|
||||||
return timeOnlyOperators.includes(condition.value.operator as any)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:是否需要日期输入
|
|
||||||
const needsDateInput = computed(() => {
|
|
||||||
return false // 暂时不支持日期输入,只支持时间
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:是否需要第二个时间输入
|
|
||||||
const needsSecondTimeInput = computed(() => {
|
|
||||||
return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:从 param 中解析时间值
|
|
||||||
const timeValue = computed(() => {
|
|
||||||
if (!condition.value.param) return ''
|
|
||||||
const params = condition.value.param.split(',')
|
|
||||||
return params[0] || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:从 param 中解析第二个时间值
|
|
||||||
const timeValue2 = computed(() => {
|
|
||||||
if (!condition.value.param) return ''
|
|
||||||
const params = condition.value.param.split(',')
|
|
||||||
return params[1] || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新条件字段
|
|
||||||
* @param field 字段名
|
|
||||||
* @param value 字段值
|
|
||||||
*/
|
|
||||||
const updateConditionField = (field: any, value: any) => {
|
|
||||||
condition.value[field] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理第一个时间值变化
|
|
||||||
* @param value 时间值
|
|
||||||
*/
|
|
||||||
const handleTimeValueChange = (value: string) => {
|
|
||||||
const currentParams = condition.value.param ? condition.value.param.split(',') : []
|
|
||||||
currentParams[0] = value || ''
|
|
||||||
|
|
||||||
// 如果是范围条件,保留第二个值;否则只保留第一个值
|
|
||||||
if (needsSecondTimeInput.value) {
|
|
||||||
condition.value.param = currentParams.slice(0, 2).join(',')
|
|
||||||
} else {
|
|
||||||
condition.value.param = currentParams[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理第二个时间值变化
|
|
||||||
* @param value 时间值
|
|
||||||
*/
|
|
||||||
const handleTimeValue2Change = (value: string) => {
|
|
||||||
const currentParams = condition.value.param ? condition.value.param.split(',') : ['']
|
|
||||||
currentParams[1] = value || ''
|
|
||||||
condition.value.param = currentParams.slice(0, 2).join(',')
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听操作符变化,清理不相关的时间值 */
|
|
||||||
watch(
|
|
||||||
() => condition.value.operator,
|
|
||||||
(newOperator) => {
|
|
||||||
if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
|
|
||||||
// 今日条件不需要时间参数
|
|
||||||
condition.value.param = ''
|
|
||||||
} else if (!needsSecondTimeInput.value) {
|
|
||||||
// 非范围条件只保留第一个时间值
|
|
||||||
const currentParams = condition.value.param ? condition.value.param.split(',') : []
|
|
||||||
condition.value.param = currentParams[0] || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,11 +1,341 @@
|
|||||||
<!-- 设备控制配置组件 -->
|
<!-- 设备控制配置组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Action } from '#/api/iot/rule/scene';
|
||||||
|
import type {
|
||||||
|
ThingModelProperty,
|
||||||
|
ThingModelService,
|
||||||
|
} from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
import {
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IotRuleSceneActionTypeEnum,
|
||||||
|
IoTThingModelAccessModeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import JsonParamsInput from '../inputs/JsonParamsInput.vue';
|
||||||
|
import DeviceSelector from '../selectors/DeviceSelector.vue';
|
||||||
|
import ProductSelector from '../selectors/ProductSelector.vue';
|
||||||
|
|
||||||
|
/** 设备控制配置组件 */
|
||||||
|
defineOptions({ name: 'DeviceControlConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Action;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: Action): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const action = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const thingModelProperties = ref<ThingModelProperty[]>([]); // 物模型属性列表
|
||||||
|
const loadingThingModel = ref(false); // 物模型加载状态
|
||||||
|
const selectedService = ref<null | ThingModelService>(null); // 选中的服务对象
|
||||||
|
const serviceList = ref<ThingModelService[]>([]); // 服务列表
|
||||||
|
const loadingServices = ref(false); // 服务加载状态
|
||||||
|
|
||||||
|
// 参数值的计算属性,用于双向绑定
|
||||||
|
const paramsValue = computed({
|
||||||
|
get: () => {
|
||||||
|
// 如果 params 是对象,转换为 JSON 字符串(兼容旧数据)
|
||||||
|
if (action.value.params && typeof action.value.params === 'object') {
|
||||||
|
return JSON.stringify(action.value.params, null, 2);
|
||||||
|
}
|
||||||
|
// 如果 params 已经是字符串,直接返回
|
||||||
|
return action.value.params || '';
|
||||||
|
},
|
||||||
|
set: (value: string) => {
|
||||||
|
// 直接保存为 JSON 字符串,不进行解析转换
|
||||||
|
action.value.params = value.trim() || '';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:是否为属性设置类型
|
||||||
|
const isPropertySetAction = computed(() => {
|
||||||
|
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:是否为服务调用类型
|
||||||
|
const isServiceInvokeAction = computed(() => {
|
||||||
|
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理产品变化事件
|
||||||
|
* @param productId 产品 ID
|
||||||
|
*/
|
||||||
|
const handleProductChange = (productId?: number) => {
|
||||||
|
// 当产品变化时,清空设备选择和参数配置
|
||||||
|
if (action.value.productId !== productId) {
|
||||||
|
action.value.deviceId = undefined;
|
||||||
|
action.value.identifier = undefined; // 清空服务标识符
|
||||||
|
action.value.params = ''; // 清空参数,保存为空字符串
|
||||||
|
selectedService.value = null; // 清空选中的服务
|
||||||
|
serviceList.value = []; // 清空服务列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载新产品的物模型属性或服务列表
|
||||||
|
if (productId) {
|
||||||
|
if (isPropertySetAction.value) {
|
||||||
|
loadThingModelProperties(productId);
|
||||||
|
} else if (isServiceInvokeAction.value) {
|
||||||
|
loadServiceList(productId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理设备变化事件
|
||||||
|
* @param deviceId 设备 ID
|
||||||
|
*/
|
||||||
|
const handleDeviceChange = (deviceId?: number) => {
|
||||||
|
// 当设备变化时,清空参数配置
|
||||||
|
if (action.value.deviceId !== deviceId) {
|
||||||
|
action.value.params = ''; // 清空参数,保存为空字符串
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理服务变化事件
|
||||||
|
* @param serviceIdentifier 服务标识符
|
||||||
|
*/
|
||||||
|
const handleServiceChange = (serviceIdentifier?: string) => {
|
||||||
|
// 根据服务标识符找到对应的服务对象
|
||||||
|
const service =
|
||||||
|
serviceList.value.find((s) => s.identifier === serviceIdentifier) || null;
|
||||||
|
selectedService.value = service;
|
||||||
|
|
||||||
|
// 当服务变化时,清空参数配置
|
||||||
|
action.value.params = '';
|
||||||
|
|
||||||
|
// 如果选择了服务且有输入参数,生成默认参数结构
|
||||||
|
if (service && service.inputParams && service.inputParams.length > 0) {
|
||||||
|
const defaultParams = {};
|
||||||
|
service.inputParams.forEach((param) => {
|
||||||
|
defaultParams[param.identifier] = getDefaultValueForParam(param);
|
||||||
|
});
|
||||||
|
// 将默认参数转换为 JSON 字符串保存
|
||||||
|
action.value.params = JSON.stringify(defaultParams, null, 2);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取物模型TSL数据
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @returns 物模型TSL数据
|
||||||
|
*/
|
||||||
|
const getThingModelTSL = async (productId: number) => {
|
||||||
|
if (!productId) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await ThingModelApi.getThingModelTSLByProductId(productId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取物模型TSL数据失败:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载物模型属性(可写属性)
|
||||||
|
* @param productId 产品ID
|
||||||
|
*/
|
||||||
|
const loadThingModelProperties = async (productId: number) => {
|
||||||
|
if (!productId) {
|
||||||
|
thingModelProperties.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadingThingModel.value = true;
|
||||||
|
const tslData = await getThingModelTSL(productId);
|
||||||
|
|
||||||
|
if (!tslData?.properties) {
|
||||||
|
thingModelProperties.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤出可写的属性(accessMode 包含 'w')
|
||||||
|
thingModelProperties.value = tslData.properties.filter(
|
||||||
|
(property: ThingModelProperty) =>
|
||||||
|
property.accessMode &&
|
||||||
|
(property.accessMode === IoTThingModelAccessModeEnum.READ_WRITE.value ||
|
||||||
|
property.accessMode === IoTThingModelAccessModeEnum.WRITE_ONLY.value),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载物模型属性失败:', error);
|
||||||
|
thingModelProperties.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingThingModel.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载服务列表
|
||||||
|
* @param productId 产品ID
|
||||||
|
*/
|
||||||
|
const loadServiceList = async (productId: number) => {
|
||||||
|
if (!productId) {
|
||||||
|
serviceList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadingServices.value = true;
|
||||||
|
const tslData = await getThingModelTSL(productId);
|
||||||
|
|
||||||
|
if (!tslData?.services) {
|
||||||
|
serviceList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceList.value = tslData.services;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载服务列表失败:', error);
|
||||||
|
serviceList.value = [];
|
||||||
|
} finally {
|
||||||
|
loadingServices.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从TSL加载服务信息(用于编辑模式回显)
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @param serviceIdentifier 服务标识符
|
||||||
|
*/
|
||||||
|
const loadServiceFromTSL = async (
|
||||||
|
productId: number,
|
||||||
|
serviceIdentifier: string,
|
||||||
|
) => {
|
||||||
|
// 先加载服务列表
|
||||||
|
await loadServiceList(productId);
|
||||||
|
|
||||||
|
// 然后设置选中的服务
|
||||||
|
const service = serviceList.value.find(
|
||||||
|
(s: any) => s.identifier === serviceIdentifier,
|
||||||
|
);
|
||||||
|
if (service) {
|
||||||
|
selectedService.value = service;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据参数类型获取默认值
|
||||||
|
* @param param 参数对象
|
||||||
|
* @returns 默认值
|
||||||
|
*/
|
||||||
|
const getDefaultValueForParam = (param: any) => {
|
||||||
|
switch (param.dataType) {
|
||||||
|
case IoTDataSpecsDataTypeEnum.BOOL: {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
case IoTDataSpecsDataTypeEnum.DOUBLE:
|
||||||
|
case IoTDataSpecsDataTypeEnum.FLOAT: {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case IoTDataSpecsDataTypeEnum.ENUM: {
|
||||||
|
// 如果有枚举值,使用第一个
|
||||||
|
if (
|
||||||
|
param.dataSpecs?.dataSpecsList &&
|
||||||
|
param.dataSpecs.dataSpecsList.length > 0
|
||||||
|
) {
|
||||||
|
return param.dataSpecs.dataSpecsList[0].value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
case IoTDataSpecsDataTypeEnum.INT: {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
case IoTDataSpecsDataTypeEnum.TEXT: {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInitialized = ref(false); // 防止重复初始化的标志
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化组件数据
|
||||||
|
*/
|
||||||
|
const initializeComponent = async () => {
|
||||||
|
if (isInitialized.value) return;
|
||||||
|
|
||||||
|
const currentAction = action.value;
|
||||||
|
if (!currentAction) return;
|
||||||
|
|
||||||
|
// 如果已经选择了产品且是属性设置类型,加载物模型
|
||||||
|
if (currentAction.productId && isPropertySetAction.value) {
|
||||||
|
await loadThingModelProperties(currentAction.productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是服务调用类型且已有标识符,初始化服务选择
|
||||||
|
if (
|
||||||
|
currentAction.productId &&
|
||||||
|
isServiceInvokeAction.value &&
|
||||||
|
currentAction.identifier
|
||||||
|
) {
|
||||||
|
// 加载物模型TSL以获取服务信息
|
||||||
|
await loadServiceFromTSL(currentAction.productId, currentAction.identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 组件初始化 */
|
||||||
|
onMounted(() => {
|
||||||
|
initializeComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 监听关键字段的变化,避免深度监听导致的性能问题 */
|
||||||
|
watch(
|
||||||
|
() => [action.value.productId, action.value.type, action.value.identifier],
|
||||||
|
async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
|
||||||
|
// 避免初始化时的重复调用
|
||||||
|
if (!isInitialized.value) return;
|
||||||
|
|
||||||
|
// 产品变化时重新加载数据
|
||||||
|
if (newProductId !== oldProductId) {
|
||||||
|
if (newProductId && isPropertySetAction.value) {
|
||||||
|
await loadThingModelProperties(newProductId as number);
|
||||||
|
} else if (newProductId && isServiceInvokeAction.value) {
|
||||||
|
await loadServiceList(newProductId as number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务标识符变化时更新选中的服务
|
||||||
|
if (
|
||||||
|
newIdentifier !== oldIdentifier &&
|
||||||
|
newProductId &&
|
||||||
|
isServiceInvokeAction.value &&
|
||||||
|
newIdentifier
|
||||||
|
) {
|
||||||
|
const service = serviceList.value.find(
|
||||||
|
(s: any) => s.identifier === newIdentifier,
|
||||||
|
);
|
||||||
|
if (service) {
|
||||||
|
selectedService.value = service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="gap-16px flex flex-col">
|
||||||
<!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
|
<!-- 产品和设备选择 - 与触发器保持一致的分离式选择器 -->
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="产品" required>
|
<el-form-item label="产品" required>
|
||||||
<ProductSelector v-model="action.productId" @change="handleProductChange" />
|
<ProductSelector
|
||||||
|
v-model="action.productId"
|
||||||
|
@change="handleProductChange"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -39,7 +369,10 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span>{{ service.name }}</span>
|
<span>{{ service.name }}</span>
|
||||||
<el-tag :type="service.callType === 'sync' ? 'primary' : 'success'" size="small">
|
<el-tag
|
||||||
|
:type="service.callType === 'sync' ? 'primary' : 'success'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{{ service.callType === 'sync' ? '同步' : '异步' }}
|
{{ service.callType === 'sync' ? '同步' : '异步' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,303 +407,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
|
||||||
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
|
||||||
import JsonParamsInput from '../inputs/JsonParamsInput.vue'
|
|
||||||
import type { Action } from '#/api/iot/rule/scene'
|
|
||||||
import type { ThingModelProperty, ThingModelService } from '#/api/iot/thingmodel'
|
|
||||||
import {
|
|
||||||
IotRuleSceneActionTypeEnum,
|
|
||||||
IoTThingModelAccessModeEnum,
|
|
||||||
IoTDataSpecsDataTypeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
import { ThingModelApi } from '#/api/iot/thingmodel'
|
|
||||||
|
|
||||||
/** 设备控制配置组件 */
|
|
||||||
defineOptions({ name: 'DeviceControlConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: Action
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: Action): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const action = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
const thingModelProperties = ref<ThingModelProperty[]>([]) // 物模型属性列表
|
|
||||||
const loadingThingModel = ref(false) // 物模型加载状态
|
|
||||||
const selectedService = ref<ThingModelService | null>(null) // 选中的服务对象
|
|
||||||
const serviceList = ref<ThingModelService[]>([]) // 服务列表
|
|
||||||
const loadingServices = ref(false) // 服务加载状态
|
|
||||||
|
|
||||||
// 参数值的计算属性,用于双向绑定
|
|
||||||
const paramsValue = computed({
|
|
||||||
get: () => {
|
|
||||||
// 如果 params 是对象,转换为 JSON 字符串(兼容旧数据)
|
|
||||||
if (action.value.params && typeof action.value.params === 'object') {
|
|
||||||
return JSON.stringify(action.value.params, null, 2)
|
|
||||||
}
|
|
||||||
// 如果 params 已经是字符串,直接返回
|
|
||||||
return action.value.params || ''
|
|
||||||
},
|
|
||||||
set: (value: string) => {
|
|
||||||
// 直接保存为 JSON 字符串,不进行解析转换
|
|
||||||
action.value.params = value.trim() || ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:是否为属性设置类型
|
|
||||||
const isPropertySetAction = computed(() => {
|
|
||||||
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:是否为服务调用类型
|
|
||||||
const isServiceInvokeAction = computed(() => {
|
|
||||||
return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理产品变化事件
|
|
||||||
* @param productId 产品 ID
|
|
||||||
*/
|
|
||||||
const handleProductChange = (productId?: number) => {
|
|
||||||
// 当产品变化时,清空设备选择和参数配置
|
|
||||||
if (action.value.productId !== productId) {
|
|
||||||
action.value.deviceId = undefined
|
|
||||||
action.value.identifier = undefined // 清空服务标识符
|
|
||||||
action.value.params = '' // 清空参数,保存为空字符串
|
|
||||||
selectedService.value = null // 清空选中的服务
|
|
||||||
serviceList.value = [] // 清空服务列表
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载新产品的物模型属性或服务列表
|
|
||||||
if (productId) {
|
|
||||||
if (isPropertySetAction.value) {
|
|
||||||
loadThingModelProperties(productId)
|
|
||||||
} else if (isServiceInvokeAction.value) {
|
|
||||||
loadServiceList(productId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理设备变化事件
|
|
||||||
* @param deviceId 设备 ID
|
|
||||||
*/
|
|
||||||
const handleDeviceChange = (deviceId?: number) => {
|
|
||||||
// 当设备变化时,清空参数配置
|
|
||||||
if (action.value.deviceId !== deviceId) {
|
|
||||||
action.value.params = '' // 清空参数,保存为空字符串
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理服务变化事件
|
|
||||||
* @param serviceIdentifier 服务标识符
|
|
||||||
*/
|
|
||||||
const handleServiceChange = (serviceIdentifier?: string) => {
|
|
||||||
// 根据服务标识符找到对应的服务对象
|
|
||||||
const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null
|
|
||||||
selectedService.value = service
|
|
||||||
|
|
||||||
// 当服务变化时,清空参数配置
|
|
||||||
action.value.params = ''
|
|
||||||
|
|
||||||
// 如果选择了服务且有输入参数,生成默认参数结构
|
|
||||||
if (service && service.inputParams && service.inputParams.length > 0) {
|
|
||||||
const defaultParams = {}
|
|
||||||
service.inputParams.forEach((param) => {
|
|
||||||
defaultParams[param.identifier] = getDefaultValueForParam(param)
|
|
||||||
})
|
|
||||||
// 将默认参数转换为 JSON 字符串保存
|
|
||||||
action.value.params = JSON.stringify(defaultParams, null, 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取物模型TSL数据
|
|
||||||
* @param productId 产品ID
|
|
||||||
* @returns 物模型TSL数据
|
|
||||||
*/
|
|
||||||
const getThingModelTSL = async (productId: number) => {
|
|
||||||
if (!productId) return null
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await ThingModelApi.getThingModelTSLByProductId(productId)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取物模型TSL数据失败:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载物模型属性(可写属性)
|
|
||||||
* @param productId 产品ID
|
|
||||||
*/
|
|
||||||
const loadThingModelProperties = async (productId: number) => {
|
|
||||||
if (!productId) {
|
|
||||||
thingModelProperties.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
loadingThingModel.value = true
|
|
||||||
const tslData = await getThingModelTSL(productId)
|
|
||||||
|
|
||||||
if (!tslData?.properties) {
|
|
||||||
thingModelProperties.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 过滤出可写的属性(accessMode 包含 'w')
|
|
||||||
thingModelProperties.value = tslData.properties.filter(
|
|
||||||
(property: ThingModelProperty) =>
|
|
||||||
property.accessMode &&
|
|
||||||
(property.accessMode === IoTThingModelAccessModeEnum.READ_WRITE.value ||
|
|
||||||
property.accessMode === IoTThingModelAccessModeEnum.WRITE_ONLY.value)
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载物模型属性失败:', error)
|
|
||||||
thingModelProperties.value = []
|
|
||||||
} finally {
|
|
||||||
loadingThingModel.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载服务列表
|
|
||||||
* @param productId 产品ID
|
|
||||||
*/
|
|
||||||
const loadServiceList = async (productId: number) => {
|
|
||||||
if (!productId) {
|
|
||||||
serviceList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
loadingServices.value = true
|
|
||||||
const tslData = await getThingModelTSL(productId)
|
|
||||||
|
|
||||||
if (!tslData?.services) {
|
|
||||||
serviceList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceList.value = tslData.services
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载服务列表失败:', error)
|
|
||||||
serviceList.value = []
|
|
||||||
} finally {
|
|
||||||
loadingServices.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从TSL加载服务信息(用于编辑模式回显)
|
|
||||||
* @param productId 产品ID
|
|
||||||
* @param serviceIdentifier 服务标识符
|
|
||||||
*/
|
|
||||||
const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
|
|
||||||
// 先加载服务列表
|
|
||||||
await loadServiceList(productId)
|
|
||||||
|
|
||||||
// 然后设置选中的服务
|
|
||||||
const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier)
|
|
||||||
if (service) {
|
|
||||||
selectedService.value = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据参数类型获取默认值
|
|
||||||
* @param param 参数对象
|
|
||||||
* @returns 默认值
|
|
||||||
*/
|
|
||||||
const getDefaultValueForParam = (param: any) => {
|
|
||||||
switch (param.dataType) {
|
|
||||||
case IoTDataSpecsDataTypeEnum.INT:
|
|
||||||
return 0
|
|
||||||
case IoTDataSpecsDataTypeEnum.FLOAT:
|
|
||||||
case IoTDataSpecsDataTypeEnum.DOUBLE:
|
|
||||||
return 0.0
|
|
||||||
case IoTDataSpecsDataTypeEnum.BOOL:
|
|
||||||
return false
|
|
||||||
case IoTDataSpecsDataTypeEnum.TEXT:
|
|
||||||
return ''
|
|
||||||
case IoTDataSpecsDataTypeEnum.ENUM:
|
|
||||||
// 如果有枚举值,使用第一个
|
|
||||||
if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
|
|
||||||
return param.dataSpecs.dataSpecsList[0].value
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
default:
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isInitialized = ref(false) // 防止重复初始化的标志
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化组件数据
|
|
||||||
*/
|
|
||||||
const initializeComponent = async () => {
|
|
||||||
if (isInitialized.value) return
|
|
||||||
|
|
||||||
const currentAction = action.value
|
|
||||||
if (!currentAction) return
|
|
||||||
|
|
||||||
// 如果已经选择了产品且是属性设置类型,加载物模型
|
|
||||||
if (currentAction.productId && isPropertySetAction.value) {
|
|
||||||
await loadThingModelProperties(currentAction.productId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是服务调用类型且已有标识符,初始化服务选择
|
|
||||||
if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) {
|
|
||||||
// 加载物模型TSL以获取服务信息
|
|
||||||
await loadServiceFromTSL(currentAction.productId, currentAction.identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
isInitialized.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 组件初始化 */
|
|
||||||
onMounted(() => {
|
|
||||||
initializeComponent()
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 监听关键字段的变化,避免深度监听导致的性能问题 */
|
|
||||||
watch(
|
|
||||||
() => [action.value.productId, action.value.type, action.value.identifier],
|
|
||||||
async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
|
|
||||||
// 避免初始化时的重复调用
|
|
||||||
if (!isInitialized.value) return
|
|
||||||
|
|
||||||
// 产品变化时重新加载数据
|
|
||||||
if (newProductId !== oldProductId) {
|
|
||||||
if (newProductId && isPropertySetAction.value) {
|
|
||||||
await loadThingModelProperties(newProductId as number)
|
|
||||||
} else if (newProductId && isServiceInvokeAction.value) {
|
|
||||||
await loadServiceList(newProductId as number)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务标识符变化时更新选中的服务
|
|
||||||
if (
|
|
||||||
newIdentifier !== oldIdentifier &&
|
|
||||||
newProductId &&
|
|
||||||
isServiceInvokeAction.value &&
|
|
||||||
newIdentifier
|
|
||||||
) {
|
|
||||||
const service = serviceList.value.find((s: any) => s.identifier === newIdentifier)
|
|
||||||
if (service) {
|
|
||||||
selectedService.value = service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,20 +1,110 @@
|
|||||||
<!-- 设备触发配置组件 -->
|
<!-- 设备触发配置组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Trigger } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import MainConditionInnerConfig from './MainConditionInnerConfig.vue';
|
||||||
|
import SubConditionGroupConfig from './SubConditionGroupConfig.vue';
|
||||||
|
|
||||||
|
/** 设备触发配置组件 */
|
||||||
|
defineOptions({ name: 'DeviceTriggerConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
index: number;
|
||||||
|
modelValue: Trigger;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: Trigger): void;
|
||||||
|
(e: 'trigger-type-change', type: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const trigger = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const maxSubGroups = 3; // 最多 3 个子条件组
|
||||||
|
const maxConditionsPerGroup = 3; // 每组最多 3 个条件
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新条件
|
||||||
|
* @param condition 条件对象
|
||||||
|
*/
|
||||||
|
const updateCondition = (condition: Trigger) => {
|
||||||
|
trigger.value = condition;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理触发器类型变化事件
|
||||||
|
* @param type 触发器类型
|
||||||
|
*/
|
||||||
|
const handleTriggerTypeChange = (type: number) => {
|
||||||
|
trigger.value.type = type;
|
||||||
|
emit('trigger-type-change', type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 添加子条件组 */
|
||||||
|
const addSubGroup = async () => {
|
||||||
|
if (!trigger.value.conditionGroups) {
|
||||||
|
trigger.value.conditionGroups = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否达到最大子组数量限制
|
||||||
|
if (trigger.value.conditionGroups?.length >= maxSubGroups) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 nextTick 确保响应式更新完成后再添加新的子组
|
||||||
|
await nextTick();
|
||||||
|
if (trigger.value.conditionGroups) {
|
||||||
|
trigger.value.conditionGroups.push([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除子条件组
|
||||||
|
* @param index 子条件组索引
|
||||||
|
*/
|
||||||
|
const removeSubGroup = (index: number) => {
|
||||||
|
if (trigger.value.conditionGroups) {
|
||||||
|
trigger.value.conditionGroups.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新子条件组
|
||||||
|
* @param index 子条件组索引
|
||||||
|
* @param subGroup 子条件组数据
|
||||||
|
*/
|
||||||
|
const updateSubGroup = (index: number, subGroup: any) => {
|
||||||
|
if (trigger.value.conditionGroups) {
|
||||||
|
trigger.value.conditionGroups[index] = subGroup;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 移除整个条件组 */
|
||||||
|
const removeConditionGroup = () => {
|
||||||
|
trigger.value.conditionGroups = undefined;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="gap-16px flex flex-col">
|
||||||
<!-- 主条件配置 - 默认直接展示 -->
|
<!-- 主条件配置 - 默认直接展示 -->
|
||||||
<div class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
<!-- 主条件配置 -->
|
<!-- 主条件配置 -->
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="gap-16px flex flex-col">
|
||||||
<!-- 主条件配置 -->
|
<!-- 主条件配置 -->
|
||||||
<div class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
<!-- 主条件头部 - 与附加条件组保持一致的绿色风格 -->
|
<!-- 主条件头部 - 与附加条件组保持一致的绿色风格 -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
|
class="p-16px rounded-8px flex items-center justify-between border border-green-200 bg-gradient-to-r from-green-50 to-emerald-50"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-12px">
|
<div class="gap-12px flex items-center">
|
||||||
<div class="flex items-center gap-8px text-16px font-600 text-green-700">
|
|
||||||
<div
|
<div
|
||||||
class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
class="gap-8px text-16px font-600 flex items-center text-green-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-24px h-24px text-12px flex items-center justify-center rounded-full bg-green-500 font-bold text-white"
|
||||||
>
|
>
|
||||||
主
|
主
|
||||||
</div>
|
</div>
|
||||||
@@ -38,15 +128,17 @@
|
|||||||
<!-- 条件组配置 -->
|
<!-- 条件组配置 -->
|
||||||
<div class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
<!-- 条件组配置 -->
|
<!-- 条件组配置 -->
|
||||||
<div class="flex flex-col gap-16px">
|
<div class="gap-16px flex flex-col">
|
||||||
<!-- 条件组容器头部 -->
|
<!-- 条件组容器头部 -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
|
class="p-16px rounded-8px flex items-center justify-between border border-green-200 bg-gradient-to-r from-green-50 to-emerald-50"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-12px">
|
<div class="gap-12px flex items-center">
|
||||||
<div class="flex items-center gap-8px text-16px font-600 text-green-700">
|
|
||||||
<div
|
<div
|
||||||
class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
class="gap-8px text-16px font-600 flex items-center text-green-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-24px h-24px text-12px flex items-center justify-center rounded-full bg-green-500 font-bold text-white"
|
||||||
>
|
>
|
||||||
组
|
组
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +149,7 @@
|
|||||||
{{ trigger.conditionGroups?.length || 0 }} 个子条件组
|
{{ trigger.conditionGroups?.length || 0 }} 个子条件组
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -67,7 +159,12 @@
|
|||||||
<Icon icon="ep:plus" />
|
<Icon icon="ep:plus" />
|
||||||
添加子条件组
|
添加子条件组
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="danger" size="small" text @click="removeConditionGroup">
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="removeConditionGroup"
|
||||||
|
>
|
||||||
<Icon icon="ep:delete" />
|
<Icon icon="ep:delete" />
|
||||||
删除条件组
|
删除条件组
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -88,22 +185,28 @@
|
|||||||
>
|
>
|
||||||
<!-- 子条件组容器 -->
|
<!-- 子条件组容器 -->
|
||||||
<div
|
<div
|
||||||
class="border-2 border-orange-200 rounded-8px bg-orange-50 shadow-sm hover:shadow-md transition-shadow"
|
class="rounded-8px border-2 border-orange-200 bg-orange-50 shadow-sm transition-shadow hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-orange-50 to-yellow-50 border-b border-orange-200 rounded-t-6px"
|
class="p-16px rounded-t-6px flex items-center justify-between border-b border-orange-200 bg-gradient-to-r from-orange-50 to-yellow-50"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-12px">
|
<div class="gap-12px flex items-center">
|
||||||
<div class="flex items-center gap-8px text-16px font-600 text-orange-700">
|
|
||||||
<div
|
<div
|
||||||
class="w-24px h-24px bg-orange-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
class="gap-8px text-16px font-600 flex items-center text-orange-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-24px h-24px text-12px flex items-center justify-center rounded-full bg-orange-500 font-bold text-white"
|
||||||
>
|
>
|
||||||
{{ subGroupIndex + 1 }}
|
{{ subGroupIndex + 1 }}
|
||||||
</div>
|
</div>
|
||||||
<span>子条件组 {{ subGroupIndex + 1 }}</span>
|
<span>子条件组 {{ subGroupIndex + 1 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag size="small" type="warning" class="font-500">组内条件为"且"关系</el-tag>
|
<el-tag size="small" type="warning" class="font-500">
|
||||||
<el-tag size="small" type="info"> {{ subGroup?.length || 0 }}个条件 </el-tag>
|
组内条件为"且"关系
|
||||||
|
</el-tag>
|
||||||
|
<el-tag size="small" type="info">
|
||||||
|
{{ subGroup?.length || 0 }}个条件
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -119,7 +222,9 @@
|
|||||||
|
|
||||||
<SubConditionGroupConfig
|
<SubConditionGroupConfig
|
||||||
:model-value="subGroup"
|
:model-value="subGroup"
|
||||||
@update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
|
@update:model-value="
|
||||||
|
(value) => updateSubGroup(subGroupIndex, value)
|
||||||
|
"
|
||||||
:trigger-type="trigger.type"
|
:trigger-type="trigger.type"
|
||||||
:max-conditions="maxConditionsPerGroup"
|
:max-conditions="maxConditionsPerGroup"
|
||||||
/>
|
/>
|
||||||
@@ -128,13 +233,15 @@
|
|||||||
<!-- 子条件组间的"或"连接符 -->
|
<!-- 子条件组间的"或"连接符 -->
|
||||||
<div
|
<div
|
||||||
v-if="subGroupIndex < trigger.conditionGroups!.length - 1"
|
v-if="subGroupIndex < trigger.conditionGroups!.length - 1"
|
||||||
class="flex items-center justify-center py-12px"
|
class="py-12px flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<!-- 连接线 -->
|
<!-- 连接线 -->
|
||||||
<div class="w-32px h-1px bg-orange-300"></div>
|
<div class="w-32px h-1px bg-orange-300"></div>
|
||||||
<!-- 或标签 -->
|
<!-- 或标签 -->
|
||||||
<div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full">
|
<div
|
||||||
|
class="px-16px py-6px rounded-full border-2 border-orange-300 bg-orange-100"
|
||||||
|
>
|
||||||
<span class="text-14px font-600 text-orange-600">或</span>
|
<span class="text-14px font-600 text-orange-600">或</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 连接线 -->
|
<!-- 连接线 -->
|
||||||
@@ -148,9 +255,9 @@
|
|||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="p-24px border-2 border-dashed border-orange-200 rounded-8px text-center bg-orange-50"
|
class="p-24px rounded-8px border-2 border-dashed border-orange-200 bg-orange-50 text-center"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center gap-12px">
|
<div class="gap-12px flex flex-col items-center">
|
||||||
<Icon icon="ep:plus" class="text-32px text-orange-400" />
|
<Icon icon="ep:plus" class="text-32px text-orange-400" />
|
||||||
<div class="text-orange-600">
|
<div class="text-orange-600">
|
||||||
<p class="text-14px font-500 mb-4px">暂无子条件组</p>
|
<p class="text-14px font-500 mb-4px">暂无子条件组</p>
|
||||||
@@ -162,90 +269,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
|
|
||||||
import MainConditionInnerConfig from './MainConditionInnerConfig.vue'
|
|
||||||
import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
|
|
||||||
import type { Trigger } from '#/api/iot/rule/scene'
|
|
||||||
|
|
||||||
/** 设备触发配置组件 */
|
|
||||||
defineOptions({ name: 'DeviceTriggerConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: Trigger
|
|
||||||
index: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: Trigger): void
|
|
||||||
(e: 'trigger-type-change', type: number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const trigger = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
const maxSubGroups = 3 // 最多 3 个子条件组
|
|
||||||
const maxConditionsPerGroup = 3 // 每组最多 3 个条件
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新条件
|
|
||||||
* @param condition 条件对象
|
|
||||||
*/
|
|
||||||
const updateCondition = (condition: Trigger) => {
|
|
||||||
trigger.value = condition
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理触发器类型变化事件
|
|
||||||
* @param type 触发器类型
|
|
||||||
*/
|
|
||||||
const handleTriggerTypeChange = (type: number) => {
|
|
||||||
trigger.value.type = type
|
|
||||||
emit('trigger-type-change', type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 添加子条件组 */
|
|
||||||
const addSubGroup = async () => {
|
|
||||||
if (!trigger.value.conditionGroups) {
|
|
||||||
trigger.value.conditionGroups = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否达到最大子组数量限制
|
|
||||||
if (trigger.value.conditionGroups?.length >= maxSubGroups) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 nextTick 确保响应式更新完成后再添加新的子组
|
|
||||||
await nextTick()
|
|
||||||
if (trigger.value.conditionGroups) {
|
|
||||||
trigger.value.conditionGroups.push([])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除子条件组
|
|
||||||
* @param index 子条件组索引
|
|
||||||
*/
|
|
||||||
const removeSubGroup = (index: number) => {
|
|
||||||
if (trigger.value.conditionGroups) {
|
|
||||||
trigger.value.conditionGroups.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新子条件组
|
|
||||||
* @param index 子条件组索引
|
|
||||||
* @param subGroup 子条件组数据
|
|
||||||
*/
|
|
||||||
const updateSubGroup = (index: number, subGroup: any) => {
|
|
||||||
if (trigger.value.conditionGroups) {
|
|
||||||
trigger.value.conditionGroups[index] = subGroup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 移除整个条件组 */
|
|
||||||
const removeConditionGroup = () => {
|
|
||||||
trigger.value.conditionGroups = undefined
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,174 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Trigger } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTriggerTypeLabel,
|
||||||
|
IoTDeviceStatusEnum,
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
triggerTypeOptions,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import JsonParamsInput from '../inputs/JsonParamsInput.vue';
|
||||||
|
import ValueInput from '../inputs/ValueInput.vue';
|
||||||
|
import DeviceSelector from '../selectors/DeviceSelector.vue';
|
||||||
|
import OperatorSelector from '../selectors/OperatorSelector.vue';
|
||||||
|
import ProductSelector from '../selectors/ProductSelector.vue';
|
||||||
|
import PropertySelector from '../selectors/PropertySelector.vue';
|
||||||
|
|
||||||
|
/** 主条件内部配置组件 */
|
||||||
|
defineOptions({ name: 'MainConditionInnerConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: Trigger;
|
||||||
|
triggerType: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: Trigger): void;
|
||||||
|
(e: 'trigger-type-change', value: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 获取设备状态变更选项(用于触发器配置) */
|
||||||
|
const deviceStatusChangeOptions = [
|
||||||
|
{
|
||||||
|
label: IoTDeviceStatusEnum.ONLINE.label,
|
||||||
|
value: IoTDeviceStatusEnum.ONLINE.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: IoTDeviceStatusEnum.OFFLINE.label,
|
||||||
|
value: IoTDeviceStatusEnum.OFFLINE.value,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const condition = useVModel(props, 'modelValue', emit);
|
||||||
|
const propertyType = ref(''); // 属性类型
|
||||||
|
const propertyConfig = ref<any>(null); // 属性配置
|
||||||
|
|
||||||
|
// 计算属性:是否为设备属性触发器
|
||||||
|
const isDevicePropertyTrigger = computed(() => {
|
||||||
|
return (
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:是否为设备状态触发器
|
||||||
|
const isDeviceStatusTrigger = computed(() => {
|
||||||
|
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:是否需要操作符选择(服务调用和事件上报不需要操作符)
|
||||||
|
const needsOperatorSelector = computed(() => {
|
||||||
|
const noOperatorTriggerTypes = [
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
|
||||||
|
] as number[];
|
||||||
|
return !noOperatorTriggerTypes.includes(props.triggerType);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:是否需要宽列布局(服务调用和事件上报不需要操作符列,所以值输入列更宽)
|
||||||
|
const isWideValueColumn = computed(() => {
|
||||||
|
const wideColumnTriggerTypes = [
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
|
||||||
|
] as number[];
|
||||||
|
return wideColumnTriggerTypes.includes(props.triggerType);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:值输入字段的标签文本
|
||||||
|
const valueInputLabel = computed(() => {
|
||||||
|
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
? '服务参数'
|
||||||
|
: '比较值';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:服务配置 - 用于 JsonParamsInput
|
||||||
|
const serviceConfig = computed(() => {
|
||||||
|
if (
|
||||||
|
propertyConfig.value &&
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
service: {
|
||||||
|
name: propertyConfig.value.name || '服务',
|
||||||
|
inputParams: propertyConfig.value.inputParams || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:事件配置 - 用于 JsonParamsInput
|
||||||
|
const eventConfig = computed(() => {
|
||||||
|
if (
|
||||||
|
propertyConfig.value &&
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
event: {
|
||||||
|
name: propertyConfig.value.name || '事件',
|
||||||
|
outputParams: propertyConfig.value.outputParams || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新条件字段
|
||||||
|
* @param field 字段名
|
||||||
|
* @param value 字段值
|
||||||
|
*/
|
||||||
|
const updateConditionField = (field: any, value: any) => {
|
||||||
|
condition.value[field] = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理触发器类型变化事件
|
||||||
|
* @param type 触发器类型
|
||||||
|
*/
|
||||||
|
const handleTriggerTypeChange = (type: number) => {
|
||||||
|
emit('trigger-type-change', type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理产品变化事件 */
|
||||||
|
const handleProductChange = () => {
|
||||||
|
// 产品变化时清空设备和属性
|
||||||
|
condition.value.deviceId = undefined;
|
||||||
|
condition.value.identifier = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理设备变化事件 */
|
||||||
|
const handleDeviceChange = () => {
|
||||||
|
// 设备变化时清空属性
|
||||||
|
condition.value.identifier = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理属性变化事件
|
||||||
|
* @param propertyInfo 属性信息对象
|
||||||
|
*/
|
||||||
|
const handlePropertyChange = (propertyInfo: any) => {
|
||||||
|
if (propertyInfo) {
|
||||||
|
propertyType.value = propertyInfo.type;
|
||||||
|
propertyConfig.value = propertyInfo.config;
|
||||||
|
|
||||||
|
// 对于事件上报和服务调用,自动设置操作符为 '='
|
||||||
|
if (
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||||
|
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
) {
|
||||||
|
condition.value.operator =
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
<!-- 触发事件类型选择 -->
|
<!-- 触发事件类型选择 -->
|
||||||
@@ -25,7 +196,9 @@
|
|||||||
<el-form-item label="产品" required>
|
<el-form-item label="产品" required>
|
||||||
<ProductSelector
|
<ProductSelector
|
||||||
:model-value="condition.productId"
|
:model-value="condition.productId"
|
||||||
@update:model-value="(value) => updateConditionField('productId', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('productId', value)
|
||||||
|
"
|
||||||
@change="handleProductChange"
|
@change="handleProductChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -34,7 +207,9 @@
|
|||||||
<el-form-item label="设备" required>
|
<el-form-item label="设备" required>
|
||||||
<DeviceSelector
|
<DeviceSelector
|
||||||
:model-value="condition.deviceId"
|
:model-value="condition.deviceId"
|
||||||
@update:model-value="(value) => updateConditionField('deviceId', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('deviceId', value)
|
||||||
|
"
|
||||||
:product-id="condition.productId"
|
:product-id="condition.productId"
|
||||||
@change="handleDeviceChange"
|
@change="handleDeviceChange"
|
||||||
/>
|
/>
|
||||||
@@ -49,7 +224,9 @@
|
|||||||
<el-form-item label="监控项" required>
|
<el-form-item label="监控项" required>
|
||||||
<PropertySelector
|
<PropertySelector
|
||||||
:model-value="condition.identifier"
|
:model-value="condition.identifier"
|
||||||
@update:model-value="(value) => updateConditionField('identifier', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('identifier', value)
|
||||||
|
"
|
||||||
:trigger-type="triggerType"
|
:trigger-type="triggerType"
|
||||||
:product-id="condition.productId"
|
:product-id="condition.productId"
|
||||||
:device-id="condition.deviceId"
|
:device-id="condition.deviceId"
|
||||||
@@ -63,7 +240,9 @@
|
|||||||
<el-form-item label="操作符" required>
|
<el-form-item label="操作符" required>
|
||||||
<OperatorSelector
|
<OperatorSelector
|
||||||
:model-value="condition.operator"
|
:model-value="condition.operator"
|
||||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('operator', value)
|
||||||
|
"
|
||||||
:property-type="propertyType"
|
:property-type="propertyType"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -74,7 +253,10 @@
|
|||||||
<el-form-item :label="valueInputLabel" required>
|
<el-form-item :label="valueInputLabel" required>
|
||||||
<!-- 服务调用参数配置 -->
|
<!-- 服务调用参数配置 -->
|
||||||
<JsonParamsInput
|
<JsonParamsInput
|
||||||
v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
|
v-if="
|
||||||
|
triggerType ===
|
||||||
|
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
||||||
|
"
|
||||||
v-model="condition.value"
|
v-model="condition.value"
|
||||||
type="service"
|
type="service"
|
||||||
:config="serviceConfig"
|
:config="serviceConfig"
|
||||||
@@ -82,7 +264,9 @@
|
|||||||
/>
|
/>
|
||||||
<!-- 事件上报参数配置 -->
|
<!-- 事件上报参数配置 -->
|
||||||
<JsonParamsInput
|
<JsonParamsInput
|
||||||
v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
|
v-else-if="
|
||||||
|
triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
||||||
|
"
|
||||||
v-model="condition.value"
|
v-model="condition.value"
|
||||||
type="event"
|
type="event"
|
||||||
:config="eventConfig"
|
:config="eventConfig"
|
||||||
@@ -92,7 +276,9 @@
|
|||||||
<ValueInput
|
<ValueInput
|
||||||
v-else
|
v-else
|
||||||
:model-value="condition.value"
|
:model-value="condition.value"
|
||||||
@update:model-value="(value) => updateConditionField('value', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('value', value)
|
||||||
|
"
|
||||||
:property-type="propertyType"
|
:property-type="propertyType"
|
||||||
:operator="condition.operator"
|
:operator="condition.operator"
|
||||||
:property-config="propertyConfig"
|
:property-config="propertyConfig"
|
||||||
@@ -110,7 +296,9 @@
|
|||||||
<el-form-item label="产品" required>
|
<el-form-item label="产品" required>
|
||||||
<ProductSelector
|
<ProductSelector
|
||||||
:model-value="condition.productId"
|
:model-value="condition.productId"
|
||||||
@update:model-value="(value) => updateConditionField('productId', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('productId', value)
|
||||||
|
"
|
||||||
@change="handleProductChange"
|
@change="handleProductChange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -119,7 +307,9 @@
|
|||||||
<el-form-item label="设备" required>
|
<el-form-item label="设备" required>
|
||||||
<DeviceSelector
|
<DeviceSelector
|
||||||
:model-value="condition.deviceId"
|
:model-value="condition.deviceId"
|
||||||
@update:model-value="(value) => updateConditionField('deviceId', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('deviceId', value)
|
||||||
|
"
|
||||||
:product-id="condition.productId"
|
:product-id="condition.productId"
|
||||||
@change="handleDeviceChange"
|
@change="handleDeviceChange"
|
||||||
/>
|
/>
|
||||||
@@ -131,13 +321,19 @@
|
|||||||
<el-form-item label="操作符" required>
|
<el-form-item label="操作符" required>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="condition.operator"
|
:model-value="condition.operator"
|
||||||
@update:model-value="(value) => updateConditionField('operator', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('operator', value)
|
||||||
|
"
|
||||||
placeholder="请选择操作符"
|
placeholder="请选择操作符"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
:label="IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name"
|
:label="
|
||||||
:value="IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value"
|
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name
|
||||||
|
"
|
||||||
|
:value="
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -146,7 +342,9 @@
|
|||||||
<el-form-item label="参数" required>
|
<el-form-item label="参数" required>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="condition.value"
|
:model-value="condition.value"
|
||||||
@update:model-value="(value) => updateConditionField('value', value)"
|
@update:model-value="
|
||||||
|
(value) => updateConditionField('value', value)
|
||||||
|
"
|
||||||
placeholder="请选择操作符"
|
placeholder="请选择操作符"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
@@ -163,8 +361,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 其他触发类型的提示 -->
|
<!-- 其他触发类型的提示 -->
|
||||||
<div v-else class="text-center py-20px">
|
<div v-else class="py-20px text-center">
|
||||||
<p class="text-14px text-[var(--el-text-color-secondary)] mb-4px">
|
<p class="text-14px mb-4px text-[var(--el-text-color-secondary)]">
|
||||||
当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
|
当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-12px text-[var(--el-text-color-placeholder)]">
|
<p class="text-12px text-[var(--el-text-color-placeholder)]">
|
||||||
@@ -173,168 +371,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import ProductSelector from '../selectors/ProductSelector.vue'
|
|
||||||
import DeviceSelector from '../selectors/DeviceSelector.vue'
|
|
||||||
import PropertySelector from '../selectors/PropertySelector.vue'
|
|
||||||
import OperatorSelector from '../selectors/OperatorSelector.vue'
|
|
||||||
import ValueInput from '../inputs/ValueInput.vue'
|
|
||||||
import JsonParamsInput from '../inputs/JsonParamsInput.vue'
|
|
||||||
|
|
||||||
import type { Trigger } from '#/api/iot/rule/scene'
|
|
||||||
import {
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
triggerTypeOptions,
|
|
||||||
getTriggerTypeLabel,
|
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
|
||||||
IoTDeviceStatusEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
|
|
||||||
/** 主条件内部配置组件 */
|
|
||||||
defineOptions({ name: 'MainConditionInnerConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: Trigger
|
|
||||||
triggerType: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: Trigger): void
|
|
||||||
(e: 'trigger-type-change', value: number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
/** 获取设备状态变更选项(用于触发器配置) */
|
|
||||||
const deviceStatusChangeOptions = [
|
|
||||||
{
|
|
||||||
label: IoTDeviceStatusEnum.ONLINE.label,
|
|
||||||
value: IoTDeviceStatusEnum.ONLINE.value
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: IoTDeviceStatusEnum.OFFLINE.label,
|
|
||||||
value: IoTDeviceStatusEnum.OFFLINE.value
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const condition = useVModel(props, 'modelValue', emit)
|
|
||||||
const propertyType = ref('') // 属性类型
|
|
||||||
const propertyConfig = ref<any>(null) // 属性配置
|
|
||||||
|
|
||||||
// 计算属性:是否为设备属性触发器
|
|
||||||
const isDevicePropertyTrigger = computed(() => {
|
|
||||||
return (
|
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:是否为设备状态触发器
|
|
||||||
const isDeviceStatusTrigger = computed(() => {
|
|
||||||
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:是否需要操作符选择(服务调用和事件上报不需要操作符)
|
|
||||||
const needsOperatorSelector = computed(() => {
|
|
||||||
const noOperatorTriggerTypes = [
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
|
||||||
] as number[]
|
|
||||||
return !noOperatorTriggerTypes.includes(props.triggerType)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:是否需要宽列布局(服务调用和事件上报不需要操作符列,所以值输入列更宽)
|
|
||||||
const isWideValueColumn = computed(() => {
|
|
||||||
const wideColumnTriggerTypes = [
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
|
|
||||||
] as number[]
|
|
||||||
return wideColumnTriggerTypes.includes(props.triggerType)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:值输入字段的标签文本
|
|
||||||
const valueInputLabel = computed(() => {
|
|
||||||
return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
? '服务参数'
|
|
||||||
: '比较值'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:服务配置 - 用于 JsonParamsInput
|
|
||||||
const serviceConfig = computed(() => {
|
|
||||||
if (
|
|
||||||
propertyConfig.value &&
|
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
service: {
|
|
||||||
name: propertyConfig.value.name || '服务',
|
|
||||||
inputParams: propertyConfig.value.inputParams || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:事件配置 - 用于 JsonParamsInput
|
|
||||||
const eventConfig = computed(() => {
|
|
||||||
if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
|
||||||
return {
|
|
||||||
event: {
|
|
||||||
name: propertyConfig.value.name || '事件',
|
|
||||||
outputParams: propertyConfig.value.outputParams || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新条件字段
|
|
||||||
* @param field 字段名
|
|
||||||
* @param value 字段值
|
|
||||||
*/
|
|
||||||
const updateConditionField = (field: any, value: any) => {
|
|
||||||
condition.value[field] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理触发器类型变化事件
|
|
||||||
* @param type 触发器类型
|
|
||||||
*/
|
|
||||||
const handleTriggerTypeChange = (type: number) => {
|
|
||||||
emit('trigger-type-change', type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理产品变化事件 */
|
|
||||||
const handleProductChange = () => {
|
|
||||||
// 产品变化时清空设备和属性
|
|
||||||
condition.value.deviceId = undefined
|
|
||||||
condition.value.identifier = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理设备变化事件 */
|
|
||||||
const handleDeviceChange = () => {
|
|
||||||
// 设备变化时清空属性
|
|
||||||
condition.value.identifier = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理属性变化事件
|
|
||||||
* @param propertyInfo 属性信息对象
|
|
||||||
*/
|
|
||||||
const handlePropertyChange = (propertyInfo: any) => {
|
|
||||||
if (propertyInfo) {
|
|
||||||
propertyType.value = propertyInfo.type
|
|
||||||
propertyConfig.value = propertyInfo.config
|
|
||||||
|
|
||||||
// 对于事件上报和服务调用,自动设置操作符为 '='
|
|
||||||
if (
|
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
|
||||||
props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
) {
|
|
||||||
condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,9 +1,93 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import ConditionConfig from './ConditionConfig.vue';
|
||||||
|
|
||||||
|
/** 子条件组配置组件 */
|
||||||
|
defineOptions({ name: 'SubConditionGroupConfig' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
maxConditions?: number;
|
||||||
|
modelValue: TriggerCondition[];
|
||||||
|
triggerType: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: TriggerCondition[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const subGroup = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const maxConditions = computed(() => props.maxConditions || 3); // 最大条件数量
|
||||||
|
|
||||||
|
/** 添加条件 */
|
||||||
|
const addCondition = async () => {
|
||||||
|
// 确保 subGroup.value 是一个数组
|
||||||
|
if (!subGroup.value) {
|
||||||
|
subGroup.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否达到最大条件数量限制
|
||||||
|
if (subGroup.value?.length >= maxConditions.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCondition: TriggerCondition = {
|
||||||
|
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
||||||
|
productId: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
identifier: '',
|
||||||
|
operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 使用枚举默认值
|
||||||
|
param: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用 nextTick 确保响应式更新完成后再添加新条件
|
||||||
|
await nextTick();
|
||||||
|
if (subGroup.value) {
|
||||||
|
subGroup.value.push(newCondition);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除条件
|
||||||
|
* @param index 条件索引
|
||||||
|
*/
|
||||||
|
const removeCondition = (index: number) => {
|
||||||
|
if (subGroup.value) {
|
||||||
|
subGroup.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新条件
|
||||||
|
* @param index 条件索引
|
||||||
|
* @param condition 条件对象
|
||||||
|
*/
|
||||||
|
const updateCondition = (index: number, condition: TriggerCondition) => {
|
||||||
|
if (subGroup.value) {
|
||||||
|
subGroup.value[index] = condition;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-16px">
|
<div class="p-16px">
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="!subGroup || subGroup.length === 0" class="text-center py-24px">
|
<div v-if="!subGroup || subGroup.length === 0" class="py-24px text-center">
|
||||||
<div class="flex flex-col items-center gap-12px">
|
<div class="gap-12px flex flex-col items-center">
|
||||||
<Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
|
<Icon
|
||||||
|
icon="ep:plus"
|
||||||
|
class="text-32px text-[var(--el-text-color-placeholder)]"
|
||||||
|
/>
|
||||||
<div class="text-[var(--el-text-color-secondary)]">
|
<div class="text-[var(--el-text-color-secondary)]">
|
||||||
<p class="text-14px font-500 mb-4px">暂无条件</p>
|
<p class="text-14px font-500 mb-4px">暂无条件</p>
|
||||||
<p class="text-12px">点击下方按钮添加第一个条件</p>
|
<p class="text-12px">点击下方按钮添加第一个条件</p>
|
||||||
@@ -24,18 +108,19 @@
|
|||||||
>
|
>
|
||||||
<!-- 条件配置 -->
|
<!-- 条件配置 -->
|
||||||
<div
|
<div
|
||||||
class="border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)] shadow-sm"
|
class="rounded-6px border border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-blank)] shadow-sm"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-12px bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-lighter)] rounded-t-4px"
|
class="p-12px rounded-t-4px flex items-center justify-between border-b border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<div
|
<div
|
||||||
class="w-20px h-20px bg-blue-500 text-white rounded-full flex items-center justify-center text-10px font-bold"
|
class="w-20px h-20px text-10px flex items-center justify-center rounded-full bg-blue-500 font-bold text-white"
|
||||||
>
|
>
|
||||||
{{ conditionIndex + 1 }}
|
{{ conditionIndex + 1 }}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-12px font-500 text-[var(--el-text-color-primary)]"
|
<span
|
||||||
|
class="text-12px font-500 text-[var(--el-text-color-primary)]"
|
||||||
>条件 {{ conditionIndex + 1 }}</span
|
>条件 {{ conditionIndex + 1 }}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +139,9 @@
|
|||||||
<div class="p-12px">
|
<div class="p-12px">
|
||||||
<ConditionConfig
|
<ConditionConfig
|
||||||
:model-value="condition"
|
:model-value="condition"
|
||||||
@update:model-value="(value) => updateCondition(conditionIndex, value)"
|
@update:model-value="
|
||||||
|
(value) => updateCondition(conditionIndex, value)
|
||||||
|
"
|
||||||
:trigger-type="triggerType"
|
:trigger-type="triggerType"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,94 +150,21 @@
|
|||||||
|
|
||||||
<!-- 添加条件按钮 -->
|
<!-- 添加条件按钮 -->
|
||||||
<div
|
<div
|
||||||
v-if="subGroup && subGroup.length > 0 && subGroup.length < maxConditions"
|
v-if="
|
||||||
class="text-center py-16px"
|
subGroup && subGroup.length > 0 && subGroup.length < maxConditions
|
||||||
|
"
|
||||||
|
class="py-16px text-center"
|
||||||
>
|
>
|
||||||
<el-button type="primary" plain @click="addCondition">
|
<el-button type="primary" plain @click="addCondition">
|
||||||
<Icon icon="ep:plus" />
|
<Icon icon="ep:plus" />
|
||||||
继续添加条件
|
继续添加条件
|
||||||
</el-button>
|
</el-button>
|
||||||
<span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
|
<span
|
||||||
|
class="mt-8px text-12px block text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
最多可添加 {{ maxConditions }} 个条件
|
最多可添加 {{ maxConditions }} 个条件
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { nextTick } from 'vue'
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import ConditionConfig from './ConditionConfig.vue'
|
|
||||||
import type { TriggerCondition } from '#/api/iot/rule/scene'
|
|
||||||
import {
|
|
||||||
IotRuleSceneTriggerConditionTypeEnum,
|
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 子条件组配置组件 */
|
|
||||||
defineOptions({ name: 'SubConditionGroupConfig' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: TriggerCondition[]
|
|
||||||
triggerType: number
|
|
||||||
maxConditions?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: TriggerCondition[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const subGroup = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
const maxConditions = computed(() => props.maxConditions || 3) // 最大条件数量
|
|
||||||
|
|
||||||
/** 添加条件 */
|
|
||||||
const addCondition = async () => {
|
|
||||||
// 确保 subGroup.value 是一个数组
|
|
||||||
if (!subGroup.value) {
|
|
||||||
subGroup.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否达到最大条件数量限制
|
|
||||||
if (subGroup.value?.length >= maxConditions.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCondition: TriggerCondition = {
|
|
||||||
type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 默认为设备属性
|
|
||||||
productId: undefined,
|
|
||||||
deviceId: undefined,
|
|
||||||
identifier: '',
|
|
||||||
operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 使用枚举默认值
|
|
||||||
param: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 nextTick 确保响应式更新完成后再添加新条件
|
|
||||||
await nextTick()
|
|
||||||
if (subGroup.value) {
|
|
||||||
subGroup.value.push(newCondition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除条件
|
|
||||||
* @param index 条件索引
|
|
||||||
*/
|
|
||||||
const removeCondition = (index: number) => {
|
|
||||||
if (subGroup.value) {
|
|
||||||
subGroup.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新条件
|
|
||||||
* @param index 条件索引
|
|
||||||
* @param condition 条件对象
|
|
||||||
*/
|
|
||||||
const updateCondition = (index: number, condition: TriggerCondition) => {
|
|
||||||
if (subGroup.value) {
|
|
||||||
subGroup.value[index] = condition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,7 +1,417 @@
|
|||||||
<!-- JSON参数输入组件 - 通用版本 -->
|
<!-- JSON参数输入组件 - 通用版本 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { JsonParamsInputType } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
JSON_PARAMS_EXAMPLE_VALUES,
|
||||||
|
JSON_PARAMS_INPUT_CONSTANTS,
|
||||||
|
JSON_PARAMS_INPUT_ICONS,
|
||||||
|
JsonParamsInputTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** JSON参数输入组件 - 通用版本 */
|
||||||
|
defineOptions({ name: 'JsonParamsInput' });
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
type: JsonParamsInputTypeEnum.SERVICE,
|
||||||
|
placeholder: JSON_PARAMS_INPUT_CONSTANTS.PLACEHOLDER,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
interface JsonParamsConfig {
|
||||||
|
// 服务配置
|
||||||
|
service?: {
|
||||||
|
inputParams?: any[];
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
// 事件配置
|
||||||
|
event?: {
|
||||||
|
name: string;
|
||||||
|
outputParams?: any[];
|
||||||
|
};
|
||||||
|
// 属性配置
|
||||||
|
properties?: any[];
|
||||||
|
// 自定义配置
|
||||||
|
custom?: {
|
||||||
|
name: string;
|
||||||
|
params: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string;
|
||||||
|
config?: JsonParamsConfig;
|
||||||
|
type?: JsonParamsInputType;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localValue = useVModel(props, 'modelValue', emit, {
|
||||||
|
defaultValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const paramsJson = ref(''); // JSON参数字符串
|
||||||
|
const jsonError = ref(''); // JSON验证错误信息
|
||||||
|
|
||||||
|
// 计算属性:参数列表
|
||||||
|
const paramsList = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case JsonParamsInputTypeEnum.CUSTOM: {
|
||||||
|
return props.config?.custom?.params || [];
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.EVENT: {
|
||||||
|
return props.config?.event?.outputParams || [];
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.PROPERTY: {
|
||||||
|
return props.config?.properties || [];
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.SERVICE: {
|
||||||
|
return props.config?.service?.inputParams || [];
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:标题
|
||||||
|
const title = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case JsonParamsInputTypeEnum.CUSTOM: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.CUSTOM(
|
||||||
|
props.config?.custom?.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.EVENT: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.EVENT(
|
||||||
|
props.config?.event?.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.PROPERTY: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.PROPERTY;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.SERVICE: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.SERVICE(
|
||||||
|
props.config?.service?.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:标题图标
|
||||||
|
const titleIcon = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case JsonParamsInputTypeEnum.CUSTOM: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.CUSTOM;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.EVENT: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.EVENT;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.PROPERTY: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.PROPERTY;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.SERVICE: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.SERVICE;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:参数图标
|
||||||
|
const paramsIcon = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case JsonParamsInputTypeEnum.CUSTOM: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.CUSTOM;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.EVENT: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.EVENT;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.PROPERTY: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.PROPERTY;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.SERVICE: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.SERVICE;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:参数标签
|
||||||
|
const paramsLabel = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case JsonParamsInputTypeEnum.CUSTOM: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.CUSTOM;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.EVENT: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.EVENT;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.PROPERTY: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.PROPERTY;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.SERVICE: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.SERVICE;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:空状态消息
|
||||||
|
const emptyMessage = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case JsonParamsInputTypeEnum.CUSTOM: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.CUSTOM;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.EVENT: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.EVENT;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.PROPERTY: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.PROPERTY;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.SERVICE: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.SERVICE;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:无配置消息
|
||||||
|
const noConfigMessage = computed(() => {
|
||||||
|
switch (props.type) {
|
||||||
|
case JsonParamsInputTypeEnum.CUSTOM: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.CUSTOM;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.EVENT: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.EVENT;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.PROPERTY: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.PROPERTY;
|
||||||
|
}
|
||||||
|
case JsonParamsInputTypeEnum.SERVICE: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.SERVICE;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.DEFAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理参数变化事件
|
||||||
|
*/
|
||||||
|
const handleParamsChange = () => {
|
||||||
|
try {
|
||||||
|
jsonError.value = ''; // 清除之前的错误
|
||||||
|
|
||||||
|
if (paramsJson.value.trim()) {
|
||||||
|
const parsed = JSON.parse(paramsJson.value);
|
||||||
|
localValue.value = paramsJson.value;
|
||||||
|
|
||||||
|
// 额外的参数验证
|
||||||
|
if (typeof parsed !== 'object' || parsed === null) {
|
||||||
|
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证必填参数
|
||||||
|
for (const param of paramsList.value) {
|
||||||
|
if (
|
||||||
|
param.required &&
|
||||||
|
(!parsed[param.identifier] || parsed[param.identifier] === '')
|
||||||
|
) {
|
||||||
|
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(
|
||||||
|
param.name,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localValue.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通过
|
||||||
|
jsonError.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_ERROR(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: JSON_PARAMS_INPUT_CONSTANTS.UNKNOWN_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速填充示例数据
|
||||||
|
*/
|
||||||
|
const fillExampleJson = () => {
|
||||||
|
paramsJson.value = generateExampleJson();
|
||||||
|
handleParamsChange();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空参数
|
||||||
|
*/
|
||||||
|
const clearParams = () => {
|
||||||
|
paramsJson.value = '';
|
||||||
|
localValue.value = '';
|
||||||
|
jsonError.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取参数类型名称
|
||||||
|
* @param dataType 数据类型
|
||||||
|
* @returns 类型名称
|
||||||
|
*/
|
||||||
|
const getParamTypeName = (dataType: string) => {
|
||||||
|
// 使用 constants.ts 中已有的 getDataTypeName 函数逻辑
|
||||||
|
const typeMap = {
|
||||||
|
[IoTDataSpecsDataTypeEnum.INT]: '整数',
|
||||||
|
[IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
|
||||||
|
[IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
|
||||||
|
[IoTDataSpecsDataTypeEnum.TEXT]: '字符串',
|
||||||
|
[IoTDataSpecsDataTypeEnum.BOOL]: '布尔值',
|
||||||
|
[IoTDataSpecsDataTypeEnum.ENUM]: '枚举',
|
||||||
|
[IoTDataSpecsDataTypeEnum.DATE]: '日期',
|
||||||
|
[IoTDataSpecsDataTypeEnum.STRUCT]: '结构体',
|
||||||
|
[IoTDataSpecsDataTypeEnum.ARRAY]: '数组',
|
||||||
|
};
|
||||||
|
return typeMap[dataType] || dataType;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取参数类型标签样式
|
||||||
|
* @param dataType 数据类型
|
||||||
|
* @returns 标签样式
|
||||||
|
*/
|
||||||
|
const getParamTypeTag = (dataType: string) => {
|
||||||
|
const tagMap = {
|
||||||
|
[IoTDataSpecsDataTypeEnum.INT]: 'primary',
|
||||||
|
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
|
||||||
|
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
|
||||||
|
[IoTDataSpecsDataTypeEnum.TEXT]: 'info',
|
||||||
|
[IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
|
||||||
|
[IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
|
||||||
|
[IoTDataSpecsDataTypeEnum.DATE]: 'primary',
|
||||||
|
[IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
|
||||||
|
[IoTDataSpecsDataTypeEnum.ARRAY]: 'warning',
|
||||||
|
};
|
||||||
|
return tagMap[dataType] || 'info';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取示例值
|
||||||
|
* @param param 参数对象
|
||||||
|
* @returns 示例值
|
||||||
|
*/
|
||||||
|
const getExampleValue = (param: any) => {
|
||||||
|
const exampleConfig =
|
||||||
|
JSON_PARAMS_EXAMPLE_VALUES[param.dataType] ||
|
||||||
|
JSON_PARAMS_EXAMPLE_VALUES.DEFAULT;
|
||||||
|
return exampleConfig.display;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成示例JSON
|
||||||
|
* @returns JSON字符串
|
||||||
|
*/
|
||||||
|
const generateExampleJson = () => {
|
||||||
|
if (paramsList.value.length === 0) {
|
||||||
|
return '{}';
|
||||||
|
}
|
||||||
|
|
||||||
|
const example = {};
|
||||||
|
paramsList.value.forEach((param) => {
|
||||||
|
const exampleConfig =
|
||||||
|
JSON_PARAMS_EXAMPLE_VALUES[param.dataType] ||
|
||||||
|
JSON_PARAMS_EXAMPLE_VALUES.DEFAULT;
|
||||||
|
example[param.identifier] = exampleConfig.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.stringify(example, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理数据回显
|
||||||
|
* @param value 值字符串
|
||||||
|
*/
|
||||||
|
const handleDataDisplay = (value: string) => {
|
||||||
|
if (!value || !value.trim()) {
|
||||||
|
paramsJson.value = '';
|
||||||
|
jsonError.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试解析JSON,如果成功则格式化
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
paramsJson.value = JSON.stringify(parsed, null, 2);
|
||||||
|
jsonError.value = '';
|
||||||
|
} catch {
|
||||||
|
// 如果不是有效的JSON,直接使用原字符串
|
||||||
|
paramsJson.value = value;
|
||||||
|
jsonError.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听外部值变化(编辑模式数据回显)
|
||||||
|
watch(
|
||||||
|
() => localValue.value,
|
||||||
|
async (newValue, oldValue) => {
|
||||||
|
// 避免循环更新
|
||||||
|
if (newValue === oldValue) return;
|
||||||
|
|
||||||
|
// 使用 nextTick 确保在下一个 tick 中处理数据
|
||||||
|
await nextTick();
|
||||||
|
handleDataDisplay(newValue || '');
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 组件挂载后也尝试处理一次数据回显
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick();
|
||||||
|
if (localValue.value) {
|
||||||
|
handleDataDisplay(localValue.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听配置变化
|
||||||
|
watch(
|
||||||
|
() => props.config,
|
||||||
|
(newConfig, oldConfig) => {
|
||||||
|
// 只有在配置真正变化时才清空数据
|
||||||
|
if (
|
||||||
|
JSON.stringify(newConfig) !== JSON.stringify(oldConfig) && // 如果没有外部传入的值,才清空数据
|
||||||
|
!localValue.value
|
||||||
|
) {
|
||||||
|
paramsJson.value = '';
|
||||||
|
jsonError.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 参数配置 -->
|
<!-- 参数配置 -->
|
||||||
<div class="w-full space-y-12px">
|
<div class="space-y-12px w-full">
|
||||||
<!-- JSON 输入框 -->
|
<!-- JSON 输入框 -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -13,7 +423,7 @@
|
|||||||
:class="{ 'is-error': jsonError }"
|
:class="{ 'is-error': jsonError }"
|
||||||
/>
|
/>
|
||||||
<!-- 查看详细示例弹出层 -->
|
<!-- 查看详细示例弹出层 -->
|
||||||
<div class="absolute top-8px right-8px">
|
<div class="top-8px right-8px absolute">
|
||||||
<el-popover
|
<el-popover
|
||||||
placement="left-start"
|
placement="left-start"
|
||||||
:width="450"
|
:width="450"
|
||||||
@@ -34,9 +444,14 @@
|
|||||||
|
|
||||||
<!-- 弹出层内容 -->
|
<!-- 弹出层内容 -->
|
||||||
<div class="json-params-detail-content">
|
<div class="json-params-detail-content">
|
||||||
<div class="flex items-center gap-8px mb-16px">
|
<div class="gap-8px mb-16px flex items-center">
|
||||||
<Icon :icon="titleIcon" class="text-[var(--el-color-primary)] text-18px" />
|
<Icon
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">
|
:icon="titleIcon"
|
||||||
|
class="text-18px text-[var(--el-color-primary)]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-16px font-600 text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,9 +459,14 @@
|
|||||||
<div class="space-y-16px">
|
<div class="space-y-16px">
|
||||||
<!-- 参数列表 -->
|
<!-- 参数列表 -->
|
||||||
<div v-if="paramsList.length > 0">
|
<div v-if="paramsList.length > 0">
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
<div class="gap-8px mb-8px flex items-center">
|
||||||
<Icon :icon="paramsIcon" class="text-[var(--el-color-primary)] text-14px" />
|
<Icon
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
:icon="paramsIcon"
|
||||||
|
class="text-14px text-[var(--el-color-primary)]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-14px font-500 text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
{{ paramsLabel }}
|
{{ paramsLabel }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,24 +474,38 @@
|
|||||||
<div
|
<div
|
||||||
v-for="param in paramsList"
|
v-for="param in paramsList"
|
||||||
:key="param.identifier"
|
:key="param.identifier"
|
||||||
class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
|
class="p-8px rounded-4px flex items-center justify-between bg-[var(--el-fill-color-lighter)]"
|
||||||
>
|
>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-12px font-500 text-[var(--el-text-color-primary)]">
|
<div
|
||||||
|
class="text-12px font-500 text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
{{ param.name }}
|
{{ param.name }}
|
||||||
<el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
|
<el-tag
|
||||||
|
v-if="param.required"
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
class="ml-4px"
|
||||||
|
>
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-11px text-[var(--el-text-color-secondary)]">
|
<div
|
||||||
|
class="text-11px text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
{{ param.identifier }}
|
{{ param.identifier }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<el-tag :type="getParamTypeTag(param.dataType)" size="small">
|
<el-tag
|
||||||
|
:type="getParamTypeTag(param.dataType)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{{ getParamTypeName(param.dataType) }}
|
{{ getParamTypeName(param.dataType) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<span class="text-11px text-[var(--el-text-color-secondary)]">
|
<span
|
||||||
|
class="text-11px text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
{{ getExampleValue(param) }}
|
{{ getExampleValue(param) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,11 +513,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-12px ml-22px">
|
<div class="mt-12px ml-22px">
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
|
<div
|
||||||
|
class="text-12px mb-6px text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
|
class="p-12px rounded-4px text-11px border-l-3px overflow-x-auto border-[var(--el-color-primary)] bg-[var(--el-fill-color-light)] text-[var(--el-text-color-primary)]"
|
||||||
>
|
>
|
||||||
<code>{{ generateExampleJson() }}</code>
|
<code>{{ generateExampleJson() }}</code>
|
||||||
</pre>
|
</pre>
|
||||||
@@ -92,8 +528,10 @@
|
|||||||
|
|
||||||
<!-- 无参数提示 -->
|
<!-- 无参数提示 -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="text-center py-16px">
|
<div class="py-16px text-center">
|
||||||
<p class="text-14px text-[var(--el-text-color-secondary)]">{{ emptyMessage }}</p>
|
<p class="text-14px text-[var(--el-text-color-secondary)]">
|
||||||
|
{{ emptyMessage }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,18 +542,26 @@
|
|||||||
|
|
||||||
<!-- 验证状态和错误提示 -->
|
<!-- 验证状态和错误提示 -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<Icon
|
<Icon
|
||||||
:icon="
|
:icon="
|
||||||
jsonError
|
jsonError
|
||||||
? JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.ERROR
|
? JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.ERROR
|
||||||
: JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.SUCCESS
|
: JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.SUCCESS
|
||||||
"
|
"
|
||||||
:class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
|
:class="
|
||||||
|
jsonError
|
||||||
|
? 'text-[var(--el-color-danger)]'
|
||||||
|
: 'text-[var(--el-color-success)]'
|
||||||
|
"
|
||||||
class="text-14px"
|
class="text-14px"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
:class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
|
:class="
|
||||||
|
jsonError
|
||||||
|
? 'text-[var(--el-color-danger)]'
|
||||||
|
: 'text-[var(--el-color-success)]'
|
||||||
|
"
|
||||||
class="text-12px"
|
class="text-12px"
|
||||||
>
|
>
|
||||||
{{ jsonError || JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_CORRECT }}
|
{{ jsonError || JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_CORRECT }}
|
||||||
@@ -123,379 +569,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 快速填充按钮 -->
|
<!-- 快速填充按钮 -->
|
||||||
<div v-if="paramsList.length > 0" class="flex items-center gap-8px">
|
<div v-if="paramsList.length > 0" class="gap-8px flex items-center">
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)]">{{
|
<span class="text-12px text-[var(--el-text-color-secondary)]">{{
|
||||||
JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL
|
JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL
|
||||||
}}</span>
|
}}</span>
|
||||||
<el-button size="small" type="primary" plain @click="fillExampleJson">
|
<el-button size="small" type="primary" plain @click="fillExampleJson">
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.EXAMPLE_DATA_BUTTON }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.EXAMPLE_DATA_BUTTON }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" type="danger" plain @click="clearParams">{{
|
<el-button size="small" type="danger" plain @click="clearParams">
|
||||||
JSON_PARAMS_INPUT_CONSTANTS.CLEAR_BUTTON
|
{{ JSON_PARAMS_INPUT_CONSTANTS.CLEAR_BUTTON }}
|
||||||
}}</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
|
||||||
import {
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
JSON_PARAMS_INPUT_CONSTANTS,
|
|
||||||
JSON_PARAMS_INPUT_ICONS,
|
|
||||||
JSON_PARAMS_EXAMPLE_VALUES,
|
|
||||||
JsonParamsInputTypeEnum,
|
|
||||||
type JsonParamsInputType
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** JSON参数输入组件 - 通用版本 */
|
|
||||||
defineOptions({ name: 'JsonParamsInput' })
|
|
||||||
|
|
||||||
interface JsonParamsConfig {
|
|
||||||
// 服务配置
|
|
||||||
service?: {
|
|
||||||
name: string
|
|
||||||
inputParams?: any[]
|
|
||||||
}
|
|
||||||
// 事件配置
|
|
||||||
event?: {
|
|
||||||
name: string
|
|
||||||
outputParams?: any[]
|
|
||||||
}
|
|
||||||
// 属性配置
|
|
||||||
properties?: any[]
|
|
||||||
// 自定义配置
|
|
||||||
custom?: {
|
|
||||||
name: string
|
|
||||||
params: any[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue?: string
|
|
||||||
config?: JsonParamsConfig
|
|
||||||
type?: JsonParamsInputType
|
|
||||||
placeholder?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'update:modelValue', value: string): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
type: JsonParamsInputTypeEnum.SERVICE,
|
|
||||||
placeholder: JSON_PARAMS_INPUT_CONSTANTS.PLACEHOLDER
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit, {
|
|
||||||
defaultValue: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const paramsJson = ref('') // JSON参数字符串
|
|
||||||
const jsonError = ref('') // JSON验证错误信息
|
|
||||||
|
|
||||||
// 计算属性:参数列表
|
|
||||||
const paramsList = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case JsonParamsInputTypeEnum.SERVICE:
|
|
||||||
return props.config?.service?.inputParams || []
|
|
||||||
case JsonParamsInputTypeEnum.EVENT:
|
|
||||||
return props.config?.event?.outputParams || []
|
|
||||||
case JsonParamsInputTypeEnum.PROPERTY:
|
|
||||||
return props.config?.properties || []
|
|
||||||
case JsonParamsInputTypeEnum.CUSTOM:
|
|
||||||
return props.config?.custom?.params || []
|
|
||||||
default:
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:标题
|
|
||||||
const title = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case JsonParamsInputTypeEnum.SERVICE:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.SERVICE(props.config?.service?.name)
|
|
||||||
case JsonParamsInputTypeEnum.EVENT:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.EVENT(props.config?.event?.name)
|
|
||||||
case JsonParamsInputTypeEnum.PROPERTY:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.PROPERTY
|
|
||||||
case JsonParamsInputTypeEnum.CUSTOM:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.CUSTOM(props.config?.custom?.name)
|
|
||||||
default:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.TITLES.DEFAULT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:标题图标
|
|
||||||
const titleIcon = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case JsonParamsInputTypeEnum.SERVICE:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.SERVICE
|
|
||||||
case JsonParamsInputTypeEnum.EVENT:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.EVENT
|
|
||||||
case JsonParamsInputTypeEnum.PROPERTY:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.PROPERTY
|
|
||||||
case JsonParamsInputTypeEnum.CUSTOM:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.CUSTOM
|
|
||||||
default:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.DEFAULT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:参数图标
|
|
||||||
const paramsIcon = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case JsonParamsInputTypeEnum.SERVICE:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.SERVICE
|
|
||||||
case JsonParamsInputTypeEnum.EVENT:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.EVENT
|
|
||||||
case JsonParamsInputTypeEnum.PROPERTY:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.PROPERTY
|
|
||||||
case JsonParamsInputTypeEnum.CUSTOM:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.CUSTOM
|
|
||||||
default:
|
|
||||||
return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.DEFAULT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:参数标签
|
|
||||||
const paramsLabel = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case JsonParamsInputTypeEnum.SERVICE:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.SERVICE
|
|
||||||
case JsonParamsInputTypeEnum.EVENT:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.EVENT
|
|
||||||
case JsonParamsInputTypeEnum.PROPERTY:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.PROPERTY
|
|
||||||
case JsonParamsInputTypeEnum.CUSTOM:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.CUSTOM
|
|
||||||
default:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.DEFAULT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:空状态消息
|
|
||||||
const emptyMessage = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case JsonParamsInputTypeEnum.SERVICE:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.SERVICE
|
|
||||||
case JsonParamsInputTypeEnum.EVENT:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.EVENT
|
|
||||||
case JsonParamsInputTypeEnum.PROPERTY:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.PROPERTY
|
|
||||||
case JsonParamsInputTypeEnum.CUSTOM:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.CUSTOM
|
|
||||||
default:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.DEFAULT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:无配置消息
|
|
||||||
const noConfigMessage = computed(() => {
|
|
||||||
switch (props.type) {
|
|
||||||
case JsonParamsInputTypeEnum.SERVICE:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.SERVICE
|
|
||||||
case JsonParamsInputTypeEnum.EVENT:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.EVENT
|
|
||||||
case JsonParamsInputTypeEnum.PROPERTY:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.PROPERTY
|
|
||||||
case JsonParamsInputTypeEnum.CUSTOM:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.CUSTOM
|
|
||||||
default:
|
|
||||||
return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.DEFAULT
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理参数变化事件
|
|
||||||
*/
|
|
||||||
const handleParamsChange = () => {
|
|
||||||
try {
|
|
||||||
jsonError.value = '' // 清除之前的错误
|
|
||||||
|
|
||||||
if (paramsJson.value.trim()) {
|
|
||||||
const parsed = JSON.parse(paramsJson.value)
|
|
||||||
localValue.value = paramsJson.value
|
|
||||||
|
|
||||||
// 额外的参数验证
|
|
||||||
if (typeof parsed !== 'object' || parsed === null) {
|
|
||||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证必填参数
|
|
||||||
for (const param of paramsList.value) {
|
|
||||||
if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
|
|
||||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(param.name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localValue.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证通过
|
|
||||||
jsonError.value = ''
|
|
||||||
} catch (error) {
|
|
||||||
jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_ERROR(
|
|
||||||
error instanceof Error ? error.message : JSON_PARAMS_INPUT_CONSTANTS.UNKNOWN_ERROR
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 快速填充示例数据
|
|
||||||
*/
|
|
||||||
const fillExampleJson = () => {
|
|
||||||
paramsJson.value = generateExampleJson()
|
|
||||||
handleParamsChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空参数
|
|
||||||
*/
|
|
||||||
const clearParams = () => {
|
|
||||||
paramsJson.value = ''
|
|
||||||
localValue.value = ''
|
|
||||||
jsonError.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取参数类型名称
|
|
||||||
* @param dataType 数据类型
|
|
||||||
* @returns 类型名称
|
|
||||||
*/
|
|
||||||
const getParamTypeName = (dataType: string) => {
|
|
||||||
// 使用 constants.ts 中已有的 getDataTypeName 函数逻辑
|
|
||||||
const typeMap = {
|
|
||||||
[IoTDataSpecsDataTypeEnum.INT]: '整数',
|
|
||||||
[IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
|
|
||||||
[IoTDataSpecsDataTypeEnum.TEXT]: '字符串',
|
|
||||||
[IoTDataSpecsDataTypeEnum.BOOL]: '布尔值',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ENUM]: '枚举',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DATE]: '日期',
|
|
||||||
[IoTDataSpecsDataTypeEnum.STRUCT]: '结构体',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ARRAY]: '数组'
|
|
||||||
}
|
|
||||||
return typeMap[dataType] || dataType
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取参数类型标签样式
|
|
||||||
* @param dataType 数据类型
|
|
||||||
* @returns 标签样式
|
|
||||||
*/
|
|
||||||
const getParamTypeTag = (dataType: string) => {
|
|
||||||
const tagMap = {
|
|
||||||
[IoTDataSpecsDataTypeEnum.INT]: 'primary',
|
|
||||||
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
|
|
||||||
[IoTDataSpecsDataTypeEnum.TEXT]: 'info',
|
|
||||||
[IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DATE]: 'primary',
|
|
||||||
[IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ARRAY]: 'warning'
|
|
||||||
}
|
|
||||||
return tagMap[dataType] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取示例值
|
|
||||||
* @param param 参数对象
|
|
||||||
* @returns 示例值
|
|
||||||
*/
|
|
||||||
const getExampleValue = (param: any) => {
|
|
||||||
const exampleConfig =
|
|
||||||
JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
|
|
||||||
return exampleConfig.display
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成示例JSON
|
|
||||||
* @returns JSON字符串
|
|
||||||
*/
|
|
||||||
const generateExampleJson = () => {
|
|
||||||
if (paramsList.value.length === 0) {
|
|
||||||
return '{}'
|
|
||||||
}
|
|
||||||
|
|
||||||
const example = {}
|
|
||||||
paramsList.value.forEach((param) => {
|
|
||||||
const exampleConfig =
|
|
||||||
JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
|
|
||||||
example[param.identifier] = exampleConfig.value
|
|
||||||
})
|
|
||||||
|
|
||||||
return JSON.stringify(example, null, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理数据回显
|
|
||||||
* @param value 值字符串
|
|
||||||
*/
|
|
||||||
const handleDataDisplay = (value: string) => {
|
|
||||||
if (!value || !value.trim()) {
|
|
||||||
paramsJson.value = ''
|
|
||||||
jsonError.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 尝试解析JSON,如果成功则格式化
|
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
paramsJson.value = JSON.stringify(parsed, null, 2)
|
|
||||||
jsonError.value = ''
|
|
||||||
} catch {
|
|
||||||
// 如果不是有效的JSON,直接使用原字符串
|
|
||||||
paramsJson.value = value
|
|
||||||
jsonError.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听外部值变化(编辑模式数据回显)
|
|
||||||
watch(
|
|
||||||
() => localValue.value,
|
|
||||||
async (newValue, oldValue) => {
|
|
||||||
// 避免循环更新
|
|
||||||
if (newValue === oldValue) return
|
|
||||||
|
|
||||||
// 使用 nextTick 确保在下一个 tick 中处理数据
|
|
||||||
await nextTick()
|
|
||||||
handleDataDisplay(newValue || '')
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
// 组件挂载后也尝试处理一次数据回显
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick()
|
|
||||||
if (localValue.value) {
|
|
||||||
handleDataDisplay(localValue.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听配置变化
|
|
||||||
watch(
|
|
||||||
() => props.config,
|
|
||||||
(newConfig, oldConfig) => {
|
|
||||||
// 只有在配置真正变化时才清空数据
|
|
||||||
if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
|
|
||||||
// 如果没有外部传入的值,才清空数据
|
|
||||||
if (!localValue.value) {
|
|
||||||
paramsJson.value = ''
|
|
||||||
jsonError.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 弹出层内容样式 */
|
/* 弹出层内容样式 */
|
||||||
.json-params-detail-content {
|
.json-params-detail-content {
|
||||||
|
|||||||
@@ -1,4 +1,152 @@
|
|||||||
<!-- 值输入组件 -->
|
<!-- 值输入组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** 值输入组件 */
|
||||||
|
defineOptions({ name: 'ValueInput' });
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue?: string;
|
||||||
|
propertyType?: string;
|
||||||
|
operator?: string;
|
||||||
|
propertyConfig?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localValue = useVModel(props, 'modelValue', emit, {
|
||||||
|
defaultValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const rangeStart = ref(''); // 范围开始值
|
||||||
|
const rangeEnd = ref(''); // 范围结束值
|
||||||
|
const dateValue = ref(''); // 日期值
|
||||||
|
const numberValue = ref<number>(); // 数字值
|
||||||
|
|
||||||
|
/** 计算属性:枚举选项 */
|
||||||
|
const enumOptions = computed(() => {
|
||||||
|
if (props.propertyConfig?.enum) {
|
||||||
|
return props.propertyConfig.enum.map((item: any) => ({
|
||||||
|
label: item.name || item.label || item.value,
|
||||||
|
value: item.value,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 计算属性:列表预览 */
|
||||||
|
const listPreview = computed(() => {
|
||||||
|
if (
|
||||||
|
props.operator ===
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value &&
|
||||||
|
localValue.value
|
||||||
|
) {
|
||||||
|
return localValue.value
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 判断是否为数字类型 */
|
||||||
|
const isNumericType = () => {
|
||||||
|
return [
|
||||||
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
].includes((props.propertyType || '') as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取输入框类型 */
|
||||||
|
const getInputType = () => {
|
||||||
|
switch (props.propertyType) {
|
||||||
|
case IoTDataSpecsDataTypeEnum.DOUBLE:
|
||||||
|
case IoTDataSpecsDataTypeEnum.FLOAT:
|
||||||
|
case IoTDataSpecsDataTypeEnum.INT: {
|
||||||
|
return 'number';
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取占位符文本 */
|
||||||
|
const getPlaceholder = () => {
|
||||||
|
const typeMap = {
|
||||||
|
[IoTDataSpecsDataTypeEnum.TEXT]: '请输入字符串',
|
||||||
|
[IoTDataSpecsDataTypeEnum.INT]: '请输入整数',
|
||||||
|
[IoTDataSpecsDataTypeEnum.FLOAT]: '请输入浮点数',
|
||||||
|
[IoTDataSpecsDataTypeEnum.DOUBLE]: '请输入双精度数',
|
||||||
|
[IoTDataSpecsDataTypeEnum.STRUCT]: '请输入 JSON 格式数据',
|
||||||
|
[IoTDataSpecsDataTypeEnum.ARRAY]: '请输入数组格式数据',
|
||||||
|
};
|
||||||
|
return typeMap[props.propertyType || ''] || '请输入值';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取数字精度 */
|
||||||
|
const getPrecision = () => {
|
||||||
|
return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 0 : 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取数字步长 */
|
||||||
|
const getStep = () => {
|
||||||
|
return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 1 : 0.1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取最小值 */
|
||||||
|
const getMin = () => {
|
||||||
|
return props.propertyConfig?.min || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取最大值 */
|
||||||
|
const getMax = () => {
|
||||||
|
return props.propertyConfig?.max || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理范围变化事件 */
|
||||||
|
const handleRangeChange = () => {
|
||||||
|
localValue.value =
|
||||||
|
rangeStart.value && rangeEnd.value
|
||||||
|
? `${rangeStart.value},${rangeEnd.value}`
|
||||||
|
: '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理日期变化事件 */
|
||||||
|
const handleDateChange = (value: string) => {
|
||||||
|
localValue.value = value || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理数字变化事件 */
|
||||||
|
const handleNumberChange = (value: number | undefined) => {
|
||||||
|
localValue.value = value?.toString() || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听操作符变化 */
|
||||||
|
watch(
|
||||||
|
() => props.operator,
|
||||||
|
() => {
|
||||||
|
localValue.value = '';
|
||||||
|
rangeStart.value = '';
|
||||||
|
rangeEnd.value = '';
|
||||||
|
dateValue.value = '';
|
||||||
|
numberValue.value = undefined;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full min-w-0">
|
<div class="w-full min-w-0">
|
||||||
<!-- 布尔值选择 -->
|
<!-- 布尔值选择 -->
|
||||||
@@ -14,7 +162,9 @@
|
|||||||
|
|
||||||
<!-- 枚举值选择 -->
|
<!-- 枚举值选择 -->
|
||||||
<el-select
|
<el-select
|
||||||
v-else-if="propertyType === IoTDataSpecsDataTypeEnum.ENUM && enumOptions.length > 0"
|
v-else-if="
|
||||||
|
propertyType === IoTDataSpecsDataTypeEnum.ENUM && enumOptions.length > 0
|
||||||
|
"
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
placeholder="请选择枚举值"
|
placeholder="请选择枚举值"
|
||||||
class="w-full!"
|
class="w-full!"
|
||||||
@@ -29,45 +179,64 @@
|
|||||||
|
|
||||||
<!-- 范围输入 (between 操作符) -->
|
<!-- 范围输入 (between 操作符) -->
|
||||||
<div
|
<div
|
||||||
v-else-if="operator === IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value"
|
v-else-if="
|
||||||
class="w-full! flex items-center gap-8px"
|
operator ===
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value
|
||||||
|
"
|
||||||
|
class="w-full! gap-8px flex items-center"
|
||||||
>
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="rangeStart"
|
v-model="rangeStart"
|
||||||
:type="getInputType()"
|
:type="getInputType()"
|
||||||
placeholder="最小值"
|
placeholder="最小值"
|
||||||
@input="handleRangeChange"
|
@input="handleRangeChange"
|
||||||
class="flex-1 min-w-0"
|
class="min-w-0 flex-1"
|
||||||
style="width: auto !important"
|
style="width: auto !important"
|
||||||
/>
|
/>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap">至</span>
|
<span
|
||||||
|
class="text-12px whitespace-nowrap text-[var(--el-text-color-secondary)]"
|
||||||
|
>至</span>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="rangeEnd"
|
v-model="rangeEnd"
|
||||||
:type="getInputType()"
|
:type="getInputType()"
|
||||||
placeholder="最大值"
|
placeholder="最大值"
|
||||||
@input="handleRangeChange"
|
@input="handleRangeChange"
|
||||||
class="flex-1 min-w-0"
|
class="min-w-0 flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 列表输入 (in 操作符) -->
|
<!-- 列表输入 (in 操作符) -->
|
||||||
<div
|
<div
|
||||||
v-else-if="operator === IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value"
|
v-else-if="
|
||||||
|
operator === IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value
|
||||||
|
"
|
||||||
|
class="w-full!"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="localValue"
|
||||||
|
placeholder="请输入值列表,用逗号分隔"
|
||||||
class="w-full!"
|
class="w-full!"
|
||||||
>
|
>
|
||||||
<el-input v-model="localValue" placeholder="请输入值列表,用逗号分隔" class="w-full!">
|
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-tooltip content="多个值用逗号分隔,如:1,2,3" placement="top">
|
<el-tooltip content="多个值用逗号分隔,如:1,2,3" placement="top">
|
||||||
<Icon
|
<Icon
|
||||||
icon="ep:question-filled"
|
icon="ep:question-filled"
|
||||||
class="text-[var(--el-text-color-placeholder)] cursor-help"
|
class="cursor-help text-[var(--el-text-color-placeholder)]"
|
||||||
/>
|
/>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
<div v-if="listPreview.length > 0" class="mt-8px flex items-center gap-6px flex-wrap">
|
<div
|
||||||
|
v-if="listPreview.length > 0"
|
||||||
|
class="mt-8px gap-6px flex flex-wrap items-center"
|
||||||
|
>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)]">解析结果:</span>
|
<span class="text-12px text-[var(--el-text-color-secondary)]">解析结果:</span>
|
||||||
<el-tag v-for="(item, index) in listPreview" :key="index" size="small" class="m-0">
|
<el-tag
|
||||||
|
v-for="(item, index) in listPreview"
|
||||||
|
:key="index"
|
||||||
|
size="small"
|
||||||
|
class="m-0"
|
||||||
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +281,7 @@
|
|||||||
:content="`单位:${propertyConfig.unit}`"
|
:content="`单位:${propertyConfig.unit}`"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] px-4px">
|
<span class="text-12px px-4px text-[var(--el-text-color-secondary)]">
|
||||||
{{ propertyConfig.unit }}
|
{{ propertyConfig.unit }}
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -120,147 +289,3 @@
|
|||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import {
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 值输入组件 */
|
|
||||||
defineOptions({ name: 'ValueInput' })
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
modelValue?: string
|
|
||||||
propertyType?: string
|
|
||||||
operator?: string
|
|
||||||
propertyConfig?: any
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emits {
|
|
||||||
(e: 'update:modelValue', value: string): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
const emit = defineEmits<Emits>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit, {
|
|
||||||
defaultValue: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const rangeStart = ref('') // 范围开始值
|
|
||||||
const rangeEnd = ref('') // 范围结束值
|
|
||||||
const dateValue = ref('') // 日期值
|
|
||||||
const numberValue = ref<number>() // 数字值
|
|
||||||
|
|
||||||
/** 计算属性:枚举选项 */
|
|
||||||
const enumOptions = computed(() => {
|
|
||||||
if (props.propertyConfig?.enum) {
|
|
||||||
return props.propertyConfig.enum.map((item: any) => ({
|
|
||||||
label: item.name || item.label || item.value,
|
|
||||||
value: item.value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 计算属性:列表预览 */
|
|
||||||
const listPreview = computed(() => {
|
|
||||||
if (
|
|
||||||
props.operator === IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value &&
|
|
||||||
localValue.value
|
|
||||||
) {
|
|
||||||
return localValue.value
|
|
||||||
.split(',')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter((item) => item)
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 判断是否为数字类型 */
|
|
||||||
const isNumericType = () => {
|
|
||||||
return [
|
|
||||||
IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE
|
|
||||||
].includes((props.propertyType || '') as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取输入框类型 */
|
|
||||||
const getInputType = () => {
|
|
||||||
switch (props.propertyType) {
|
|
||||||
case IoTDataSpecsDataTypeEnum.INT:
|
|
||||||
case IoTDataSpecsDataTypeEnum.FLOAT:
|
|
||||||
case IoTDataSpecsDataTypeEnum.DOUBLE:
|
|
||||||
return 'number'
|
|
||||||
default:
|
|
||||||
return 'text'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取占位符文本 */
|
|
||||||
const getPlaceholder = () => {
|
|
||||||
const typeMap = {
|
|
||||||
[IoTDataSpecsDataTypeEnum.TEXT]: '请输入字符串',
|
|
||||||
[IoTDataSpecsDataTypeEnum.INT]: '请输入整数',
|
|
||||||
[IoTDataSpecsDataTypeEnum.FLOAT]: '请输入浮点数',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DOUBLE]: '请输入双精度数',
|
|
||||||
[IoTDataSpecsDataTypeEnum.STRUCT]: '请输入 JSON 格式数据',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ARRAY]: '请输入数组格式数据'
|
|
||||||
}
|
|
||||||
return typeMap[props.propertyType || ''] || '请输入值'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取数字精度 */
|
|
||||||
const getPrecision = () => {
|
|
||||||
return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 0 : 2
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取数字步长 */
|
|
||||||
const getStep = () => {
|
|
||||||
return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 1 : 0.1
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取最小值 */
|
|
||||||
const getMin = () => {
|
|
||||||
return props.propertyConfig?.min || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取最大值 */
|
|
||||||
const getMax = () => {
|
|
||||||
return props.propertyConfig?.max || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理范围变化事件 */
|
|
||||||
const handleRangeChange = () => {
|
|
||||||
if (rangeStart.value && rangeEnd.value) {
|
|
||||||
localValue.value = `${rangeStart.value},${rangeEnd.value}`
|
|
||||||
} else {
|
|
||||||
localValue.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理日期变化事件 */
|
|
||||||
const handleDateChange = (value: string) => {
|
|
||||||
localValue.value = value || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理数字变化事件 */
|
|
||||||
const handleNumberChange = (value: number | undefined) => {
|
|
||||||
localValue.value = value?.toString() || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听操作符变化 */
|
|
||||||
watch(
|
|
||||||
() => props.operator,
|
|
||||||
() => {
|
|
||||||
localValue.value = ''
|
|
||||||
rangeStart.value = ''
|
|
||||||
rangeEnd.value = ''
|
|
||||||
dateValue.value = ''
|
|
||||||
numberValue.value = undefined
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,14 +1,168 @@
|
|||||||
<!-- 执行器配置组件 -->
|
<!-- 执行器配置组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Action } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getActionTypeLabel,
|
||||||
|
getActionTypeOptions,
|
||||||
|
IotRuleSceneActionTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import AlertConfig from '../configs/AlertConfig.vue';
|
||||||
|
import DeviceControlConfig from '../configs/DeviceControlConfig.vue';
|
||||||
|
|
||||||
|
/** 执行器配置组件 */
|
||||||
|
defineOptions({ name: 'ActionSection' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
actions: Action[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:actions', value: Action[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const actions = useVModel(props, 'actions', emit);
|
||||||
|
|
||||||
|
/** 获取执行器标签类型(用于 el-tag 的 type 属性) */
|
||||||
|
const getActionTypeTag = (
|
||||||
|
type: number,
|
||||||
|
): 'danger' | 'info' | 'primary' | 'success' | 'warning' => {
|
||||||
|
const actionTypeTags = {
|
||||||
|
[IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
|
||||||
|
[IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
|
||||||
|
[IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger',
|
||||||
|
[IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning',
|
||||||
|
} as const;
|
||||||
|
return actionTypeTags[type] || 'info';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 判断是否为设备执行器类型 */
|
||||||
|
const isDeviceAction = (type: number): boolean => {
|
||||||
|
const deviceActionTypes = [
|
||||||
|
IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
|
||||||
|
IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
|
||||||
|
] as number[];
|
||||||
|
return deviceActionTypes.includes(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 判断是否为告警执行器类型 */
|
||||||
|
const isAlertAction = (type: number): boolean => {
|
||||||
|
const alertActionTypes = [
|
||||||
|
IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
|
||||||
|
IotRuleSceneActionTypeEnum.ALERT_RECOVER,
|
||||||
|
] as number[];
|
||||||
|
return alertActionTypes.includes(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认的执行器数据
|
||||||
|
* @returns 默认执行器对象
|
||||||
|
*/
|
||||||
|
const createDefaultActionData = (): Action => {
|
||||||
|
return {
|
||||||
|
type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
|
||||||
|
productId: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
identifier: undefined, // 物模型标识符(服务调用时使用)
|
||||||
|
params: undefined,
|
||||||
|
alertConfigId: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加执行器
|
||||||
|
*/
|
||||||
|
const addAction = () => {
|
||||||
|
const newAction = createDefaultActionData();
|
||||||
|
actions.value.push(newAction);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除执行器
|
||||||
|
* @param index 执行器索引
|
||||||
|
*/
|
||||||
|
const removeAction = (index: number) => {
|
||||||
|
actions.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新执行器类型
|
||||||
|
* @param index 执行器索引
|
||||||
|
* @param type 执行器类型
|
||||||
|
*/
|
||||||
|
const updateActionType = (index: number, type: number) => {
|
||||||
|
actions.value[index].type = type;
|
||||||
|
onActionTypeChange(actions.value[index], type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新执行器
|
||||||
|
* @param index 执行器索引
|
||||||
|
* @param action 执行器对象
|
||||||
|
*/
|
||||||
|
const updateAction = (index: number, action: Action) => {
|
||||||
|
actions.value[index] = action;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新告警配置
|
||||||
|
* @param index 执行器索引
|
||||||
|
* @param alertConfigId 告警配置ID
|
||||||
|
*/
|
||||||
|
const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
|
||||||
|
actions.value[index].alertConfigId = alertConfigId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听执行器类型变化
|
||||||
|
* @param action 执行器对象
|
||||||
|
* @param type 执行器类型
|
||||||
|
*/
|
||||||
|
const onActionTypeChange = (action: Action, type: number) => {
|
||||||
|
// 清理不相关的配置,确保数据结构干净
|
||||||
|
if (isDeviceAction(type)) {
|
||||||
|
// 设备控制类型:清理告警配置,确保设备参数存在
|
||||||
|
action.alertConfigId = undefined;
|
||||||
|
if (!action.params) {
|
||||||
|
action.params = '';
|
||||||
|
}
|
||||||
|
// 如果从其他类型切换到设备控制类型,清空identifier(让用户重新选择)
|
||||||
|
if (action.identifier && type !== action.type) {
|
||||||
|
action.identifier = undefined;
|
||||||
|
}
|
||||||
|
} else if (isAlertAction(type)) {
|
||||||
|
action.productId = undefined;
|
||||||
|
action.deviceId = undefined;
|
||||||
|
action.identifier = undefined; // 清理服务标识符
|
||||||
|
action.params = undefined;
|
||||||
|
action.alertConfigId = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
|
<el-card
|
||||||
|
class="rounded-8px border border-[var(--el-border-color-light)]"
|
||||||
|
shadow="never"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<Icon icon="ep:setting" class="text-[var(--el-color-primary)] text-18px" />
|
<Icon
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">执行器配置</span>
|
icon="ep:setting"
|
||||||
<el-tag size="small" type="info">{{ actions.length }} 个执行器</el-tag>
|
class="text-18px text-[var(--el-color-primary)]"
|
||||||
|
/>
|
||||||
|
<span class="text-16px font-600 text-[var(--el-text-color-primary)]"
|
||||||
|
>执行器配置</span
|
||||||
|
>
|
||||||
|
<el-tag size="small" type="info">
|
||||||
|
{{ actions.length }} 个执行器
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<el-button type="primary" size="small" @click="addAction">
|
<el-button type="primary" size="small" @click="addAction">
|
||||||
<Icon icon="ep:plus" />
|
<Icon icon="ep:plus" />
|
||||||
添加执行器
|
添加执行器
|
||||||
@@ -33,26 +187,32 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(action, index) in actions"
|
v-for="(action, index) in actions"
|
||||||
:key="`action-${index}`"
|
:key="`action-${index}`"
|
||||||
class="border-2 border-blue-200 rounded-8px bg-blue-50 shadow-sm hover:shadow-md transition-shadow"
|
class="rounded-8px border-2 border-blue-200 bg-blue-50 shadow-sm transition-shadow hover:shadow-md"
|
||||||
>
|
>
|
||||||
<!-- 执行器头部 - 蓝色主题 -->
|
<!-- 执行器头部 - 蓝色主题 -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-blue-50 to-sky-50 border-b border-blue-200 rounded-t-6px"
|
class="p-16px rounded-t-6px flex items-center justify-between border-b border-blue-200 bg-gradient-to-r from-blue-50 to-sky-50"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-12px">
|
<div class="gap-12px flex items-center">
|
||||||
<div class="flex items-center gap-8px text-16px font-600 text-blue-700">
|
|
||||||
<div
|
<div
|
||||||
class="w-24px h-24px bg-blue-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
class="gap-8px text-16px font-600 flex items-center text-blue-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-24px h-24px text-12px flex items-center justify-center rounded-full bg-blue-500 font-bold text-white"
|
||||||
>
|
>
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</div>
|
</div>
|
||||||
<span>执行器 {{ index + 1 }}</span>
|
<span>执行器 {{ index + 1 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag :type="getActionTypeTag(action.type)" size="small" class="font-500">
|
<el-tag
|
||||||
|
:type="getActionTypeTag(action.type)"
|
||||||
|
size="small"
|
||||||
|
class="font-500"
|
||||||
|
>
|
||||||
{{ getActionTypeLabel(action.type) }}
|
{{ getActionTypeLabel(action.type) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="actions.length > 1"
|
v-if="actions.length > 1"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -74,7 +234,9 @@
|
|||||||
<el-form-item label="执行类型" required>
|
<el-form-item label="执行类型" required>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="action.type"
|
:model-value="action.type"
|
||||||
@update:model-value="(value) => updateActionType(index, value)"
|
@update:model-value="
|
||||||
|
(value) => updateActionType(index, value)
|
||||||
|
"
|
||||||
@change="(value) => onActionTypeChange(action, value)"
|
@change="(value) => onActionTypeChange(action, value)"
|
||||||
placeholder="请选择执行类型"
|
placeholder="请选择执行类型"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -100,21 +262,32 @@
|
|||||||
<AlertConfig
|
<AlertConfig
|
||||||
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
|
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
|
||||||
:model-value="action.alertConfigId"
|
:model-value="action.alertConfigId"
|
||||||
@update:model-value="(value) => updateActionAlertConfig(index, value)"
|
@update:model-value="
|
||||||
|
(value) => updateActionAlertConfig(index, value)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 触发告警提示 - 触发告警时显示 -->
|
<!-- 触发告警提示 - 触发告警时显示 -->
|
||||||
<div
|
<div
|
||||||
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER"
|
v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER"
|
||||||
class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
|
class="rounded-6px p-16px border border-[var(--el-border-color-light)] bg-[var(--el-fill-color-blank)]"
|
||||||
|
>
|
||||||
|
<div class="gap-8px mb-8px flex items-center">
|
||||||
|
<Icon
|
||||||
|
icon="ep:warning"
|
||||||
|
class="text-16px text-[var(--el-color-warning)]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-14px font-600 text-[var(--el-text-color-primary)]"
|
||||||
|
>触发告警</span
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-8px mb-8px">
|
|
||||||
<Icon icon="ep:warning" class="text-[var(--el-color-warning)] text-16px" />
|
|
||||||
<span class="text-14px font-600 text-[var(--el-text-color-primary)]">触发告警</span>
|
|
||||||
<el-tag size="small" type="warning">自动执行</el-tag>
|
<el-tag size="small" type="warning">自动执行</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
|
<div
|
||||||
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 -> 告警配置] 管理。
|
class="text-12px leading-relaxed text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
|
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 ->
|
||||||
|
告警配置] 管理。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +295,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 添加提示 -->
|
<!-- 添加提示 -->
|
||||||
<div v-if="actions.length > 0" class="text-center py-16px">
|
<div v-if="actions.length > 0" class="py-16px text-center">
|
||||||
<el-button type="primary" plain @click="addAction">
|
<el-button type="primary" plain @click="addAction">
|
||||||
<Icon icon="ep:plus" />
|
<Icon icon="ep:plus" />
|
||||||
继续添加执行器
|
继续添加执行器
|
||||||
@@ -131,142 +304,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
|
|
||||||
import AlertConfig from '../configs/AlertConfig.vue'
|
|
||||||
import type { Action } from '#/api/iot/rule/scene'
|
|
||||||
import {
|
|
||||||
getActionTypeLabel,
|
|
||||||
getActionTypeOptions,
|
|
||||||
IotRuleSceneActionTypeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 执行器配置组件 */
|
|
||||||
defineOptions({ name: 'ActionSection' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
actions: Action[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:actions', value: Action[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const actions = useVModel(props, 'actions', emit)
|
|
||||||
|
|
||||||
/** 获取执行器标签类型(用于 el-tag 的 type 属性) */
|
|
||||||
const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
|
|
||||||
const actionTypeTags = {
|
|
||||||
[IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
|
|
||||||
[IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
|
|
||||||
[IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger',
|
|
||||||
[IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning'
|
|
||||||
} as const
|
|
||||||
return actionTypeTags[type] || 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 判断是否为设备执行器类型 */
|
|
||||||
const isDeviceAction = (type: number): boolean => {
|
|
||||||
const deviceActionTypes = [
|
|
||||||
IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
|
|
||||||
IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
|
|
||||||
] as number[]
|
|
||||||
return deviceActionTypes.includes(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 判断是否为告警执行器类型 */
|
|
||||||
const isAlertAction = (type: number): boolean => {
|
|
||||||
const alertActionTypes = [
|
|
||||||
IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
|
|
||||||
IotRuleSceneActionTypeEnum.ALERT_RECOVER
|
|
||||||
] as number[]
|
|
||||||
return alertActionTypes.includes(type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建默认的执行器数据
|
|
||||||
* @returns 默认执行器对象
|
|
||||||
*/
|
|
||||||
const createDefaultActionData = (): Action => {
|
|
||||||
return {
|
|
||||||
type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, // 默认为设备属性设置
|
|
||||||
productId: undefined,
|
|
||||||
deviceId: undefined,
|
|
||||||
identifier: undefined, // 物模型标识符(服务调用时使用)
|
|
||||||
params: undefined,
|
|
||||||
alertConfigId: undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加执行器
|
|
||||||
*/
|
|
||||||
const addAction = () => {
|
|
||||||
const newAction = createDefaultActionData()
|
|
||||||
actions.value.push(newAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除执行器
|
|
||||||
* @param index 执行器索引
|
|
||||||
*/
|
|
||||||
const removeAction = (index: number) => {
|
|
||||||
actions.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新执行器类型
|
|
||||||
* @param index 执行器索引
|
|
||||||
* @param type 执行器类型
|
|
||||||
*/
|
|
||||||
const updateActionType = (index: number, type: number) => {
|
|
||||||
actions.value[index].type = type
|
|
||||||
onActionTypeChange(actions.value[index], type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新执行器
|
|
||||||
* @param index 执行器索引
|
|
||||||
* @param action 执行器对象
|
|
||||||
*/
|
|
||||||
const updateAction = (index: number, action: Action) => {
|
|
||||||
actions.value[index] = action
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新告警配置
|
|
||||||
* @param index 执行器索引
|
|
||||||
* @param alertConfigId 告警配置ID
|
|
||||||
*/
|
|
||||||
const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
|
|
||||||
actions.value[index].alertConfigId = alertConfigId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听执行器类型变化
|
|
||||||
* @param action 执行器对象
|
|
||||||
* @param type 执行器类型
|
|
||||||
*/
|
|
||||||
const onActionTypeChange = (action: Action, type: number) => {
|
|
||||||
// 清理不相关的配置,确保数据结构干净
|
|
||||||
if (isDeviceAction(type)) {
|
|
||||||
// 设备控制类型:清理告警配置,确保设备参数存在
|
|
||||||
action.alertConfigId = undefined
|
|
||||||
if (!action.params) {
|
|
||||||
action.params = ''
|
|
||||||
}
|
|
||||||
// 如果从其他类型切换到设备控制类型,清空identifier(让用户重新选择)
|
|
||||||
if (action.identifier && type !== action.type) {
|
|
||||||
action.identifier = undefined
|
|
||||||
}
|
|
||||||
} else if (isAlertAction(type)) {
|
|
||||||
action.productId = undefined
|
|
||||||
action.deviceId = undefined
|
|
||||||
action.identifier = undefined // 清理服务标识符
|
|
||||||
action.params = undefined
|
|
||||||
action.alertConfigId = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,13 +1,41 @@
|
|||||||
<!-- 基础信息配置组件 -->
|
<!-- 基础信息配置组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IotSceneRule } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@vben/constants';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
/** 基础信息配置组件 */
|
||||||
|
defineOptions({ name: 'BasicInfoSection' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: IotSceneRule;
|
||||||
|
rules?: any;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: IotSceneRule): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const formData = useVModel(props, 'modelValue', emit); // 表单数据
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
|
<el-card
|
||||||
|
class="rounded-8px mb-10px border border-[var(--el-border-color-light)]"
|
||||||
|
shadow="never"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<Icon icon="ep:info-filled" class="text-[var(--el-color-primary)] text-18px" />
|
<Icon
|
||||||
|
icon="ep:info-filled"
|
||||||
|
class="text-18px text-[var(--el-color-primary)]"
|
||||||
|
/>
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
|
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">基础信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
|
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,26 +83,6 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { DICT_TYPE, getIntDictOptions } from '@vben/constants'
|
|
||||||
import type { IotSceneRule } from '#/api/iot/rule/scene'
|
|
||||||
|
|
||||||
/** 基础信息配置组件 */
|
|
||||||
defineOptions({ name: 'BasicInfoSection' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue: IotSceneRule
|
|
||||||
rules?: any
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: IotSceneRule): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const formData = useVModel(props, 'modelValue', emit) // 表单数据
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|||||||
@@ -1,11 +1,135 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Trigger } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
|
import { Crontab } from '@/components/Crontab';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTriggerTypeLabel,
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
isDeviceTrigger,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue';
|
||||||
|
|
||||||
|
/** 触发器配置组件 */
|
||||||
|
defineOptions({ name: 'TriggerSection' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
triggers: Trigger[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:triggers', value: Trigger[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const triggers = useVModel(props, 'triggers', emit);
|
||||||
|
|
||||||
|
/** 获取触发器标签类型(用于 el-tag 的 type 属性) */
|
||||||
|
const getTriggerTagType = (
|
||||||
|
type: number,
|
||||||
|
): 'danger' | 'info' | 'primary' | 'success' | 'warning' => {
|
||||||
|
if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
return isDeviceTrigger(type) ? 'success' : 'info';
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 添加触发器 */
|
||||||
|
const addTrigger = () => {
|
||||||
|
const newTrigger: Trigger = {
|
||||||
|
type: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
|
||||||
|
productId: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
identifier: undefined,
|
||||||
|
operator: undefined,
|
||||||
|
value: undefined,
|
||||||
|
cronExpression: undefined,
|
||||||
|
conditionGroups: [], // 空的条件组数组
|
||||||
|
};
|
||||||
|
triggers.value.push(newTrigger);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除触发器
|
||||||
|
* @param index 触发器索引
|
||||||
|
*/
|
||||||
|
const removeTrigger = (index: number) => {
|
||||||
|
if (triggers.value.length > 1) {
|
||||||
|
triggers.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新触发器类型
|
||||||
|
* @param index 触发器索引
|
||||||
|
* @param type 触发器类型
|
||||||
|
*/
|
||||||
|
const updateTriggerType = (index: number, type: number) => {
|
||||||
|
triggers.value[index].type = type;
|
||||||
|
onTriggerTypeChange(index, type);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新触发器设备配置
|
||||||
|
* @param index 触发器索引
|
||||||
|
* @param newTrigger 新的触发器对象
|
||||||
|
*/
|
||||||
|
const updateTriggerDeviceConfig = (index: number, newTrigger: Trigger) => {
|
||||||
|
triggers.value[index] = newTrigger;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新触发器 CRON 配置
|
||||||
|
* @param index 触发器索引
|
||||||
|
* @param cronExpression CRON 表达式
|
||||||
|
*/
|
||||||
|
const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
|
||||||
|
triggers.value[index].cronExpression = cronExpression;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理触发器类型变化事件
|
||||||
|
* @param index 触发器索引
|
||||||
|
* @param _ 触发器类型(未使用)
|
||||||
|
*/
|
||||||
|
const onTriggerTypeChange = (index: number, _: number) => {
|
||||||
|
const triggerItem = triggers.value[index];
|
||||||
|
triggerItem.productId = undefined;
|
||||||
|
triggerItem.deviceId = undefined;
|
||||||
|
triggerItem.identifier = undefined;
|
||||||
|
triggerItem.operator = undefined;
|
||||||
|
triggerItem.value = undefined;
|
||||||
|
triggerItem.cronExpression = undefined;
|
||||||
|
triggerItem.conditionGroups = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化:确保至少有一个触发器 */
|
||||||
|
onMounted(() => {
|
||||||
|
if (triggers.value.length === 0) {
|
||||||
|
addTrigger();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
|
<el-card
|
||||||
|
class="rounded-8px mb-10px border border-[var(--el-border-color-light)]"
|
||||||
|
shadow="never"
|
||||||
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
|
<Icon
|
||||||
<span class="text-16px font-600 text-[var(--el-text-color-primary)]">触发器配置</span>
|
icon="ep:lightning"
|
||||||
<el-tag size="small" type="info">{{ triggers.length }} 个触发器</el-tag>
|
class="text-18px text-[var(--el-color-primary)]"
|
||||||
|
/>
|
||||||
|
<span class="text-16px font-600 text-[var(--el-text-color-primary)]"
|
||||||
|
>触发器配置</span
|
||||||
|
>
|
||||||
|
<el-tag size="small" type="info">
|
||||||
|
{{ triggers.length }} 个触发器
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" size="small" @click="addTrigger">
|
<el-button type="primary" size="small" @click="addTrigger">
|
||||||
<Icon icon="ep:plus" />
|
<Icon icon="ep:plus" />
|
||||||
@@ -20,26 +144,32 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(triggerItem, index) in triggers"
|
v-for="(triggerItem, index) in triggers"
|
||||||
:key="`trigger-${index}`"
|
:key="`trigger-${index}`"
|
||||||
class="border-2 border-green-200 rounded-8px bg-green-50 shadow-sm hover:shadow-md transition-shadow"
|
class="rounded-8px border-2 border-green-200 bg-green-50 shadow-sm transition-shadow hover:shadow-md"
|
||||||
>
|
>
|
||||||
<!-- 触发器头部 - 绿色主题 -->
|
<!-- 触发器头部 - 绿色主题 -->
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border-b border-green-200 rounded-t-6px"
|
class="p-16px rounded-t-6px flex items-center justify-between border-b border-green-200 bg-gradient-to-r from-green-50 to-emerald-50"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-12px">
|
<div class="gap-12px flex items-center">
|
||||||
<div class="flex items-center gap-8px text-16px font-600 text-green-700">
|
|
||||||
<div
|
<div
|
||||||
class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
|
class="gap-8px text-16px font-600 flex items-center text-green-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-24px h-24px text-12px flex items-center justify-center rounded-full bg-green-500 font-bold text-white"
|
||||||
>
|
>
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</div>
|
</div>
|
||||||
<span>触发器 {{ index + 1 }}</span>
|
<span>触发器 {{ index + 1 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-tag size="small" :type="getTriggerTagType(triggerItem.type)" class="font-500">
|
<el-tag
|
||||||
|
size="small"
|
||||||
|
:type="getTriggerTagType(triggerItem.type)"
|
||||||
|
class="font-500"
|
||||||
|
>
|
||||||
{{ getTriggerTypeLabel(triggerItem.type) }}
|
{{ getTriggerTypeLabel(triggerItem.type) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="triggers.length > 1"
|
v-if="triggers.length > 1"
|
||||||
type="danger"
|
type="danger"
|
||||||
@@ -61,32 +191,40 @@
|
|||||||
v-if="isDeviceTrigger(triggerItem.type)"
|
v-if="isDeviceTrigger(triggerItem.type)"
|
||||||
:model-value="triggerItem"
|
:model-value="triggerItem"
|
||||||
:index="index"
|
:index="index"
|
||||||
@update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
|
@update:model-value="
|
||||||
|
(value) => updateTriggerDeviceConfig(index, value)
|
||||||
|
"
|
||||||
@trigger-type-change="(type) => updateTriggerType(index, type)"
|
@trigger-type-change="(type) => updateTriggerType(index, type)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 定时触发配置 -->
|
<!-- 定时触发配置 -->
|
||||||
<div
|
<div
|
||||||
v-else-if="triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER"
|
v-else-if="triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER"
|
||||||
class="flex flex-col gap-16px"
|
class="gap-16px flex flex-col"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-8px p-12px px-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
|
class="gap-8px p-12px px-16px rounded-6px flex items-center border border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-light)]"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:timer" class="text-[var(--el-color-danger)] text-18px" />
|
<Icon
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]"
|
icon="ep:timer"
|
||||||
|
class="text-18px text-[var(--el-color-danger)]"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-14px font-500 text-[var(--el-text-color-primary)]"
|
||||||
>定时触发配置</span
|
>定时触发配置</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CRON 表达式配置 -->
|
<!-- CRON 表达式配置 -->
|
||||||
<div
|
<div
|
||||||
class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
|
class="p-16px rounded-6px border border-[var(--el-border-color-lighter)] bg-[var(--el-fill-color-blank)]"
|
||||||
>
|
>
|
||||||
<el-form-item label="CRON表达式" required>
|
<el-form-item label="CRON表达式" required>
|
||||||
<Crontab
|
<Crontab
|
||||||
:model-value="triggerItem.cronExpression || '0 0 12 * * ?'"
|
:model-value="triggerItem.cronExpression || '0 0 12 * * ?'"
|
||||||
@update:model-value="(value) => updateTriggerCronConfig(index, value)"
|
@update:model-value="
|
||||||
|
(value) => updateTriggerCronConfig(index, value)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +238,9 @@
|
|||||||
<el-empty description="暂无触发器">
|
<el-empty description="暂无触发器">
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="space-y-8px">
|
<div class="space-y-8px">
|
||||||
<p class="text-[var(--el-text-color-secondary)]">暂无触发器配置</p>
|
<p class="text-[var(--el-text-color-secondary)]">
|
||||||
|
暂无触发器配置
|
||||||
|
</p>
|
||||||
<p class="text-12px text-[var(--el-text-color-placeholder)]">
|
<p class="text-12px text-[var(--el-text-color-placeholder)]">
|
||||||
请使用上方的"添加触发器"按钮来设置触发规则
|
请使用上方的"添加触发器"按钮来设置触发规则
|
||||||
</p>
|
</p>
|
||||||
@@ -111,112 +251,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
|
|
||||||
import { Crontab } from '@/components/Crontab'
|
|
||||||
import type { Trigger } from '#/api/iot/rule/scene'
|
|
||||||
import {
|
|
||||||
getTriggerTypeLabel,
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
isDeviceTrigger
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 触发器配置组件 */
|
|
||||||
defineOptions({ name: 'TriggerSection' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
triggers: Trigger[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:triggers', value: Trigger[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const triggers = useVModel(props, 'triggers', emit)
|
|
||||||
|
|
||||||
/** 获取触发器标签类型(用于 el-tag 的 type 属性) */
|
|
||||||
const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
|
|
||||||
if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
|
||||||
return 'warning'
|
|
||||||
}
|
|
||||||
return isDeviceTrigger(type) ? 'success' : 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 添加触发器 */
|
|
||||||
const addTrigger = () => {
|
|
||||||
const newTrigger: Trigger = {
|
|
||||||
type: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
|
|
||||||
productId: undefined,
|
|
||||||
deviceId: undefined,
|
|
||||||
identifier: undefined,
|
|
||||||
operator: undefined,
|
|
||||||
value: undefined,
|
|
||||||
cronExpression: undefined,
|
|
||||||
conditionGroups: [] // 空的条件组数组
|
|
||||||
}
|
|
||||||
triggers.value.push(newTrigger)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除触发器
|
|
||||||
* @param index 触发器索引
|
|
||||||
*/
|
|
||||||
const removeTrigger = (index: number) => {
|
|
||||||
if (triggers.value.length > 1) {
|
|
||||||
triggers.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新触发器类型
|
|
||||||
* @param index 触发器索引
|
|
||||||
* @param type 触发器类型
|
|
||||||
*/
|
|
||||||
const updateTriggerType = (index: number, type: number) => {
|
|
||||||
triggers.value[index].type = type
|
|
||||||
onTriggerTypeChange(index, type)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新触发器设备配置
|
|
||||||
* @param index 触发器索引
|
|
||||||
* @param newTrigger 新的触发器对象
|
|
||||||
*/
|
|
||||||
const updateTriggerDeviceConfig = (index: number, newTrigger: Trigger) => {
|
|
||||||
triggers.value[index] = newTrigger
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新触发器 CRON 配置
|
|
||||||
* @param index 触发器索引
|
|
||||||
* @param cronExpression CRON 表达式
|
|
||||||
*/
|
|
||||||
const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
|
|
||||||
triggers.value[index].cronExpression = cronExpression
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理触发器类型变化事件
|
|
||||||
* @param index 触发器索引
|
|
||||||
* @param _ 触发器类型(未使用)
|
|
||||||
*/
|
|
||||||
const onTriggerTypeChange = (index: number, _: number) => {
|
|
||||||
const triggerItem = triggers.value[index]
|
|
||||||
triggerItem.productId = undefined
|
|
||||||
triggerItem.deviceId = undefined
|
|
||||||
triggerItem.identifier = undefined
|
|
||||||
triggerItem.operator = undefined
|
|
||||||
triggerItem.value = undefined
|
|
||||||
triggerItem.cronExpression = undefined
|
|
||||||
triggerItem.conditionGroups = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化:确保至少有一个触发器 */
|
|
||||||
onMounted(() => {
|
|
||||||
if (triggers.value.length === 0) {
|
|
||||||
addTrigger()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,4 +1,76 @@
|
|||||||
<!-- 设备选择器组件 -->
|
<!-- 设备选择器组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
|
||||||
|
import { DeviceApi } from '#/api/iot/device/device';
|
||||||
|
import { DEVICE_SELECTOR_OPTIONS } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** 设备选择器组件 */
|
||||||
|
defineOptions({ name: 'DeviceSelector' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: number;
|
||||||
|
productId?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value?: number): void;
|
||||||
|
(e: 'change', value?: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const deviceLoading = ref(false); // 设备加载状态
|
||||||
|
const deviceList = ref<any[]>([]); // 设备列表
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理选择变化事件
|
||||||
|
* @param value 选中的设备ID
|
||||||
|
*/
|
||||||
|
const handleChange = (value?: number) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
emit('change', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取设备列表
|
||||||
|
*/
|
||||||
|
const getDeviceList = async () => {
|
||||||
|
if (!props.productId) {
|
||||||
|
deviceList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
deviceLoading.value = true;
|
||||||
|
const res = await DeviceApi.getDeviceListByProductId(props.productId);
|
||||||
|
deviceList.value = res || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取设备列表失败:', error);
|
||||||
|
deviceList.value = [];
|
||||||
|
} finally {
|
||||||
|
deviceList.value.unshift(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES);
|
||||||
|
deviceLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听产品变化
|
||||||
|
watch(
|
||||||
|
() => props.productId,
|
||||||
|
(newProductId) => {
|
||||||
|
if (newProductId) {
|
||||||
|
getDeviceList();
|
||||||
|
} else {
|
||||||
|
deviceList.value = [];
|
||||||
|
// 清空当前选择的设备
|
||||||
|
if (props.modelValue) {
|
||||||
|
emit('update:modelValue', undefined);
|
||||||
|
emit('change', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
@@ -16,88 +88,21 @@
|
|||||||
:label="device.deviceName"
|
:label="device.deviceName"
|
||||||
:value="device.id"
|
:value="device.id"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="py-4px flex w-full items-center justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
|
<div
|
||||||
|
class="text-14px font-500 mb-2px text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
{{ device.deviceName }}
|
{{ device.deviceName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
|
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
||||||
|
{{ device.deviceKey }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-4px" v-if="device.id > 0">
|
</div>
|
||||||
|
<div class="gap-4px flex items-center" v-if="device.id > 0">
|
||||||
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
|
<dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { DeviceApi } from '#/api/iot/device/device'
|
|
||||||
import { DEVICE_SELECTOR_OPTIONS } from '#/views/iot/utils/constants'
|
|
||||||
import { DICT_TYPE } from '@vben/constants'
|
|
||||||
|
|
||||||
/** 设备选择器组件 */
|
|
||||||
defineOptions({ name: 'DeviceSelector' })
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue?: number
|
|
||||||
productId?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value?: number): void
|
|
||||||
(e: 'change', value?: number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const deviceLoading = ref(false) // 设备加载状态
|
|
||||||
const deviceList = ref<any[]>([]) // 设备列表
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理选择变化事件
|
|
||||||
* @param value 选中的设备ID
|
|
||||||
*/
|
|
||||||
const handleChange = (value?: number) => {
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
emit('change', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取设备列表
|
|
||||||
*/
|
|
||||||
const getDeviceList = async () => {
|
|
||||||
if (!props.productId) {
|
|
||||||
deviceList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
deviceLoading.value = true
|
|
||||||
const res = await DeviceApi.getDeviceListByProductId(props.productId)
|
|
||||||
deviceList.value = res || []
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取设备列表失败:', error)
|
|
||||||
deviceList.value = []
|
|
||||||
} finally {
|
|
||||||
deviceList.value.unshift(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES)
|
|
||||||
deviceLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听产品变化
|
|
||||||
watch(
|
|
||||||
() => props.productId,
|
|
||||||
(newProductId) => {
|
|
||||||
if (newProductId) {
|
|
||||||
getDeviceList()
|
|
||||||
} else {
|
|
||||||
deviceList.value = []
|
|
||||||
// 清空当前选择的设备
|
|
||||||
if (props.modelValue) {
|
|
||||||
emit('update:modelValue', undefined)
|
|
||||||
emit('change', undefined)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,59 +1,26 @@
|
|||||||
<!-- 操作符选择器组件 -->
|
<!-- 操作符选择器组件 -->
|
||||||
<template>
|
|
||||||
<div class="w-full">
|
|
||||||
<el-select
|
|
||||||
v-model="localValue"
|
|
||||||
placeholder="请选择操作符"
|
|
||||||
@change="handleChange"
|
|
||||||
class="w-full"
|
|
||||||
>
|
|
||||||
<el-option
|
|
||||||
v-for="operator in availableOperators"
|
|
||||||
:key="operator.value"
|
|
||||||
:label="operator.label"
|
|
||||||
:value="operator.value"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
|
||||||
<div class="flex items-center gap-8px">
|
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
|
||||||
{{ operator.label }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono"
|
|
||||||
>
|
|
||||||
{{ operator.symbol }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
|
||||||
{{ operator.description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
IoTDataSpecsDataTypeEnum
|
} from '#/views/iot/utils/constants';
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 操作符选择器组件 */
|
/** 操作符选择器组件 */
|
||||||
defineOptions({ name: 'OperatorSelector' })
|
defineOptions({ name: 'OperatorSelector' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: string
|
modelValue?: string;
|
||||||
propertyType?: string
|
propertyType?: string;
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', value: string): void;
|
||||||
(e: 'change', value: string): void
|
(e: 'change', value: string): void;
|
||||||
}>()
|
}>();
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
const localValue = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
// 基于枚举的操作符定义
|
// 基于枚举的操作符定义
|
||||||
const allOperators = [
|
const allOperators = [
|
||||||
@@ -69,8 +36,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.TEXT,
|
IoTDataSpecsDataTypeEnum.TEXT,
|
||||||
IoTDataSpecsDataTypeEnum.BOOL,
|
IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
|
||||||
@@ -84,8 +51,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.TEXT,
|
IoTDataSpecsDataTypeEnum.TEXT,
|
||||||
IoTDataSpecsDataTypeEnum.BOOL,
|
IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.value,
|
||||||
@@ -97,12 +64,16 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.value,
|
value:
|
||||||
label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.name,
|
IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS
|
||||||
|
.value,
|
||||||
|
label:
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS
|
||||||
|
.name,
|
||||||
symbol: '≥',
|
symbol: '≥',
|
||||||
description: '值大于或等于指定值时触发',
|
description: '值大于或等于指定值时触发',
|
||||||
example: 'humidity >= 80',
|
example: 'humidity >= 80',
|
||||||
@@ -110,8 +81,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.value,
|
||||||
@@ -123,12 +94,16 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.value,
|
value:
|
||||||
label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.name,
|
IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS
|
||||||
|
.value,
|
||||||
|
label:
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS
|
||||||
|
.name,
|
||||||
symbol: '≤',
|
symbol: '≤',
|
||||||
description: '值小于或等于指定值时触发',
|
description: '值小于或等于指定值时触发',
|
||||||
example: 'battery <= 20',
|
example: 'battery <= 20',
|
||||||
@@ -136,8 +111,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value,
|
||||||
@@ -149,8 +124,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.TEXT,
|
IoTDataSpecsDataTypeEnum.TEXT,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.value,
|
||||||
@@ -162,8 +137,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.TEXT,
|
IoTDataSpecsDataTypeEnum.TEXT,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value,
|
||||||
@@ -175,8 +150,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.value,
|
||||||
@@ -188,8 +163,8 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.value,
|
||||||
@@ -197,7 +172,7 @@ const allOperators = [
|
|||||||
symbol: '≈',
|
symbol: '≈',
|
||||||
description: '字符串匹配指定模式时触发',
|
description: '字符串匹配指定模式时触发',
|
||||||
example: 'message like "%error%"',
|
example: 'message like "%error%"',
|
||||||
supportedTypes: [IoTDataSpecsDataTypeEnum.TEXT]
|
supportedTypes: [IoTDataSpecsDataTypeEnum.TEXT],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value,
|
value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value,
|
||||||
@@ -212,33 +187,33 @@ const allOperators = [
|
|||||||
IoTDataSpecsDataTypeEnum.TEXT,
|
IoTDataSpecsDataTypeEnum.TEXT,
|
||||||
IoTDataSpecsDataTypeEnum.BOOL,
|
IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM,
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
// 计算属性:可用的操作符
|
// 计算属性:可用的操作符
|
||||||
const availableOperators = computed(() => {
|
const availableOperators = computed(() => {
|
||||||
if (!props.propertyType) {
|
if (!props.propertyType) {
|
||||||
return allOperators
|
return allOperators;
|
||||||
}
|
}
|
||||||
return allOperators.filter((op) =>
|
return allOperators.filter((op) =>
|
||||||
(op.supportedTypes as any[]).includes(props.propertyType || '')
|
(op.supportedTypes as any[]).includes(props.propertyType || ''),
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
// 计算属性:当前选中的操作符
|
// 计算属性:当前选中的操作符
|
||||||
const selectedOperator = computed(() => {
|
const selectedOperator = computed(() => {
|
||||||
return allOperators.find((op) => op.value === localValue.value)
|
return allOperators.find((op) => op.value === localValue.value);
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理选择变化事件
|
* 处理选择变化事件
|
||||||
* @param value 选中的操作符值
|
* @param value 选中的操作符值
|
||||||
*/
|
*/
|
||||||
const handleChange = (value: string) => {
|
const handleChange = (value: string) => {
|
||||||
emit('change', value)
|
emit('change', value);
|
||||||
}
|
};
|
||||||
|
|
||||||
/** 监听属性类型变化 */
|
/** 监听属性类型变化 */
|
||||||
watch(
|
watch(
|
||||||
@@ -248,14 +223,50 @@ watch(
|
|||||||
if (
|
if (
|
||||||
localValue.value &&
|
localValue.value &&
|
||||||
selectedOperator.value &&
|
selectedOperator.value &&
|
||||||
!(selectedOperator.value.supportedTypes as any[]).includes(props.propertyType || '')
|
!(selectedOperator.value.supportedTypes as any[]).includes(
|
||||||
|
props.propertyType || '',
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
localValue.value = ''
|
localValue.value = '';
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<el-select
|
||||||
|
v-model="localValue"
|
||||||
|
placeholder="请选择操作符"
|
||||||
|
@change="handleChange"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="operator in availableOperators"
|
||||||
|
:key="operator.value"
|
||||||
|
:label="operator.label"
|
||||||
|
:value="operator.value"
|
||||||
|
>
|
||||||
|
<div class="py-4px flex w-full items-center justify-between">
|
||||||
|
<div class="gap-8px flex items-center">
|
||||||
|
<div class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
|
{{ operator.label }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-12px px-6px py-2px rounded-4px bg-[var(--el-color-primary-light-9)] font-mono text-[var(--el-color-primary)]"
|
||||||
|
>
|
||||||
|
{{ operator.symbol }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
||||||
|
{{ operator.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.el-select-dropdown__item) {
|
:deep(.el-select-dropdown__item) {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|||||||
@@ -1,4 +1,53 @@
|
|||||||
<!-- 产品选择器组件 -->
|
<!-- 产品选择器组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
|
||||||
|
import { ProductApi } from '#/api/iot/product/product';
|
||||||
|
|
||||||
|
/** 产品选择器组件 */
|
||||||
|
defineOptions({ name: 'ProductSelector' });
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value?: number): void;
|
||||||
|
(e: 'change', value?: number): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const productLoading = ref(false); // 产品加载状态
|
||||||
|
const productList = ref<any[]>([]); // 产品列表
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理选择变化事件
|
||||||
|
* @param value 选中的产品 ID
|
||||||
|
*/
|
||||||
|
const handleChange = (value?: number) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
emit('change', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 获取产品列表 */
|
||||||
|
const getProductList = async () => {
|
||||||
|
try {
|
||||||
|
productLoading.value = true;
|
||||||
|
const res = await ProductApi.getSimpleProductList();
|
||||||
|
productList.value = res || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取产品列表失败:', error);
|
||||||
|
productList.value = [];
|
||||||
|
} finally {
|
||||||
|
productLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时获取产品列表
|
||||||
|
onMounted(() => {
|
||||||
|
getProductList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-select
|
<el-select
|
||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
@@ -15,9 +64,11 @@
|
|||||||
:label="product.name"
|
:label="product.name"
|
||||||
:value="product.id"
|
:value="product.id"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-4px">
|
<div class="py-4px flex w-full items-center justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
|
<div
|
||||||
|
class="text-14px font-500 mb-2px text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
{{ product.name }}
|
{{ product.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
<div class="text-12px text-[var(--el-text-color-secondary)]">
|
||||||
@@ -29,51 +80,3 @@
|
|||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ProductApi } from '#/api/iot/product/product'
|
|
||||||
import { DICT_TYPE } from '@vben/constants'
|
|
||||||
|
|
||||||
/** 产品选择器组件 */
|
|
||||||
defineOptions({ name: 'ProductSelector' })
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
modelValue?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value?: number): void
|
|
||||||
(e: 'change', value?: number): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const productLoading = ref(false) // 产品加载状态
|
|
||||||
const productList = ref<any[]>([]) // 产品列表
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理选择变化事件
|
|
||||||
* @param value 选中的产品 ID
|
|
||||||
*/
|
|
||||||
const handleChange = (value?: number) => {
|
|
||||||
emit('update:modelValue', value)
|
|
||||||
emit('change', value)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 获取产品列表 */
|
|
||||||
const getProductList = async () => {
|
|
||||||
try {
|
|
||||||
productLoading.value = true
|
|
||||||
const res = await ProductApi.getSimpleProductList()
|
|
||||||
productList.value = res || []
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取产品列表失败:', error)
|
|
||||||
productList.value = []
|
|
||||||
} finally {
|
|
||||||
productLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时获取产品列表
|
|
||||||
onMounted(() => {
|
|
||||||
getProductList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,6 +1,279 @@
|
|||||||
<!-- 属性选择器组件 -->
|
<!-- 属性选择器组件 -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type {
|
||||||
|
IotThingModelTSLResp,
|
||||||
|
ThingModelEvent,
|
||||||
|
ThingModelParam,
|
||||||
|
ThingModelProperty,
|
||||||
|
ThingModelService,
|
||||||
|
} from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
import {
|
||||||
|
getAccessModeLabel,
|
||||||
|
getDataTypeName,
|
||||||
|
getDataTypeTagType,
|
||||||
|
getEventTypeLabel,
|
||||||
|
getThingModelServiceCallTypeLabel,
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
THING_MODEL_GROUP_LABELS,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** 属性选择器组件 */
|
||||||
|
defineOptions({ name: 'PropertySelector' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
deviceId?: number;
|
||||||
|
modelValue?: string;
|
||||||
|
productId?: number;
|
||||||
|
triggerType: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string): void;
|
||||||
|
(e: 'change', value: { config: any; type: string }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 属性选择器内部使用的统一数据结构 */
|
||||||
|
interface PropertySelectorItem {
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
dataType: string;
|
||||||
|
type: number; // IoTThingModelTypeEnum
|
||||||
|
accessMode?: string;
|
||||||
|
required?: boolean;
|
||||||
|
unit?: string;
|
||||||
|
range?: string;
|
||||||
|
eventType?: string;
|
||||||
|
callType?: string;
|
||||||
|
inputParams?: ThingModelParam[];
|
||||||
|
outputParams?: ThingModelParam[];
|
||||||
|
property?: ThingModelProperty;
|
||||||
|
event?: ThingModelEvent;
|
||||||
|
service?: ThingModelService;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localValue = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const loading = ref(false); // 加载状态
|
||||||
|
const propertyList = ref<PropertySelectorItem[]>([]); // 属性列表
|
||||||
|
const thingModelTSL = ref<IotThingModelTSLResp | null>(null); // 物模型TSL数据
|
||||||
|
|
||||||
|
// 计算属性:属性分组
|
||||||
|
const propertyGroups = computed(() => {
|
||||||
|
const groups: { label: string; options: any[] }[] = [];
|
||||||
|
|
||||||
|
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
|
||||||
|
groups.push({
|
||||||
|
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||||
|
options: propertyList.value.filter(
|
||||||
|
(p) => p.type === IoTThingModelTypeEnum.PROPERTY,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
||||||
|
groups.push({
|
||||||
|
label: THING_MODEL_GROUP_LABELS.EVENT,
|
||||||
|
options: propertyList.value.filter(
|
||||||
|
(p) => p.type === IoTThingModelTypeEnum.EVENT,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
|
||||||
|
groups.push({
|
||||||
|
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
||||||
|
options: propertyList.value.filter(
|
||||||
|
(p) => p.type === IoTThingModelTypeEnum.SERVICE,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.filter((group) => group.options.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:当前选中的属性
|
||||||
|
const selectedProperty = computed(() => {
|
||||||
|
return propertyList.value.find((p) => p.identifier === localValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理选择变化事件
|
||||||
|
* @param value 选中的属性标识符
|
||||||
|
*/
|
||||||
|
const handleChange = (value: string) => {
|
||||||
|
const property = propertyList.value.find((p) => p.identifier === value);
|
||||||
|
if (property) {
|
||||||
|
emit('change', {
|
||||||
|
type: property.dataType,
|
||||||
|
config: property,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取物模型TSL数据
|
||||||
|
*/
|
||||||
|
const getThingModelTSL = async () => {
|
||||||
|
if (!props.productId) {
|
||||||
|
thingModelTSL.value = null;
|
||||||
|
propertyList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const tslData = await ThingModelApi.getThingModelTSLByProductId(
|
||||||
|
props.productId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tslData) {
|
||||||
|
thingModelTSL.value = tslData;
|
||||||
|
parseThingModelData();
|
||||||
|
} else {
|
||||||
|
console.error('获取物模型TSL失败: 返回数据为空');
|
||||||
|
propertyList.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取物模型TSL失败:', error);
|
||||||
|
propertyList.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 解析物模型 TSL 数据 */
|
||||||
|
const parseThingModelData = () => {
|
||||||
|
const tsl = thingModelTSL.value;
|
||||||
|
const properties: PropertySelectorItem[] = [];
|
||||||
|
|
||||||
|
if (!tsl) {
|
||||||
|
propertyList.value = properties;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 解析属性
|
||||||
|
if (tsl.properties && Array.isArray(tsl.properties)) {
|
||||||
|
tsl.properties.forEach((prop) => {
|
||||||
|
properties.push({
|
||||||
|
identifier: prop.identifier,
|
||||||
|
name: prop.name,
|
||||||
|
description: prop.description,
|
||||||
|
dataType: prop.dataType,
|
||||||
|
type: IoTThingModelTypeEnum.PROPERTY,
|
||||||
|
accessMode: prop.accessMode,
|
||||||
|
required: prop.required,
|
||||||
|
unit: getPropertyUnit(prop),
|
||||||
|
range: getPropertyRange(prop),
|
||||||
|
property: prop,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析事件
|
||||||
|
if (tsl.events && Array.isArray(tsl.events)) {
|
||||||
|
tsl.events.forEach((event) => {
|
||||||
|
properties.push({
|
||||||
|
identifier: event.identifier,
|
||||||
|
name: event.name,
|
||||||
|
description: event.description,
|
||||||
|
dataType: 'struct',
|
||||||
|
type: IoTThingModelTypeEnum.EVENT,
|
||||||
|
eventType: event.type,
|
||||||
|
required: event.required,
|
||||||
|
outputParams: event.outputParams,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析服务
|
||||||
|
if (tsl.services && Array.isArray(tsl.services)) {
|
||||||
|
tsl.services.forEach((service) => {
|
||||||
|
properties.push({
|
||||||
|
identifier: service.identifier,
|
||||||
|
name: service.name,
|
||||||
|
description: service.description,
|
||||||
|
dataType: 'struct',
|
||||||
|
type: IoTThingModelTypeEnum.SERVICE,
|
||||||
|
callType: service.callType,
|
||||||
|
required: service.required,
|
||||||
|
inputParams: service.inputParams,
|
||||||
|
outputParams: service.outputParams,
|
||||||
|
service,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
propertyList.value = properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取属性单位
|
||||||
|
* @param property 属性对象
|
||||||
|
* @returns 属性单位
|
||||||
|
*/
|
||||||
|
const getPropertyUnit = (property: any) => {
|
||||||
|
if (!property) return undefined;
|
||||||
|
|
||||||
|
// 数值型数据的单位
|
||||||
|
if (property.dataSpecs && property.dataSpecs.unit) {
|
||||||
|
return property.dataSpecs.unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取属性范围描述
|
||||||
|
* @param property 属性对象
|
||||||
|
* @returns 属性范围描述
|
||||||
|
*/
|
||||||
|
const getPropertyRange = (property: any) => {
|
||||||
|
if (!property) return undefined;
|
||||||
|
|
||||||
|
// 数值型数据的范围
|
||||||
|
if (property.dataSpecs) {
|
||||||
|
const specs = property.dataSpecs;
|
||||||
|
if (specs.min !== undefined && specs.max !== undefined) {
|
||||||
|
return `${specs.min}~${specs.max}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 枚举型和布尔型数据的选项
|
||||||
|
if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
|
||||||
|
return property.dataSpecsList
|
||||||
|
.map((item: any) => `${item.name}(${item.value})`)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听产品变化 */
|
||||||
|
watch(
|
||||||
|
() => props.productId,
|
||||||
|
() => {
|
||||||
|
getThingModelTSL();
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 监听触发类型变化 */
|
||||||
|
watch(
|
||||||
|
() => props.triggerType,
|
||||||
|
() => {
|
||||||
|
localValue.value = '';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-8px">
|
<div class="gap-8px flex items-center">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="localValue"
|
v-model="localValue"
|
||||||
placeholder="请选择监控项"
|
placeholder="请选择监控项"
|
||||||
@@ -10,15 +283,21 @@
|
|||||||
class="!w-150px"
|
class="!w-150px"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
>
|
>
|
||||||
<el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
|
<el-option-group
|
||||||
|
v-for="group in propertyGroups"
|
||||||
|
:key="group.label"
|
||||||
|
:label="group.label"
|
||||||
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="property in group.options"
|
v-for="property in group.options"
|
||||||
:key="property.identifier"
|
:key="property.identifier"
|
||||||
:label="property.name"
|
:label="property.name"
|
||||||
:value="property.identifier"
|
:value="property.identifier"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between w-full py-2px">
|
<div class="py-2px flex w-full items-center justify-between">
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)] flex-1 truncate">
|
<span
|
||||||
|
class="text-14px font-500 flex-1 truncate text-[var(--el-text-color-primary)]"
|
||||||
|
>
|
||||||
{{ property.name }}
|
{{ property.name }}
|
||||||
</span>
|
</span>
|
||||||
<el-tag
|
<el-tag
|
||||||
@@ -56,49 +335,66 @@
|
|||||||
|
|
||||||
<!-- 弹出层内容 -->
|
<!-- 弹出层内容 -->
|
||||||
<div class="property-detail-content">
|
<div class="property-detail-content">
|
||||||
<div class="flex items-center gap-8px mb-12px">
|
<div class="gap-8px mb-12px flex items-center">
|
||||||
<Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
|
<Icon
|
||||||
|
icon="ep:info-filled"
|
||||||
|
class="text-16px text-[var(--el-color-info)]"
|
||||||
|
/>
|
||||||
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
<span class="text-14px font-500 text-[var(--el-text-color-primary)]">
|
||||||
{{ selectedProperty.name }}
|
{{ selectedProperty.name }}
|
||||||
</span>
|
</span>
|
||||||
<el-tag :type="getDataTypeTagType(selectedProperty.dataType)" size="small">
|
<el-tag
|
||||||
|
:type="getDataTypeTagType(selectedProperty.dataType)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{{ getDataTypeName(selectedProperty.dataType) }}
|
{{ getDataTypeName(selectedProperty.dataType) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-8px ml-24px">
|
<div class="space-y-8px ml-24px">
|
||||||
<div class="flex items-start gap-8px">
|
<div class="gap-8px flex items-start">
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
<span
|
||||||
|
class="text-12px min-w-60px flex-shrink-0 text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
标识符:
|
标识符:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
<span class="text-12px flex-1 text-[var(--el-text-color-primary)]">
|
||||||
{{ selectedProperty.identifier }}
|
{{ selectedProperty.identifier }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedProperty.description" class="flex items-start gap-8px">
|
<div
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
v-if="selectedProperty.description"
|
||||||
|
class="gap-8px flex items-start"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-12px min-w-60px flex-shrink-0 text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
描述:
|
描述:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
<span class="text-12px flex-1 text-[var(--el-text-color-primary)]">
|
||||||
{{ selectedProperty.description }}
|
{{ selectedProperty.description }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedProperty.unit" class="flex items-start gap-8px">
|
<div v-if="selectedProperty.unit" class="gap-8px flex items-start">
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
<span
|
||||||
|
class="text-12px min-w-60px flex-shrink-0 text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
单位:
|
单位:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
<span class="text-12px flex-1 text-[var(--el-text-color-primary)]">
|
||||||
{{ selectedProperty.unit }}
|
{{ selectedProperty.unit }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedProperty.range" class="flex items-start gap-8px">
|
<div v-if="selectedProperty.range" class="gap-8px flex items-start">
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
<span
|
||||||
|
class="text-12px min-w-60px flex-shrink-0 text-[var(--el-text-color-secondary)]"
|
||||||
|
>
|
||||||
取值范围:
|
取值范围:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
<span class="text-12px flex-1 text-[var(--el-text-color-primary)]">
|
||||||
{{ selectedProperty.range }}
|
{{ selectedProperty.range }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,40 +405,48 @@
|
|||||||
selectedProperty.type === IoTThingModelTypeEnum.PROPERTY &&
|
selectedProperty.type === IoTThingModelTypeEnum.PROPERTY &&
|
||||||
selectedProperty.accessMode
|
selectedProperty.accessMode
|
||||||
"
|
"
|
||||||
class="flex items-start gap-8px"
|
class="gap-8px flex items-start"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-12px min-w-60px flex-shrink-0 text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
|
||||||
访问模式:
|
访问模式:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
<span class="text-12px flex-1 text-[var(--el-text-color-primary)]">
|
||||||
{{ getAccessModeLabel(selectedProperty.accessMode) }}
|
{{ getAccessModeLabel(selectedProperty.accessMode) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
selectedProperty.type === IoTThingModelTypeEnum.EVENT && selectedProperty.eventType
|
selectedProperty.type === IoTThingModelTypeEnum.EVENT &&
|
||||||
|
selectedProperty.eventType
|
||||||
"
|
"
|
||||||
class="flex items-start gap-8px"
|
class="gap-8px flex items-start"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-12px min-w-60px flex-shrink-0 text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
|
||||||
事件类型:
|
事件类型:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
<span class="text-12px flex-1 text-[var(--el-text-color-primary)]">
|
||||||
{{ getEventTypeLabel(selectedProperty.eventType) }}
|
{{ getEventTypeLabel(selectedProperty.eventType) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
selectedProperty.type === IoTThingModelTypeEnum.SERVICE && selectedProperty.callType
|
selectedProperty.type === IoTThingModelTypeEnum.SERVICE &&
|
||||||
|
selectedProperty.callType
|
||||||
"
|
"
|
||||||
class="flex items-start gap-8px"
|
class="gap-8px flex items-start"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-12px min-w-60px flex-shrink-0 text-[var(--el-text-color-secondary)]"
|
||||||
>
|
>
|
||||||
<span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
|
|
||||||
调用类型:
|
调用类型:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-12px text-[var(--el-text-color-primary)] flex-1">
|
<span class="text-12px flex-1 text-[var(--el-text-color-primary)]">
|
||||||
{{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
|
{{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,267 +456,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
|
||||||
import {
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
getAccessModeLabel,
|
|
||||||
getEventTypeLabel,
|
|
||||||
getThingModelServiceCallTypeLabel,
|
|
||||||
getDataTypeName,
|
|
||||||
getDataTypeTagType,
|
|
||||||
THING_MODEL_GROUP_LABELS
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
import type {
|
|
||||||
IotThingModelTSLResp,
|
|
||||||
ThingModelEvent,
|
|
||||||
ThingModelParam,
|
|
||||||
ThingModelProperty,
|
|
||||||
ThingModelService
|
|
||||||
} from '#/api/iot/thingmodel'
|
|
||||||
import { ThingModelApi } from '#/api/iot/thingmodel'
|
|
||||||
|
|
||||||
/** 属性选择器组件 */
|
|
||||||
defineOptions({ name: 'PropertySelector' })
|
|
||||||
|
|
||||||
/** 属性选择器内部使用的统一数据结构 */
|
|
||||||
interface PropertySelectorItem {
|
|
||||||
identifier: string
|
|
||||||
name: string
|
|
||||||
description?: string
|
|
||||||
dataType: string
|
|
||||||
type: number // IoTThingModelTypeEnum
|
|
||||||
accessMode?: string
|
|
||||||
required?: boolean
|
|
||||||
unit?: string
|
|
||||||
range?: string
|
|
||||||
eventType?: string
|
|
||||||
callType?: string
|
|
||||||
inputParams?: ThingModelParam[]
|
|
||||||
outputParams?: ThingModelParam[]
|
|
||||||
property?: ThingModelProperty
|
|
||||||
event?: ThingModelEvent
|
|
||||||
service?: ThingModelService
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
modelValue?: string
|
|
||||||
triggerType: number
|
|
||||||
productId?: number
|
|
||||||
deviceId?: number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'update:modelValue', value: string): void
|
|
||||||
(e: 'change', value: { type: string; config: any }): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit)
|
|
||||||
|
|
||||||
const loading = ref(false) // 加载状态
|
|
||||||
const propertyList = ref<PropertySelectorItem[]>([]) // 属性列表
|
|
||||||
const thingModelTSL = ref<IotThingModelTSLResp | null>(null) // 物模型TSL数据
|
|
||||||
|
|
||||||
// 计算属性:属性分组
|
|
||||||
const propertyGroups = computed(() => {
|
|
||||||
const groups: { label: string; options: any[] }[] = []
|
|
||||||
|
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
|
||||||
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.PROPERTY)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.EVENT,
|
|
||||||
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.EVENT)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
|
||||||
options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.SERVICE)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.filter((group) => group.options.length > 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算属性:当前选中的属性
|
|
||||||
const selectedProperty = computed(() => {
|
|
||||||
return propertyList.value.find((p) => p.identifier === localValue.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理选择变化事件
|
|
||||||
* @param value 选中的属性标识符
|
|
||||||
*/
|
|
||||||
const handleChange = (value: string) => {
|
|
||||||
const property = propertyList.value.find((p) => p.identifier === value)
|
|
||||||
if (property) {
|
|
||||||
emit('change', {
|
|
||||||
type: property.dataType,
|
|
||||||
config: property
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取物模型TSL数据
|
|
||||||
*/
|
|
||||||
const getThingModelTSL = async () => {
|
|
||||||
if (!props.productId) {
|
|
||||||
thingModelTSL.value = null
|
|
||||||
propertyList.value = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
|
|
||||||
|
|
||||||
if (tslData) {
|
|
||||||
thingModelTSL.value = tslData
|
|
||||||
parseThingModelData()
|
|
||||||
} else {
|
|
||||||
console.error('获取物模型TSL失败: 返回数据为空')
|
|
||||||
propertyList.value = []
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取物模型TSL失败:', error)
|
|
||||||
propertyList.value = []
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 解析物模型 TSL 数据 */
|
|
||||||
const parseThingModelData = () => {
|
|
||||||
const tsl = thingModelTSL.value
|
|
||||||
const properties: PropertySelectorItem[] = []
|
|
||||||
|
|
||||||
if (!tsl) {
|
|
||||||
propertyList.value = properties
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 解析属性
|
|
||||||
if (tsl.properties && Array.isArray(tsl.properties)) {
|
|
||||||
tsl.properties.forEach((prop) => {
|
|
||||||
properties.push({
|
|
||||||
identifier: prop.identifier,
|
|
||||||
name: prop.name,
|
|
||||||
description: prop.description,
|
|
||||||
dataType: prop.dataType,
|
|
||||||
type: IoTThingModelTypeEnum.PROPERTY,
|
|
||||||
accessMode: prop.accessMode,
|
|
||||||
required: prop.required,
|
|
||||||
unit: getPropertyUnit(prop),
|
|
||||||
range: getPropertyRange(prop),
|
|
||||||
property: prop
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析事件
|
|
||||||
if (tsl.events && Array.isArray(tsl.events)) {
|
|
||||||
tsl.events.forEach((event) => {
|
|
||||||
properties.push({
|
|
||||||
identifier: event.identifier,
|
|
||||||
name: event.name,
|
|
||||||
description: event.description,
|
|
||||||
dataType: 'struct',
|
|
||||||
type: IoTThingModelTypeEnum.EVENT,
|
|
||||||
eventType: event.type,
|
|
||||||
required: event.required,
|
|
||||||
outputParams: event.outputParams,
|
|
||||||
event: event
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析服务
|
|
||||||
if (tsl.services && Array.isArray(tsl.services)) {
|
|
||||||
tsl.services.forEach((service) => {
|
|
||||||
properties.push({
|
|
||||||
identifier: service.identifier,
|
|
||||||
name: service.name,
|
|
||||||
description: service.description,
|
|
||||||
dataType: 'struct',
|
|
||||||
type: IoTThingModelTypeEnum.SERVICE,
|
|
||||||
callType: service.callType,
|
|
||||||
required: service.required,
|
|
||||||
inputParams: service.inputParams,
|
|
||||||
outputParams: service.outputParams,
|
|
||||||
service: service
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
propertyList.value = properties
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取属性单位
|
|
||||||
* @param property 属性对象
|
|
||||||
* @returns 属性单位
|
|
||||||
*/
|
|
||||||
const getPropertyUnit = (property: any) => {
|
|
||||||
if (!property) return undefined
|
|
||||||
|
|
||||||
// 数值型数据的单位
|
|
||||||
if (property.dataSpecs && property.dataSpecs.unit) {
|
|
||||||
return property.dataSpecs.unit
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取属性范围描述
|
|
||||||
* @param property 属性对象
|
|
||||||
* @returns 属性范围描述
|
|
||||||
*/
|
|
||||||
const getPropertyRange = (property: any) => {
|
|
||||||
if (!property) return undefined
|
|
||||||
|
|
||||||
// 数值型数据的范围
|
|
||||||
if (property.dataSpecs) {
|
|
||||||
const specs = property.dataSpecs
|
|
||||||
if (specs.min !== undefined && specs.max !== undefined) {
|
|
||||||
return `${specs.min}~${specs.max}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 枚举型和布尔型数据的选项
|
|
||||||
if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
|
|
||||||
return property.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 监听产品变化 */
|
|
||||||
watch(
|
|
||||||
() => props.productId,
|
|
||||||
() => {
|
|
||||||
getThingModelTSL()
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 监听触发类型变化 */
|
|
||||||
watch(
|
|
||||||
() => props.triggerType,
|
|
||||||
() => {
|
|
||||||
localValue.value = ''
|
|
||||||
}
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 下拉选项样式 */
|
/* 下拉选项样式 */
|
||||||
:deep(.el-select-dropdown__item) {
|
:deep(.el-select-dropdown__item) {
|
||||||
|
|||||||
@@ -129,7 +129,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
{
|
{
|
||||||
label: row.status === 0 ? '停用' : '启用',
|
label: row.status === 0 ? '停用' : '启用',
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: row.status === 0 ? 'ant-design:stop-outlined' : 'ant-design:check-circle-outlined',
|
icon:
|
||||||
|
row.status === 0
|
||||||
|
? 'ant-design:stop-outlined'
|
||||||
|
: 'ant-design:check-circle-outlined',
|
||||||
onClick: handleToggleStatus.bind(null, row),
|
onClick: handleToggleStatus.bind(null, row),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
// 提交表单
|
// 提交表单
|
||||||
const data = (await formApi.getValues()) as RuleSceneApi.SceneRule;
|
const data = (await formApi.getValues()) as RuleSceneApi.SceneRule;
|
||||||
try {
|
try {
|
||||||
await (formData.value?.id ? updateSceneRule(data) : createSceneRule(data));
|
await (formData.value?.id
|
||||||
|
? updateSceneRule(data)
|
||||||
|
: createSceneRule(data));
|
||||||
// 关闭并提示
|
// 关闭并提示
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
@@ -83,4 +85,3 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
<Form class="mx-4" />
|
<Form class="mx-4" />
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VxeGridInstance } from '#/adapter/vxe-table';
|
import type { VxeGridInstance } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
|
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
|
||||||
import { getDataTypeOptionsLabel } from '../utils/constants';
|
import { getDataTypeOptionsLabel } from '../utils/constants';
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
|
||||||
defineOptions({ name: 'IoTThingModel' });
|
defineOptions({ name: 'IoTThingModel' });
|
||||||
|
|
||||||
@@ -87,9 +88,7 @@ const getDataTypeLabel = (row: any) => {
|
|||||||
<Icon icon="ant-design:plus-outlined" class="mr-1" />
|
<Icon icon="ant-design:plus-outlined" class="mr-1" />
|
||||||
添加功能
|
添加功能
|
||||||
</VbenButton>
|
</VbenButton>
|
||||||
<VbenButton type="success" @click="handleOpenTSL">
|
<VbenButton type="success" @click="handleOpenTSL"> TSL </VbenButton>
|
||||||
TSL
|
|
||||||
</VbenButton>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 数据类型列 -->
|
<!-- 数据类型列 -->
|
||||||
|
|||||||
@@ -1,4 +1,38 @@
|
|||||||
<!-- 产品的物模型表单(event 项) -->
|
<!-- 产品的物模型表单(event 项) -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { ThingModelEvent } from '#/api/iot/thingmodel';
|
||||||
|
import {
|
||||||
|
IoTThingModelEventTypeEnum,
|
||||||
|
IoTThingModelParamDirectionEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue';
|
||||||
|
|
||||||
|
/** IoT 物模型事件 */
|
||||||
|
defineOptions({ name: 'ThingModelEvent' });
|
||||||
|
|
||||||
|
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
|
const thingModelEvent = useVModel(
|
||||||
|
props,
|
||||||
|
'modelValue',
|
||||||
|
emits,
|
||||||
|
) as Ref<ThingModelEvent>;
|
||||||
|
|
||||||
|
// 默认选中,INFO 信息
|
||||||
|
watch(
|
||||||
|
() => thingModelEvent.value.type,
|
||||||
|
(val: string) =>
|
||||||
|
isEmpty(val) &&
|
||||||
|
(thingModelEvent.value.type = IoTThingModelEventTypeEnum.INFO.value),
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
|
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
|
||||||
@@ -23,32 +57,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { ThingModelEvent } from '#/api/iot/thingmodel'
|
|
||||||
import { isEmpty } from '@vben/utils'
|
|
||||||
import {
|
|
||||||
IoTThingModelEventTypeEnum,
|
|
||||||
IoTThingModelParamDirectionEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** IoT 物模型事件 */
|
|
||||||
defineOptions({ name: 'ThingModelEvent' })
|
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>()
|
|
||||||
const emits = defineEmits(['update:modelValue'])
|
|
||||||
const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<ThingModelEvent>
|
|
||||||
|
|
||||||
// 默认选中,INFO 信息
|
|
||||||
watch(
|
|
||||||
() => thingModelEvent.value.type,
|
|
||||||
(val: string) =>
|
|
||||||
isEmpty(val) && (thingModelEvent.value.type = IoTThingModelEventTypeEnum.INFO.value),
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
|
|||||||
@@ -1,4 +1,184 @@
|
|||||||
<!-- 产品的物模型表单 -->
|
<!-- 产品的物模型表单 -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { ProductVO } from '#/api/iot/product/product';
|
||||||
|
import type { ThingModelData } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { DICT_TYPE, getIntDictOptions } from '@vben/constants';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
|
import { ThingModelApi, ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||||
|
import {
|
||||||
|
IOT_PROVIDE_KEY,
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import ThingModelEvent from './ThingModelEvent.vue';
|
||||||
|
import ThingModelProperty from './ThingModelProperty.vue';
|
||||||
|
import ThingModelService from './ThingModelService.vue';
|
||||||
|
|
||||||
|
/** IoT 物模型数据表单 */
|
||||||
|
defineOptions({ name: 'IoTThingModelForm' });
|
||||||
|
|
||||||
|
/** 提交表单 */
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
|
||||||
|
|
||||||
|
const { t } = useI18n(); // 国际化
|
||||||
|
|
||||||
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
|
const dialogTitle = ref(''); // 弹窗的标题
|
||||||
|
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const formType = ref(''); // 表单的类型:create - 新增;update - 修改
|
||||||
|
const formData = ref<ThingModelData>({
|
||||||
|
type: IoTThingModelTypeEnum.PROPERTY,
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
property: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
dataSpecs: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
service: {},
|
||||||
|
event: {},
|
||||||
|
} as ThingModelData);
|
||||||
|
|
||||||
|
const formRef = ref(); // 表单 Ref
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true;
|
||||||
|
dialogTitle.value = t(`action.${type}`);
|
||||||
|
formType.value = type;
|
||||||
|
resetForm();
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true;
|
||||||
|
try {
|
||||||
|
formData.value = await ThingModelApi.getThingModel(id);
|
||||||
|
// 情况一:属性初始化
|
||||||
|
if (
|
||||||
|
!formData.value.property ||
|
||||||
|
Object.keys(formData.value.property).length === 0
|
||||||
|
) {
|
||||||
|
formData.value.dataType = IoTDataSpecsDataTypeEnum.INT;
|
||||||
|
formData.value.property = {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
dataSpecs: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 情况二:服务初始化
|
||||||
|
if (
|
||||||
|
!formData.value.service ||
|
||||||
|
Object.keys(formData.value.service).length === 0
|
||||||
|
) {
|
||||||
|
formData.value.service = {};
|
||||||
|
}
|
||||||
|
// 情况三:事件初始化
|
||||||
|
if (
|
||||||
|
!formData.value.event ||
|
||||||
|
Object.keys(formData.value.event).length === 0
|
||||||
|
) {
|
||||||
|
formData.value.event = {};
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
defineExpose({ open, close: () => (dialogVisible.value = false) });
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate();
|
||||||
|
formLoading.value = true;
|
||||||
|
try {
|
||||||
|
const data = cloneDeep(formData.value) as ThingModelData;
|
||||||
|
// 信息补全
|
||||||
|
data.productId = product!.value.id;
|
||||||
|
data.productKey = product!.value.productKey;
|
||||||
|
fillExtraAttributes(data);
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await ThingModelApi.createThingModel(data);
|
||||||
|
message.success({ content: t('common.createSuccess') });
|
||||||
|
} else {
|
||||||
|
await ThingModelApi.updateThingModel(data);
|
||||||
|
message.success({ content: t('common.updateSuccess') });
|
||||||
|
}
|
||||||
|
// 关闭弹窗
|
||||||
|
dialogVisible.value = false;
|
||||||
|
emit('success');
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 填写额外的属性(处理不同类型的情况) */
|
||||||
|
const fillExtraAttributes = (data: any) => {
|
||||||
|
// 属性
|
||||||
|
if (data.type === IoTThingModelTypeEnum.PROPERTY) {
|
||||||
|
removeDataSpecs(data.property);
|
||||||
|
data.dataType = data.property.dataType;
|
||||||
|
data.property.identifier = data.identifier;
|
||||||
|
data.property.name = data.name;
|
||||||
|
delete data.service;
|
||||||
|
delete data.event;
|
||||||
|
}
|
||||||
|
// 服务
|
||||||
|
if (data.type === IoTThingModelTypeEnum.SERVICE) {
|
||||||
|
removeDataSpecs(data.service);
|
||||||
|
data.dataType = data.service.dataType;
|
||||||
|
data.service.identifier = data.identifier;
|
||||||
|
data.service.name = data.name;
|
||||||
|
delete data.property;
|
||||||
|
delete data.event;
|
||||||
|
}
|
||||||
|
// 事件
|
||||||
|
if (data.type === IoTThingModelTypeEnum.EVENT) {
|
||||||
|
removeDataSpecs(data.event);
|
||||||
|
data.dataType = data.event.dataType;
|
||||||
|
data.event.identifier = data.identifier;
|
||||||
|
data.event.name = data.name;
|
||||||
|
delete data.property;
|
||||||
|
delete data.service;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 处理 dataSpecs 为空的情况 */
|
||||||
|
const removeDataSpecs = (val: any) => {
|
||||||
|
if (!val.dataSpecs || Object.keys(val.dataSpecs).length === 0) {
|
||||||
|
delete val.dataSpecs;
|
||||||
|
}
|
||||||
|
if (!val.dataSpecsList || val.dataSpecsList.length === 0) {
|
||||||
|
delete val.dataSpecsList;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
type: IoTThingModelTypeEnum.PROPERTY,
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
property: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
dataSpecs: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
service: {},
|
||||||
|
event: {},
|
||||||
|
} as ThingModelData;
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||||
<a-form
|
<a-form
|
||||||
@@ -24,7 +204,10 @@
|
|||||||
<a-input v-model:value="formData.name" placeholder="请输入功能名称" />
|
<a-input v-model:value="formData.name" placeholder="请输入功能名称" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="标识符" name="identifier">
|
<a-form-item label="标识符" name="identifier">
|
||||||
<a-input v-model:value="formData.identifier" placeholder="请输入标识符" />
|
<a-input
|
||||||
|
v-model:value="formData.identifier"
|
||||||
|
placeholder="请输入标识符"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<!-- 属性配置 -->
|
<!-- 属性配置 -->
|
||||||
<ThingModelProperty
|
<ThingModelProperty
|
||||||
@@ -52,172 +235,10 @@
|
|||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<a-button :disabled="formLoading" type="primary" @click="submitForm">确 定</a-button>
|
<a-button :disabled="formLoading" type="primary" @click="submitForm">
|
||||||
|
确 定
|
||||||
|
</a-button>
|
||||||
<a-button @click="dialogVisible = false">取 消</a-button>
|
<a-button @click="dialogVisible = false">取 消</a-button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ref, inject, type Ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import type { ProductVO } from '#/api/iot/product/product'
|
|
||||||
import ThingModelProperty from './ThingModelProperty.vue'
|
|
||||||
import ThingModelService from './ThingModelService.vue'
|
|
||||||
import ThingModelEvent from './ThingModelEvent.vue'
|
|
||||||
import { ThingModelApi, ThingModelFormRules } from '#/api/iot/thingmodel'
|
|
||||||
import type { ThingModelData } from '#/api/iot/thingmodel'
|
|
||||||
import {
|
|
||||||
IOT_PROVIDE_KEY,
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
IoTThingModelTypeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
import { cloneDeep } from 'lodash-es'
|
|
||||||
import { DICT_TYPE, getIntDictOptions } from '@vben/constants'
|
|
||||||
|
|
||||||
/** IoT 物模型数据表单 */
|
|
||||||
defineOptions({ name: 'IoTThingModelForm' })
|
|
||||||
|
|
||||||
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
|
|
||||||
|
|
||||||
const { t } = useI18n() // 国际化
|
|
||||||
|
|
||||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
|
||||||
const dialogTitle = ref('') // 弹窗的标题
|
|
||||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
||||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
|
||||||
const formData = ref<ThingModelData>({
|
|
||||||
type: IoTThingModelTypeEnum.PROPERTY,
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
property: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
dataSpecs: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT
|
|
||||||
}
|
|
||||||
},
|
|
||||||
service: {},
|
|
||||||
event: {}
|
|
||||||
} as ThingModelData)
|
|
||||||
|
|
||||||
const formRef = ref() // 表单 Ref
|
|
||||||
|
|
||||||
/** 打开弹窗 */
|
|
||||||
const open = async (type: string, id?: number) => {
|
|
||||||
dialogVisible.value = true
|
|
||||||
dialogTitle.value = t('action.' + type)
|
|
||||||
formType.value = type
|
|
||||||
resetForm()
|
|
||||||
if (id) {
|
|
||||||
formLoading.value = true
|
|
||||||
try {
|
|
||||||
formData.value = await ThingModelApi.getThingModel(id)
|
|
||||||
// 情况一:属性初始化
|
|
||||||
if (!formData.value.property || Object.keys(formData.value.property).length === 0) {
|
|
||||||
formData.value.dataType = IoTDataSpecsDataTypeEnum.INT
|
|
||||||
formData.value.property = {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
dataSpecs: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 情况二:服务初始化
|
|
||||||
if (!formData.value.service || Object.keys(formData.value.service).length === 0) {
|
|
||||||
formData.value.service = {}
|
|
||||||
}
|
|
||||||
// 情况三:事件初始化
|
|
||||||
if (!formData.value.event || Object.keys(formData.value.event).length === 0) {
|
|
||||||
formData.value.event = {}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
formLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defineExpose({ open, close: () => (dialogVisible.value = false) })
|
|
||||||
|
|
||||||
/** 提交表单 */
|
|
||||||
const emit = defineEmits(['success'])
|
|
||||||
const submitForm = async () => {
|
|
||||||
await formRef.value.validate()
|
|
||||||
formLoading.value = true
|
|
||||||
try {
|
|
||||||
const data = cloneDeep(formData.value) as ThingModelData
|
|
||||||
// 信息补全
|
|
||||||
data.productId = product!.value.id
|
|
||||||
data.productKey = product!.value.productKey
|
|
||||||
fillExtraAttributes(data)
|
|
||||||
if (formType.value === 'create') {
|
|
||||||
await ThingModelApi.createThingModel(data)
|
|
||||||
message.success({ content: t('common.createSuccess') })
|
|
||||||
} else {
|
|
||||||
await ThingModelApi.updateThingModel(data)
|
|
||||||
message.success({ content: t('common.updateSuccess') })
|
|
||||||
}
|
|
||||||
// 关闭弹窗
|
|
||||||
dialogVisible.value = false
|
|
||||||
emit('success')
|
|
||||||
} finally {
|
|
||||||
formLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 填写额外的属性(处理不同类型的情况) */
|
|
||||||
const fillExtraAttributes = (data: any) => {
|
|
||||||
// 属性
|
|
||||||
if (data.type === IoTThingModelTypeEnum.PROPERTY) {
|
|
||||||
removeDataSpecs(data.property)
|
|
||||||
data.dataType = data.property.dataType
|
|
||||||
data.property.identifier = data.identifier
|
|
||||||
data.property.name = data.name
|
|
||||||
delete data.service
|
|
||||||
delete data.event
|
|
||||||
}
|
|
||||||
// 服务
|
|
||||||
if (data.type === IoTThingModelTypeEnum.SERVICE) {
|
|
||||||
removeDataSpecs(data.service)
|
|
||||||
data.dataType = data.service.dataType
|
|
||||||
data.service.identifier = data.identifier
|
|
||||||
data.service.name = data.name
|
|
||||||
delete data.property
|
|
||||||
delete data.event
|
|
||||||
}
|
|
||||||
// 事件
|
|
||||||
if (data.type === IoTThingModelTypeEnum.EVENT) {
|
|
||||||
removeDataSpecs(data.event)
|
|
||||||
data.dataType = data.event.dataType
|
|
||||||
data.event.identifier = data.identifier
|
|
||||||
data.event.name = data.name
|
|
||||||
delete data.property
|
|
||||||
delete data.service
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 处理 dataSpecs 为空的情况 */
|
|
||||||
const removeDataSpecs = (val: any) => {
|
|
||||||
if (!val.dataSpecs || Object.keys(val.dataSpecs).length === 0) {
|
|
||||||
delete val.dataSpecs
|
|
||||||
}
|
|
||||||
if (!val.dataSpecsList || val.dataSpecsList.length === 0) {
|
|
||||||
delete val.dataSpecsList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 重置表单 */
|
|
||||||
const resetForm = () => {
|
|
||||||
formData.value = {
|
|
||||||
type: IoTThingModelTypeEnum.PROPERTY,
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
property: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
dataSpecs: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT
|
|
||||||
}
|
|
||||||
},
|
|
||||||
service: {},
|
|
||||||
event: {}
|
|
||||||
} as ThingModelData
|
|
||||||
formRef.value?.resetFields()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,18 +1,135 @@
|
|||||||
<!-- 产品的物模型表单(event、service 项里的参数) -->
|
<!-- 产品的物模型表单(event、service 项里的参数) -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||||
|
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import ThingModelProperty from './ThingModelProperty.vue';
|
||||||
|
|
||||||
|
/** 输入输出参数配置组件 */
|
||||||
|
defineOptions({ name: 'ThingModelInputOutputParam' });
|
||||||
|
|
||||||
|
const props = defineProps<{ direction: string; modelValue: any }>();
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
|
const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>;
|
||||||
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
|
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const paramFormRef = ref(); // 表单 ref
|
||||||
|
const formData = ref<any>({
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
property: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
dataSpecs: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 打开 param 表单 */
|
||||||
|
const openParamForm = (val: any) => {
|
||||||
|
dialogVisible.value = true;
|
||||||
|
resetForm();
|
||||||
|
if (isEmpty(val)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 编辑时回显数据
|
||||||
|
formData.value = {
|
||||||
|
identifier: val.identifier,
|
||||||
|
name: val.name,
|
||||||
|
description: val.description,
|
||||||
|
property: {
|
||||||
|
dataType: val.dataType,
|
||||||
|
dataSpecs: val.dataSpecs,
|
||||||
|
dataSpecsList: val.dataSpecsList,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除 param 项 */
|
||||||
|
const deleteParamItem = (index: number) => {
|
||||||
|
thingModelParams.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 添加参数 */
|
||||||
|
const submitForm = async () => {
|
||||||
|
// 初始化参数列表
|
||||||
|
if (isEmpty(thingModelParams.value)) {
|
||||||
|
thingModelParams.value = [];
|
||||||
|
}
|
||||||
|
// 校验参数
|
||||||
|
await paramFormRef.value.validate();
|
||||||
|
try {
|
||||||
|
// 构建数据对象
|
||||||
|
const data = unref(formData);
|
||||||
|
const item = {
|
||||||
|
identifier: data.identifier,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
dataType: data.property.dataType,
|
||||||
|
paraOrder: 0, // TODO @puhui999: 先写死默认看看后续
|
||||||
|
direction: props.direction,
|
||||||
|
dataSpecs:
|
||||||
|
!!data.property.dataSpecs &&
|
||||||
|
Object.keys(data.property.dataSpecs).length > 1
|
||||||
|
? data.property.dataSpecs
|
||||||
|
: undefined,
|
||||||
|
dataSpecsList: isEmpty(data.property.dataSpecsList)
|
||||||
|
? undefined
|
||||||
|
: data.property.dataSpecsList,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增或修改同 identifier 的参数
|
||||||
|
const existingIndex = thingModelParams.value.findIndex(
|
||||||
|
(spec) => spec.identifier === data.identifier,
|
||||||
|
);
|
||||||
|
if (existingIndex === -1) {
|
||||||
|
thingModelParams.value.push(item);
|
||||||
|
} else {
|
||||||
|
thingModelParams.value[existingIndex] = item;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
dialogVisible.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
property: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
dataSpecs: {
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.INT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
paramFormRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-for="(item, index) in thingModelParams"
|
v-for="(item, index) in thingModelParams"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="w-1/1 param-item flex justify-between px-10px mb-10px"
|
class="w-1/1 param-item px-10px mb-10px flex justify-between"
|
||||||
>
|
>
|
||||||
<span>参数名称:{{ item.name }}</span>
|
<span>参数名称:{{ item.name }}</span>
|
||||||
<div class="btn">
|
<div class="btn">
|
||||||
<el-button link type="primary" @click="openParamForm(item)">编辑</el-button>
|
<el-button link type="primary" @click="openParamForm(item)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
<el-divider direction="vertical" />
|
<el-divider direction="vertical" />
|
||||||
<el-button link type="danger" @click="deleteParamItem(index)">删除</el-button>
|
<el-button link type="danger" @click="deleteParamItem(index)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-button link type="primary" @click="openParamForm(null)">+新增参数</el-button>
|
<el-button link type="primary" @click="openParamForm(null)">
|
||||||
|
+新增参数
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<!-- param 表单 -->
|
<!-- param 表单 -->
|
||||||
<Dialog v-model="dialogVisible" title="新增参数" append-to-body>
|
<Dialog v-model="dialogVisible" title="新增参数" append-to-body>
|
||||||
@@ -33,117 +150,14 @@
|
|||||||
<ThingModelProperty v-model="formData.property" is-params />
|
<ThingModelProperty v-model="formData.property" is-params />
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
<el-button :disabled="formLoading" type="primary" @click="submitForm">
|
||||||
|
确 定
|
||||||
|
</el-button>
|
||||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import ThingModelProperty from './ThingModelProperty.vue'
|
|
||||||
import { isEmpty } from '@vben/utils'
|
|
||||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants'
|
|
||||||
import { ThingModelFormRules } from '#/api/iot/thingmodel'
|
|
||||||
|
|
||||||
/** 输入输出参数配置组件 */
|
|
||||||
defineOptions({ name: 'ThingModelInputOutputParam' })
|
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: any; direction: string }>()
|
|
||||||
const emits = defineEmits(['update:modelValue'])
|
|
||||||
const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>
|
|
||||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
|
||||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
|
||||||
const paramFormRef = ref() // 表单 ref
|
|
||||||
const formData = ref<any>({
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
property: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
dataSpecs: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 打开 param 表单 */
|
|
||||||
const openParamForm = (val: any) => {
|
|
||||||
dialogVisible.value = true
|
|
||||||
resetForm()
|
|
||||||
if (isEmpty(val)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 编辑时回显数据
|
|
||||||
formData.value = {
|
|
||||||
identifier: val.identifier,
|
|
||||||
name: val.name,
|
|
||||||
description: val.description,
|
|
||||||
property: {
|
|
||||||
dataType: val.dataType,
|
|
||||||
dataSpecs: val.dataSpecs,
|
|
||||||
dataSpecsList: val.dataSpecsList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 删除 param 项 */
|
|
||||||
const deleteParamItem = (index: number) => {
|
|
||||||
thingModelParams.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 添加参数 */
|
|
||||||
const submitForm = async () => {
|
|
||||||
// 初始化参数列表
|
|
||||||
if (isEmpty(thingModelParams.value)) {
|
|
||||||
thingModelParams.value = []
|
|
||||||
}
|
|
||||||
// 校验参数
|
|
||||||
await paramFormRef.value.validate()
|
|
||||||
try {
|
|
||||||
// 构建数据对象
|
|
||||||
const data = unref(formData)
|
|
||||||
const item = {
|
|
||||||
identifier: data.identifier,
|
|
||||||
name: data.name,
|
|
||||||
description: data.description,
|
|
||||||
dataType: data.property.dataType,
|
|
||||||
paraOrder: 0, // TODO @puhui999: 先写死默认看看后续
|
|
||||||
direction: props.direction,
|
|
||||||
dataSpecs:
|
|
||||||
!!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1
|
|
||||||
? data.property.dataSpecs
|
|
||||||
: undefined,
|
|
||||||
dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增或修改同 identifier 的参数
|
|
||||||
const existingIndex = thingModelParams.value.findIndex(
|
|
||||||
(spec) => spec.identifier === data.identifier
|
|
||||||
)
|
|
||||||
if (existingIndex > -1) {
|
|
||||||
thingModelParams.value[existingIndex] = item
|
|
||||||
} else {
|
|
||||||
thingModelParams.value.push(item)
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
dialogVisible.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 重置表单 */
|
|
||||||
const resetForm = () => {
|
|
||||||
formData.value = {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
property: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
dataSpecs: {
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.INT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
paramFormRef.value?.resetFields()
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.param-item {
|
.param-item {
|
||||||
background-color: #e4f2fd;
|
background-color: #e4f2fd;
|
||||||
|
|||||||
@@ -1,11 +1,108 @@
|
|||||||
<!-- 产品的物模型表单(property 项) -->
|
<!-- 产品的物模型表单(property 项) -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { ThingModelProperty, validateBoolName } from '#/api/iot/thingmodel';
|
||||||
|
import {
|
||||||
|
getDataTypeOptions,
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IoTThingModelAccessModeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ThingModelArrayDataSpecs,
|
||||||
|
ThingModelEnumDataSpecs,
|
||||||
|
ThingModelNumberDataSpecs,
|
||||||
|
ThingModelStructDataSpecs,
|
||||||
|
} from './dataSpecs';
|
||||||
|
|
||||||
|
/** IoT 物模型属性 */
|
||||||
|
defineOptions({ name: 'ThingModelProperty' });
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isParams?: boolean;
|
||||||
|
isStructDataSpecs?: boolean;
|
||||||
|
modelValue: any;
|
||||||
|
}>();
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
|
const property = useVModel(
|
||||||
|
props,
|
||||||
|
'modelValue',
|
||||||
|
emits,
|
||||||
|
) as Ref<ThingModelProperty>;
|
||||||
|
const getDataTypeOptions2 = computed(() => {
|
||||||
|
if (!props.isStructDataSpecs) {
|
||||||
|
return getDataTypeOptions();
|
||||||
|
}
|
||||||
|
const excludedTypes = new Set([
|
||||||
|
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||||
|
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||||
|
]);
|
||||||
|
return getDataTypeOptions().filter(
|
||||||
|
(item: any) => !excludedTypes.has(item.value),
|
||||||
|
);
|
||||||
|
}); // 获得数据类型列表
|
||||||
|
|
||||||
|
/** 属性值的数据类型切换时初始化相关数据 */
|
||||||
|
const handleChange = (dataType: any) => {
|
||||||
|
property.value.dataSpecs = {};
|
||||||
|
property.value.dataSpecsList = [];
|
||||||
|
// 不是列表型数据才设置 dataSpecs.dataType
|
||||||
|
![
|
||||||
|
IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
|
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||||
|
].includes(dataType) && (property.value.dataSpecs.dataType = dataType);
|
||||||
|
switch (dataType) {
|
||||||
|
case IoTDataSpecsDataTypeEnum.BOOL: {
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
property.value.dataSpecsList.push({
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
|
name: '', // 布尔值的名称
|
||||||
|
value: i, // 布尔值
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IoTDataSpecsDataTypeEnum.ENUM: {
|
||||||
|
property.value.dataSpecsList.push({
|
||||||
|
dataType: IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
|
name: '', // 枚举项的名称
|
||||||
|
value: undefined, // 枚举值
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 默认选中读写 */
|
||||||
|
watch(
|
||||||
|
() => property.value.accessMode,
|
||||||
|
(val: string) => {
|
||||||
|
if (props.isStructDataSpecs || props.isParams) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isEmpty(val) &&
|
||||||
|
(property.value.accessMode =
|
||||||
|
IoTThingModelAccessModeEnum.READ_WRITE.value);
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
|
:rules="[{ required: true, message: '请选择数据类型', trigger: 'change' }]"
|
||||||
label="数据类型"
|
label="数据类型"
|
||||||
prop="property.dataType"
|
prop="property.dataType"
|
||||||
>
|
>
|
||||||
<el-select v-model="property.dataType" placeholder="请选择数据类型" @change="handleChange">
|
<el-select
|
||||||
|
v-model="property.dataType"
|
||||||
|
placeholder="请选择数据类型"
|
||||||
|
@change="handleChange"
|
||||||
|
>
|
||||||
<!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套 2 层(父和子) -->
|
<!-- ARRAY 和 STRUCT 类型数据相互嵌套时,最多支持递归嵌套 2 层(父和子) -->
|
||||||
<el-option
|
<el-option
|
||||||
v-for="option in getDataTypeOptions2"
|
v-for="option in getDataTypeOptions2"
|
||||||
@@ -21,7 +118,7 @@
|
|||||||
[
|
[
|
||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
].includes(property.dataType || '')
|
].includes(property.dataType || '')
|
||||||
"
|
"
|
||||||
v-model="property.dataSpecs"
|
v-model="property.dataSpecs"
|
||||||
@@ -32,18 +129,21 @@
|
|||||||
v-model="property.dataSpecsList"
|
v-model="property.dataSpecsList"
|
||||||
/>
|
/>
|
||||||
<!-- 布尔型配置 -->
|
<!-- 布尔型配置 -->
|
||||||
<el-form-item v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL" label="布尔值">
|
<el-form-item
|
||||||
|
v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL"
|
||||||
|
label="布尔值"
|
||||||
|
>
|
||||||
<template v-for="(item, index) in property.dataSpecsList" :key="item.value">
|
<template v-for="(item, index) 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>{{ item.value }}</span>
|
||||||
<span class="mx-2">-</span>
|
<span class="mx-2">-</span>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:prop="`property.dataSpecsList[${index}].name`"
|
:prop="`property.dataSpecsList[${index}].name`"
|
||||||
:rules="[
|
:rules="[
|
||||||
{ required: true, message: '枚举描述不能为空' },
|
{ required: true, message: '枚举描述不能为空' },
|
||||||
{ validator: validateBoolName, trigger: 'blur' }
|
{ validator: validateBoolName, trigger: 'blur' },
|
||||||
]"
|
]"
|
||||||
class="flex-1 mb-0"
|
class="mb-0 flex-1"
|
||||||
>
|
>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="item.name"
|
v-model="item.name"
|
||||||
@@ -60,7 +160,11 @@
|
|||||||
label="数据长度"
|
label="数据长度"
|
||||||
prop="property.dataSpecs.length"
|
prop="property.dataSpecs.length"
|
||||||
>
|
>
|
||||||
<el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="请输入文本字节长度">
|
<el-input
|
||||||
|
v-model="property.dataSpecs.length"
|
||||||
|
class="w-255px!"
|
||||||
|
placeholder="请输入文本字节长度"
|
||||||
|
>
|
||||||
<template #append>字节</template>
|
<template #append>字节</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -70,7 +174,11 @@
|
|||||||
label="时间格式"
|
label="时间格式"
|
||||||
prop="date"
|
prop="date"
|
||||||
>
|
>
|
||||||
<el-input class="w-255px!" disabled placeholder="String 类型的 UTC 时间戳(毫秒)" />
|
<el-input
|
||||||
|
class="w-255px!"
|
||||||
|
disabled
|
||||||
|
placeholder="String 类型的 UTC 时间戳(毫秒)"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- 数组型配置-->
|
<!-- 数组型配置-->
|
||||||
<ThingModelArrayDataSpecs
|
<ThingModelArrayDataSpecs
|
||||||
@@ -82,7 +190,11 @@
|
|||||||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT"
|
v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT"
|
||||||
v-model="property.dataSpecsList"
|
v-model="property.dataSpecsList"
|
||||||
/>
|
/>
|
||||||
<el-form-item v-if="!isStructDataSpecs && !isParams" label="读写类型" prop="property.accessMode">
|
<el-form-item
|
||||||
|
v-if="!isStructDataSpecs && !isParams"
|
||||||
|
label="读写类型"
|
||||||
|
prop="property.accessMode"
|
||||||
|
>
|
||||||
<el-radio-group v-model="property.accessMode">
|
<el-radio-group v-model="property.accessMode">
|
||||||
<el-radio
|
<el-radio
|
||||||
v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
|
v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
|
||||||
@@ -95,79 +207,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import {
|
|
||||||
ThingModelArrayDataSpecs,
|
|
||||||
ThingModelEnumDataSpecs,
|
|
||||||
ThingModelNumberDataSpecs,
|
|
||||||
ThingModelStructDataSpecs
|
|
||||||
} from './dataSpecs'
|
|
||||||
import { ThingModelProperty, validateBoolName } from '#/api/iot/thingmodel'
|
|
||||||
import { isEmpty } from '@vben/utils'
|
|
||||||
import {
|
|
||||||
getDataTypeOptions,
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
IoTThingModelAccessModeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** IoT 物模型属性 */
|
|
||||||
defineOptions({ name: 'ThingModelProperty' })
|
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean; isParams?: boolean }>()
|
|
||||||
const emits = defineEmits(['update:modelValue'])
|
|
||||||
const property = useVModel(props, 'modelValue', emits) as Ref<ThingModelProperty>
|
|
||||||
const getDataTypeOptions2 = computed(() => {
|
|
||||||
if (!props.isStructDataSpecs) {
|
|
||||||
return getDataTypeOptions()
|
|
||||||
}
|
|
||||||
const excludedTypes = [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY]
|
|
||||||
return getDataTypeOptions().filter((item: any) => !excludedTypes.includes(item.value))
|
|
||||||
}) // 获得数据类型列表
|
|
||||||
|
|
||||||
/** 属性值的数据类型切换时初始化相关数据 */
|
|
||||||
const handleChange = (dataType: any) => {
|
|
||||||
property.value.dataSpecs = {}
|
|
||||||
property.value.dataSpecsList = []
|
|
||||||
// 不是列表型数据才设置 dataSpecs.dataType
|
|
||||||
![
|
|
||||||
IoTDataSpecsDataTypeEnum.ENUM,
|
|
||||||
IoTDataSpecsDataTypeEnum.BOOL,
|
|
||||||
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({
|
|
||||||
dataType: IoTDataSpecsDataTypeEnum.BOOL,
|
|
||||||
name: '', // 布尔值的名称
|
|
||||||
value: i // 布尔值
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 默认选中读写 */
|
|
||||||
watch(
|
|
||||||
() => property.value.accessMode,
|
|
||||||
(val: string) => {
|
|
||||||
if (props.isStructDataSpecs || props.isParams) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
isEmpty(val) && (property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value)
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
|
|||||||
@@ -1,4 +1,34 @@
|
|||||||
<!-- 产品的物模型表单(service 项) -->
|
<!-- 产品的物模型表单(service 项) -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
||||||
|
import { ThingModelService } from '#/api/iot/thingmodel';
|
||||||
|
import {
|
||||||
|
IoTThingModelParamDirectionEnum,
|
||||||
|
IoTThingModelServiceCallTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue';
|
||||||
|
|
||||||
|
/** IoT 物模型服务 */
|
||||||
|
defineOptions({ name: 'ThingModelService' });
|
||||||
|
|
||||||
|
const props = defineProps<{ isStructDataSpecs?: boolean; modelValue: any }>();
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
|
const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService>;
|
||||||
|
|
||||||
|
/** 默认选中,ASYNC 异步 */
|
||||||
|
watch(
|
||||||
|
() => service.value.callType,
|
||||||
|
(val: string) =>
|
||||||
|
isEmpty(val) &&
|
||||||
|
(service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value),
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
|
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
|
||||||
@@ -29,32 +59,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
|
|
||||||
import { useVModel } from '@vueuse/core'
|
|
||||||
import { ThingModelService } from '#/api/iot/thingmodel'
|
|
||||||
import { isEmpty } from '@vben/utils'
|
|
||||||
import {
|
|
||||||
IoTThingModelParamDirectionEnum,
|
|
||||||
IoTThingModelServiceCallTypeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** IoT 物模型服务 */
|
|
||||||
defineOptions({ name: 'ThingModelService' })
|
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>()
|
|
||||||
const emits = defineEmits(['update:modelValue'])
|
|
||||||
const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService>
|
|
||||||
|
|
||||||
/** 默认选中,ASYNC 异步 */
|
|
||||||
watch(
|
|
||||||
() => service.value.callType,
|
|
||||||
(val: string) =>
|
|
||||||
isEmpty(val) && (service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value),
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.el-form-item) {
|
:deep(.el-form-item) {
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
|
|||||||
@@ -1,3 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import hljs from 'highlight.js'; // 导入代码高亮文件
|
||||||
|
import json from 'highlight.js/lib/languages/json';
|
||||||
|
|
||||||
|
import { ProductVO } from '#/api/iot/product/product';
|
||||||
|
import { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
import 'highlight.js/styles/github.css'; // 导入代码高亮样式
|
||||||
|
|
||||||
|
defineOptions({ name: 'ThingModelTSL' });
|
||||||
|
|
||||||
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
|
const dialogTitle = ref('物模型 TSL'); // 弹窗的标题
|
||||||
|
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT); // 注入产品信息
|
||||||
|
const viewMode = ref('code'); // 查看模式:code-代码视图,editor-编辑器视图
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = () => {
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
defineExpose({ open });
|
||||||
|
|
||||||
|
/** 获取 TSL */
|
||||||
|
const thingModelTSL = ref({});
|
||||||
|
const getTsl = async () => {
|
||||||
|
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(
|
||||||
|
product?.value?.id || 0,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
|
onMounted(async () => {
|
||||||
|
// 注册代码高亮的各种语言
|
||||||
|
hljs.registerLanguage('json', json);
|
||||||
|
await getTsl();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
@@ -13,38 +52,3 @@
|
|||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import hljs from 'highlight.js' // 导入代码高亮文件
|
|
||||||
import 'highlight.js/styles/github.css' // 导入代码高亮样式
|
|
||||||
import json from 'highlight.js/lib/languages/json'
|
|
||||||
import { ThingModelApi } from '#/api/iot/thingmodel'
|
|
||||||
import { ProductVO } from '#/api/iot/product/product'
|
|
||||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
defineOptions({ name: 'ThingModelTSL' })
|
|
||||||
|
|
||||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
|
||||||
const dialogTitle = ref('物模型 TSL') // 弹窗的标题
|
|
||||||
const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 注入产品信息
|
|
||||||
const viewMode = ref('code') // 查看模式:code-代码视图,editor-编辑器视图
|
|
||||||
|
|
||||||
/** 打开弹窗 */
|
|
||||||
const open = () => {
|
|
||||||
dialogVisible.value = true
|
|
||||||
}
|
|
||||||
defineExpose({ open })
|
|
||||||
|
|
||||||
/** 获取 TSL */
|
|
||||||
const thingModelTSL = ref({})
|
|
||||||
const getTsl = async () => {
|
|
||||||
thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product?.value?.id || 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 初始化 **/
|
|
||||||
onMounted(async () => {
|
|
||||||
// 注册代码高亮的各种语言
|
|
||||||
hljs.registerLanguage('json', json)
|
|
||||||
await getTsl()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { ThingModelData } from '#/api/iot/thingmodel';
|
||||||
|
import {
|
||||||
|
getEventTypeLabel,
|
||||||
|
getThingModelServiceCallTypeLabel,
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '#/views/iot/utils/constants';
|
||||||
|
|
||||||
|
/** 数据定义展示组件 */
|
||||||
|
defineOptions({ name: 'DataDefinition' });
|
||||||
|
|
||||||
|
defineProps<{ data: ThingModelData }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- 属性 -->
|
<!-- 属性 -->
|
||||||
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY">
|
<template v-if="data.type === IoTThingModelTypeEnum.PROPERTY">
|
||||||
@@ -7,11 +22,13 @@
|
|||||||
[
|
[
|
||||||
IoTDataSpecsDataTypeEnum.INT,
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
].includes(data.property.dataType)
|
].includes(data.property.dataType)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
取值范围:{{ `${data.property.dataSpecs.min}~${data.property.dataSpecs.max}` }}
|
取值范围:{{
|
||||||
|
`${data.property.dataSpecs.min}~${data.property.dataSpecs.max}`
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<!-- 非列表型:文本 -->
|
<!-- 非列表型:文本 -->
|
||||||
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property.dataType">
|
<div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property.dataType">
|
||||||
@@ -23,7 +40,7 @@
|
|||||||
[
|
[
|
||||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||||
IoTDataSpecsDataTypeEnum.DATE
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
].includes(data.property.dataType)
|
].includes(data.property.dataType)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -33,12 +50,16 @@
|
|||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
[IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
|
[IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
|
||||||
data.property.dataType
|
data.property.dataType,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{{ IoTDataSpecsDataTypeEnum.BOOL === data.property.dataType ? '布尔值' : '枚举值' }}:
|
{{
|
||||||
|
IoTDataSpecsDataTypeEnum.BOOL === data.property.dataType
|
||||||
|
? '布尔值'
|
||||||
|
: '枚举值'
|
||||||
|
}}:
|
||||||
</div>
|
</div>
|
||||||
<div v-for="item in data.property.dataSpecsList" :key="item.value">
|
<div v-for="item in data.property.dataSpecsList" :key="item.value">
|
||||||
{{ `${item.name}-${item.value}` }}
|
{{ `${item.name}-${item.value}` }}
|
||||||
@@ -55,19 +76,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { ThingModelData } from '#/api/iot/thingmodel'
|
|
||||||
import {
|
|
||||||
getEventTypeLabel,
|
|
||||||
getThingModelServiceCallTypeLabel,
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
IoTThingModelTypeEnum
|
|
||||||
} from '#/views/iot/utils/constants'
|
|
||||||
|
|
||||||
/** 数据定义展示组件 */
|
|
||||||
defineOptions({ name: 'DataDefinition' })
|
|
||||||
|
|
||||||
defineProps<{ data: ThingModelData }>()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user