feat(iot): 优化 iot 的代码风格(迁移 constants.ts)地址
parent
e816288b82
commit
3f09fc1498
|
|
@ -11,6 +11,7 @@ export namespace RuleSceneApi {
|
||||||
status?: number;
|
status?: number;
|
||||||
triggers?: Trigger[];
|
triggers?: Trigger[];
|
||||||
actions?: Action[];
|
actions?: Action[];
|
||||||
|
lastTriggeredTime?: Date;
|
||||||
createTime?: Date;
|
createTime?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export namespace IotStatisticsApi {
|
||||||
|
|
||||||
/** 设备消息数量统计(按日期) */
|
/** 设备消息数量统计(按日期) */
|
||||||
export interface DeviceMessageSummaryByDateRespVO {
|
export interface DeviceMessageSummaryByDateRespVO {
|
||||||
time: Date; // 时间轴
|
time: string; // 时间轴
|
||||||
upstreamCount: number; // 上行消息数量
|
upstreamCount: number; // 上行消息数量
|
||||||
downstreamCount: number; // 下行消息数量
|
downstreamCount: number; // 下行消息数量
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,15 @@ export namespace ThingModelApi {
|
||||||
dataSpecsList?: any[];
|
dataSpecsList?: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** IoT 物模型 TSL(树形)响应 */
|
||||||
|
export interface ThingModelTSL {
|
||||||
|
productId?: number;
|
||||||
|
productKey?: string;
|
||||||
|
properties?: Property[];
|
||||||
|
events?: Event[];
|
||||||
|
services?: Service[];
|
||||||
|
}
|
||||||
|
|
||||||
/** IoT 数据定义(数值型) */
|
/** IoT 数据定义(数值型) */
|
||||||
export interface DataSpecsNumberData {
|
export interface DataSpecsNumberData {
|
||||||
min?: number | string;
|
min?: number | string;
|
||||||
|
|
@ -236,7 +245,10 @@ export function deleteThingModel(id: number) {
|
||||||
|
|
||||||
/** 获取物模型 TSL */
|
/** 获取物模型 TSL */
|
||||||
export function getThingModelTSL(productId: number) {
|
export function getThingModelTSL(productId: number) {
|
||||||
return requestClient.get<any>('/iot/thing-model/get-tsl', {
|
return requestClient.get<ThingModelApi.ThingModelTSL>(
|
||||||
|
'/iot/thing-model/get-tsl',
|
||||||
|
{
|
||||||
params: { productId },
|
params: { productId },
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,20 +115,20 @@ export function useGridColumns(): VxeTableGridOptions<AlertRecordApi.AlertRecord
|
||||||
props: { type: DICT_TYPE.IOT_ALERT_LEVEL },
|
props: { type: DICT_TYPE.IOT_ALERT_LEVEL },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO @AI:非必要,不缩写;product、device
|
|
||||||
{
|
{
|
||||||
field: 'productId',
|
field: 'productId',
|
||||||
title: '产品名称',
|
title: '产品名称',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
formatter: ({ cellValue }) =>
|
formatter: ({ cellValue }) =>
|
||||||
productList.find((p) => p.id === cellValue)?.name || '-',
|
productList.find((product) => product.id === cellValue)?.name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceId',
|
field: 'deviceId',
|
||||||
title: '设备名称',
|
title: '设备名称',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
formatter: ({ cellValue }) =>
|
formatter: ({ cellValue }) =>
|
||||||
deviceList.find((d) => d.id === cellValue)?.deviceName || '-',
|
deviceList.find((device) => device.id === cellValue)?.deviceName ||
|
||||||
|
'-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceMessage',
|
field: 'deviceMessage',
|
||||||
|
|
|
||||||
|
|
@ -24,16 +24,13 @@ const props = defineProps<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/** 查询参数 */
|
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
method: undefined,
|
method: undefined,
|
||||||
upstream: undefined,
|
upstream: undefined,
|
||||||
});
|
}); // 查询参数
|
||||||
|
|
||||||
/** 自动刷新开关 */
|
const autoRefresh = ref(false); // 自动刷新开关
|
||||||
const autoRefresh = ref(false);
|
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||||
/** 自动刷新定时器 */
|
|
||||||
let autoRefreshTimer: any = null;
|
|
||||||
|
|
||||||
/** 消息方法选项 */
|
/** 消息方法选项 */
|
||||||
const methodOptions = computed(() => {
|
const methodOptions = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ import type { IotDeviceModbusConfigApi } from '#/api/iot/device/modbus/config';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
ModbusFrameFormatEnum,
|
||||||
|
ModbusModeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
@ -14,10 +19,6 @@ import { useVbenForm, z } from '#/adapter/form';
|
||||||
import { saveModbusConfig } from '#/api/iot/device/modbus/config';
|
import { saveModbusConfig } from '#/api/iot/device/modbus/config';
|
||||||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import {
|
|
||||||
ModbusFrameFormatEnum,
|
|
||||||
ModbusModeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import type { DescriptionItemSchema } from '#/components/description';
|
||||||
import { computed, h, onMounted, ref } from 'vue';
|
import { computed, h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, ModbusFunctionCodeOptions } from '@vben/constants';
|
||||||
|
|
||||||
import { Button, message } from 'ant-design-vue';
|
import { Button, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
|
@ -25,7 +25,6 @@ import {
|
||||||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||||
import { useDescription } from '#/components/description';
|
import { useDescription } from '#/components/description';
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { DictTag } from '#/components/dict-tag';
|
||||||
import { ModbusFunctionCodeOptions } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import DeviceModbusConfigForm from './modbus-config-form.vue';
|
import DeviceModbusConfigForm from './modbus-config-form.vue';
|
||||||
import DeviceModbusPointForm from './modbus-point-form.vue';
|
import DeviceModbusPointForm from './modbus-point-form.vue';
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,14 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, h, ref } from 'vue';
|
import { computed, h, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
getByteOrderOptions,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
ModbusFunctionCodeOptions,
|
||||||
|
ModbusRawDataTypeOptions,
|
||||||
|
} from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
@ -19,12 +26,6 @@ import {
|
||||||
updateModbusPoint,
|
updateModbusPoint,
|
||||||
} from '#/api/iot/device/modbus/point';
|
} from '#/api/iot/device/modbus/point';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import {
|
|
||||||
getByteOrderOptions,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
ModbusFunctionCodeOptions,
|
|
||||||
ModbusRawDataTypeOptions,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { ContentWrap } from '@vben/common-ui';
|
import { ContentWrap } from '@vben/common-ui';
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import {
|
||||||
|
DeviceStateEnum,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -25,10 +29,6 @@ import {
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { sendDeviceMessage } from '#/api/iot/device/device';
|
import { sendDeviceMessage } from '#/api/iot/device/device';
|
||||||
import {
|
|
||||||
DeviceStateEnum,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import DataDefinition from '../../../../thingmodel/modules/components/data-definition.vue';
|
import DataDefinition from '../../../../thingmodel/modules/components/data-definition.vue';
|
||||||
import DeviceDetailsMessage from './message.vue';
|
import DeviceDetailsMessage from './message.vue';
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, onMounted, reactive, watch } from 'vue';
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import {
|
||||||
|
getEventTypeLabel,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatDateTime } from '@vben/utils';
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
|
@ -14,10 +18,6 @@ import { Button, RangePicker, Select, Space, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||||
import {
|
|
||||||
getEventTypeLabel,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@ import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
import { formatDate, formatDateTime } from '@vben/utils';
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -26,13 +27,11 @@ import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
||||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
defineProps<{ deviceId: number }>();
|
defineProps<{ deviceId: number }>();
|
||||||
|
|
||||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const exporting = ref(false);
|
|
||||||
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
||||||
const total = ref(0); // 总数据量
|
const total = ref(0); // 总数据量
|
||||||
|
|
@ -330,54 +329,6 @@ function handleRefresh() {
|
||||||
getList();
|
getList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 导出数据 */
|
|
||||||
async function handleExport() {
|
|
||||||
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,
|
|
||||||
formatDateTime(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 关闭弹窗 */
|
/** 关闭弹窗 */
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
|
|
@ -429,18 +380,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- 导出按钮 -->
|
|
||||||
<Button
|
|
||||||
:disabled="list.length === 0"
|
|
||||||
:loading="exporting"
|
|
||||||
@click="handleExport"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<IconifyIcon icon="ant-design:export-outlined" />
|
|
||||||
</template>
|
|
||||||
导出
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- 视图切换 -->
|
<!-- 视图切换 -->
|
||||||
<Button.Group class="ml-auto">
|
<Button.Group class="ml-auto">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, onMounted, reactive, watch } from 'vue';
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import {
|
||||||
|
getThingModelServiceCallTypeLabel,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatDateTime } from '@vben/utils';
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
|
@ -14,10 +18,6 @@ import { Button, RangePicker, Select, Space, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||||
import {
|
|
||||||
getThingModelServiceCallTypeLabel,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="device-card-view">
|
<div>
|
||||||
<!-- 设备卡片列表 -->
|
<!-- 设备卡片列表 -->
|
||||||
<div v-loading="loading" class="min-h-96">
|
<div v-loading="loading" class="min-h-96">
|
||||||
<Row v-if="list.length > 0" :gutter="[16, 16]">
|
<Row v-if="list.length > 0" :gutter="[16, 16]">
|
||||||
|
|
@ -118,30 +118,43 @@ onMounted(() => {
|
||||||
:lg="6"
|
:lg="6"
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
:body-style="{ padding: '16px' }"
|
:body-style="{
|
||||||
class="device-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
padding: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
}"
|
||||||
|
class="h-full overflow-hidden rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<!-- 顶部标题区域 -->
|
<!-- 顶部标题区域 -->
|
||||||
<div class="mb-3 flex items-center">
|
<div class="mb-3 flex items-center">
|
||||||
<div class="device-icon">
|
<div
|
||||||
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
|
>
|
||||||
<IconifyIcon icon="mdi:chip" class="text-xl" />
|
<IconifyIcon icon="mdi:chip" class="text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 min-w-0 flex-1">
|
<div class="ml-3 min-w-0 flex-1">
|
||||||
<div class="device-title">{{ item.deviceName }}</div>
|
<div
|
||||||
|
class="truncate text-[15px] font-semibold leading-9 dark:text-white/85"
|
||||||
|
>
|
||||||
|
{{ item.deviceName }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DictTag
|
<DictTag
|
||||||
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
||||||
:value="item.state"
|
:value="item.state"
|
||||||
class="status-tag"
|
class="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="mb-3 flex items-start">
|
<div class="mb-3 flex items-start">
|
||||||
<div class="info-list flex-1">
|
<div class="flex-1">
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">所属产品</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
所属产品
|
||||||
|
</span>
|
||||||
<a
|
<a
|
||||||
class="info-value cursor-pointer text-primary"
|
class="cursor-pointer truncate font-medium text-primary"
|
||||||
@click="
|
@click="
|
||||||
(e) => {
|
(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -152,25 +165,33 @@ onMounted(() => {
|
||||||
{{ getProductName(item.productId) }}
|
{{ getProductName(item.productId) }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">设备类型</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
设备类型
|
||||||
|
</span>
|
||||||
<DictTag
|
<DictTag
|
||||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||||
:value="item.deviceType"
|
:value="item.deviceType"
|
||||||
class="info-tag m-0"
|
class="m-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="flex items-center text-[13px]">
|
||||||
<span class="info-label">Deviceid</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
Deviceid
|
||||||
|
</span>
|
||||||
<Tooltip :title="String(item.id)" placement="top">
|
<Tooltip :title="String(item.id)" placement="top">
|
||||||
<span class="info-value device-id cursor-pointer">
|
<span
|
||||||
|
class="inline-block max-w-[150px] cursor-pointer truncate align-middle font-mono text-xs opacity-85 dark:text-white/75"
|
||||||
|
>
|
||||||
{{ item.id }}
|
{{ item.id }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 设备图片 -->
|
<!-- 设备图片 -->
|
||||||
<div class="device-image">
|
<div
|
||||||
|
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
:src="item.picUrl"
|
:src="item.picUrl"
|
||||||
|
|
@ -185,11 +206,13 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按钮组 -->
|
<!-- 按钮组 -->
|
||||||
<div class="action-buttons">
|
<div
|
||||||
|
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
v-if="hasAccessByCodes(['iot:device:update'])"
|
v-if="hasAccessByCodes(['iot:device:update'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-edit"
|
class="!h-8 flex-1 rounded-md !border-[#1890ff] !text-[13px] !text-[#1890ff] transition-all duration-200 hover:!bg-[#1890ff] hover:!text-white"
|
||||||
@click="emit('edit', item)"
|
@click="emit('edit', item)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||||
|
|
@ -198,7 +221,7 @@ onMounted(() => {
|
||||||
<Button
|
<Button
|
||||||
v-if="hasAccessByCodes(['iot:device:query'])"
|
v-if="hasAccessByCodes(['iot:device:query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-detail"
|
class="!h-8 flex-1 rounded-md !border-[#52c41a] !text-[13px] !text-[#52c41a] transition-all duration-200 hover:!bg-[#52c41a] hover:!text-white"
|
||||||
@click="emit('detail', item.id!)"
|
@click="emit('detail', item.id!)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||||
|
|
@ -207,7 +230,7 @@ onMounted(() => {
|
||||||
<Button
|
<Button
|
||||||
v-if="hasAccessByCodes(['iot:device:message-query'])"
|
v-if="hasAccessByCodes(['iot:device:message-query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-data"
|
class="!h-8 flex-1 rounded-md !border-[#fa8c16] !text-[13px] !text-[#fa8c16] transition-all duration-200 hover:!bg-[#fa8c16] hover:!text-white"
|
||||||
@click="emit('model', item.id!)"
|
@click="emit('model', item.id!)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:database" class="mr-1" />
|
<IconifyIcon icon="lucide:database" class="mr-1" />
|
||||||
|
|
@ -221,7 +244,7 @@ onMounted(() => {
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="h-8 rounded-md p-0 text-[13px] transition-all duration-200 !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -249,187 +272,3 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.device-card-view {
|
|
||||||
.device-card {
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.ant-card-body) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备图标
|
|
||||||
.device-icon {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备标题
|
|
||||||
.device-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 36px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态标签
|
|
||||||
.status-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备图片
|
|
||||||
.device-image {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
color: #1890ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 信息列表
|
|
||||||
.info-list {
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 8px;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.text-primary {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-id {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮组
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 12px;
|
|
||||||
margin-top: auto;
|
|
||||||
border-top: 1px solid var(--ant-color-split);
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
flex: 1;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&.action-btn-edit {
|
|
||||||
color: #1890ff;
|
|
||||||
border-color: #1890ff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-detail {
|
|
||||||
color: #52c41a;
|
|
||||||
border-color: #52c41a;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #52c41a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-data {
|
|
||||||
color: #fa8c16;
|
|
||||||
border-color: #fa8c16;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #fa8c16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-delete {
|
|
||||||
flex: 0 0 32px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 夜间模式适配
|
|
||||||
html.dark {
|
|
||||||
.device-card-view {
|
|
||||||
.device-card {
|
|
||||||
.device-title {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-list {
|
|
||||||
.info-label {
|
|
||||||
color: rgb(255 255 255 / 65%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-id {
|
|
||||||
color: rgb(255 255 255 / 75%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-image {
|
|
||||||
color: #69c0ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,7 @@ export function getMessageTrendChartOptions(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 设备状态仪表盘图表配置 */
|
||||||
* 设备状态仪表盘图表配置
|
|
||||||
*/
|
|
||||||
export function getDeviceStateGaugeChartOptions(
|
export function getDeviceStateGaugeChartOptions(
|
||||||
value: number,
|
value: number,
|
||||||
max: number,
|
max: number,
|
||||||
|
|
@ -129,9 +127,7 @@ export function getDeviceStateGaugeChartOptions(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 设备数量饼图配置 */
|
||||||
* 设备数量饼图配置
|
|
||||||
*/
|
|
||||||
export function getDeviceCountPieChartOptions(
|
export function getDeviceCountPieChartOptions(
|
||||||
data: Array<{ name: string; value: number }>,
|
data: Array<{ name: string; value: number }>,
|
||||||
): any {
|
): any {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ async function loadData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 初始化 */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,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(
|
const categories = Object.entries(
|
||||||
props.statsData.productCategoryDeviceCounts || {},
|
props.statsData.productCategoryDeviceCounts || {},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictLabel } from '@vben/hooks';
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
|
||||||
import { Card, Empty, Spin } from 'ant-design-vue';
|
import { Card, Empty, Spin } from 'ant-design-vue';
|
||||||
|
|
||||||
import { getDeviceLocationList } from '#/api/iot/device/device';
|
import { getDeviceLocationList } from '#/api/iot/device/device';
|
||||||
import { loadBaiduMapSdk } from '#/components/map';
|
import { loadBaiduMapSdk } from '#/components/map';
|
||||||
import { DeviceStateEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceMapCard' });
|
defineOptions({ name: 'DeviceMapCard' });
|
||||||
|
|
||||||
|
|
@ -152,6 +151,8 @@ async function init() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await loadBaiduMapSdk();
|
await loadBaiduMapSdk();
|
||||||
|
// 等待 v-show 容器渲染完成;SDK 缓存命中时上一行会同步 resolve,DOM 来不及切换
|
||||||
|
await nextTick();
|
||||||
initMap();
|
initMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
import { useDetailSchema } from '../../data';
|
import { useDetailSchema } from '../../data';
|
||||||
|
|
||||||
/** IoT OTA 固件基本信息 */
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
firmware?: IoTOtaFirmwareApi.Firmware;
|
firmware?: IoTOtaFirmwareApi.Firmware;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,10 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { DescriptionItemSchema } from '#/components/description';
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, IoTOtaTaskDeviceScopeEnum } from '@vben/constants';
|
||||||
import { getDictLabel, getDictOptions } from '@vben/hooks';
|
import { getDictLabel, getDictOptions } from '@vben/hooks';
|
||||||
import { formatDateTime } from '@vben/utils';
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
/** 任务详情的描述字段 */
|
/** 任务详情的描述字段 */
|
||||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { IoTOtaTaskStatusEnum } from '@vben/constants';
|
||||||
|
|
||||||
import { Input, message } from 'ant-design-vue';
|
import { Input, message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { cancelOtaTask, getOtaTaskPage } from '#/api/iot/ota/task';
|
import { cancelOtaTask, getOtaTaskPage } from '#/api/iot/ota/task';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import { useGridColumns } from '../data';
|
import { useGridColumns } from '../data';
|
||||||
import OtaTaskDetail from './detail.vue';
|
import OtaTaskDetail from './detail.vue';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||||
import { getDictLabel } from '@vben/hooks';
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
|
||||||
import { Card, Col, Row } from 'ant-design-vue';
|
import { Card, Col, Row } from 'ant-design-vue';
|
||||||
|
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
statistics: Record<string, number>;
|
statistics: Record<string, number>;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import type { IoTOtaTaskRecordApi } from '#/api/iot/ota/task/record';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||||
|
|
||||||
import { Card, message, Tabs } from 'ant-design-vue';
|
import { Card, message, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
|
@ -12,7 +14,6 @@ import {
|
||||||
getOtaTaskRecordPage,
|
getOtaTaskRecordPage,
|
||||||
} from '#/api/iot/ota/task/record';
|
} from '#/api/iot/ota/task/record';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import { useGridColumns } from '../data';
|
import { useGridColumns } from '../data';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import { onMounted, provide, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||||
|
|
||||||
import { message, Tabs } from 'ant-design-vue';
|
import { message, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
import { getDeviceCount } from '#/api/iot/device/device';
|
import { getDeviceCount } from '#/api/iot/device/device';
|
||||||
import { getProduct } from '#/api/iot/product/product';
|
import { getProduct } from '#/api/iot/product/product';
|
||||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ProductDetailsHeader from './modules/header.vue';
|
import ProductDetailsHeader from './modules/header.vue';
|
||||||
import ProductDetailsInfo from './modules/info.vue';
|
import ProductDetailsInfo from './modules/info.vue';
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const showProductSecret = ref(false); // 是否显示产品密钥
|
||||||
|
|
||||||
/** 格式化日期 */
|
/** 格式化日期 */
|
||||||
function formatDate(date?: Date | string) {
|
function formatDate(date?: Date | string) {
|
||||||
if (!date) return '-';
|
if (!date) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
return new Date(date).toLocaleString('zh-CN');
|
return new Date(date).toLocaleString('zh-CN');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="product-card-view">
|
<div>
|
||||||
<!-- 产品卡片列表 -->
|
<!-- 产品卡片列表 -->
|
||||||
<div v-loading="loading" class="min-h-96">
|
<div v-loading="loading" class="min-h-96">
|
||||||
<Row v-if="list.length > 0" :gutter="[16, 16]">
|
<Row v-if="list.length > 0" :gutter="[16, 16]">
|
||||||
|
|
@ -104,49 +104,70 @@ onMounted(() => {
|
||||||
:lg="6"
|
:lg="6"
|
||||||
>
|
>
|
||||||
<Card
|
<Card
|
||||||
:body-style="{ padding: '16px' }"
|
:body-style="{
|
||||||
class="product-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
padding: '16px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
}"
|
||||||
|
class="h-full overflow-hidden rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<!-- 顶部标题区域 -->
|
<!-- 顶部标题区域 -->
|
||||||
<div class="mb-3 flex items-center">
|
<div class="mb-3 flex items-center">
|
||||||
<div class="product-icon">
|
<div
|
||||||
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
:icon="item.icon || 'lucide:box'"
|
:icon="item.icon || 'lucide:box'"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 min-w-0 flex-1">
|
<div class="ml-3 min-w-0 flex-1">
|
||||||
<div class="product-title">{{ item.name }}</div>
|
<div
|
||||||
|
class="truncate text-[15px] font-semibold leading-9 dark:text-white/85"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="mb-3 flex items-start">
|
<div class="mb-3 flex items-start">
|
||||||
<div class="info-list flex-1">
|
<div class="flex-1">
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">产品分类</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
<span class="info-value text-primary">
|
产品分类
|
||||||
|
</span>
|
||||||
|
<span class="truncate font-medium text-primary">
|
||||||
{{ getCategoryName(item.categoryId) }}
|
{{ getCategoryName(item.categoryId) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">产品类型</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
产品类型
|
||||||
|
</span>
|
||||||
<DictTag
|
<DictTag
|
||||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||||
:value="item.deviceType"
|
:value="item.deviceType"
|
||||||
class="info-tag m-0"
|
class="m-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="flex items-center text-[13px]">
|
||||||
<span class="info-label">产品标识</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
产品标识
|
||||||
|
</span>
|
||||||
<Tooltip :title="item.productKey || item.id" placement="top">
|
<Tooltip :title="item.productKey || item.id" placement="top">
|
||||||
<span class="info-value product-key cursor-pointer">
|
<span
|
||||||
|
class="inline-block max-w-[150px] cursor-pointer truncate align-middle font-mono text-xs opacity-85 dark:text-white/75"
|
||||||
|
>
|
||||||
{{ item.productKey || item.id }}
|
{{ item.productKey || item.id }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 产品图片 -->
|
<!-- 产品图片 -->
|
||||||
<div class="product-image">
|
<div
|
||||||
|
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
:src="item.picUrl"
|
:src="item.picUrl"
|
||||||
|
|
@ -161,11 +182,13 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按钮组 -->
|
<!-- 按钮组 -->
|
||||||
<div class="action-buttons">
|
<div
|
||||||
|
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
v-if="hasAccessByCodes(['iot:product:update'])"
|
v-if="hasAccessByCodes(['iot:product:update'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-edit"
|
class="!h-8 flex-1 rounded-md !border-[#1890ff] !text-[13px] !text-[#1890ff] transition-all duration-200 hover:!bg-[#1890ff] hover:!text-white"
|
||||||
@click="emit('edit', item)"
|
@click="emit('edit', item)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||||
|
|
@ -174,7 +197,7 @@ onMounted(() => {
|
||||||
<Button
|
<Button
|
||||||
v-if="hasAccessByCodes(['iot:product:query'])"
|
v-if="hasAccessByCodes(['iot:product:query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-detail"
|
class="!h-8 flex-1 rounded-md !border-[#52c41a] !text-[13px] !text-[#52c41a] transition-all duration-200 hover:!bg-[#52c41a] hover:!text-white"
|
||||||
@click="emit('detail', item.id)"
|
@click="emit('detail', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||||
|
|
@ -183,7 +206,7 @@ onMounted(() => {
|
||||||
<Button
|
<Button
|
||||||
v-if="hasAccessByCodes(['iot:thing-model:query'])"
|
v-if="hasAccessByCodes(['iot:thing-model:query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-model"
|
class="!h-8 flex-1 rounded-md !border-[#fa8c16] !text-[13px] !text-[#fa8c16] transition-all duration-200 hover:!bg-[#fa8c16] hover:!text-white"
|
||||||
@click="emit('thingModel', item.id)"
|
@click="emit('thingModel', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
||||||
|
|
@ -198,7 +221,7 @@ onMounted(() => {
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
disabled
|
disabled
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="h-8 rounded-md p-0 text-[13px] transition-all duration-200 !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -211,7 +234,7 @@ onMounted(() => {
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="h-8 rounded-md p-0 text-[13px] transition-all duration-200 !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -240,182 +263,3 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.product-card-view {
|
|
||||||
.product-card {
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.ant-card-body) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 产品图标
|
|
||||||
.product-icon {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 产品标题
|
|
||||||
.product-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 36px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 信息列表
|
|
||||||
.info-list {
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 8px;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.text-primary {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-key {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 产品图片
|
|
||||||
.product-image {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
color: #1890ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮组
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 12px;
|
|
||||||
margin-top: auto;
|
|
||||||
border-top: 1px solid var(--ant-color-split);
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
flex: 1;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&.action-btn-edit {
|
|
||||||
color: #1890ff;
|
|
||||||
border-color: #1890ff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-detail {
|
|
||||||
color: #52c41a;
|
|
||||||
border-color: #52c41a;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #52c41a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-model {
|
|
||||||
color: #fa8c16;
|
|
||||||
border-color: #fa8c16;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #fa8c16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-delete {
|
|
||||||
flex: 0 0 32px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 夜间模式适配
|
|
||||||
html.dark {
|
|
||||||
.product-card-view {
|
|
||||||
.product-card {
|
|
||||||
.product-title {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-list {
|
|
||||||
.info-label {
|
|
||||||
color: rgb(255 255 255 / 65%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-key {
|
|
||||||
color: rgb(255 255 255 / 75%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image {
|
|
||||||
color: #69c0ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { Button, message, Select } from 'ant-design-vue';
|
import { Button, message, Select } from 'ant-design-vue';
|
||||||
|
|
@ -10,7 +10,6 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
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';
|
||||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||||
import { IoTThingModelTypeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import { useSourceConfigColumns } from '../data';
|
import { useSourceConfigColumns } from '../data';
|
||||||
|
|
||||||
|
|
@ -184,13 +183,12 @@ async function setData(data: any[]) {
|
||||||
...item,
|
...item,
|
||||||
identifierLoading: false,
|
identifierLoading: false,
|
||||||
}));
|
}));
|
||||||
// 为已有数据预加载物模型
|
// 为已有数据预加载物模型;并行加载,不阻塞 reloadGrid
|
||||||
// TODO @AI:这里有 linter 报错:Promise returned from forEach argument is ignored
|
await Promise.all(
|
||||||
data?.forEach(async (item) => {
|
(data || [])
|
||||||
if (item.productId && shouldShowIdentifierSelect(item)) {
|
.filter((item) => item.productId && shouldShowIdentifierSelect(item))
|
||||||
await loadThingModel(item.productId);
|
.map((item) => loadThingModel(item.productId)),
|
||||||
}
|
);
|
||||||
});
|
|
||||||
await reloadGrid();
|
await reloadGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,7 +272,7 @@ defineExpose({ validate, getData, setData });
|
||||||
:options="getThingModelOptions(formData[rowIndex])"
|
:options="getThingModelOptions(formData[rowIndex])"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<span v-else class="text-xs text-secondary">-</span>
|
<span v-else class="text-xs text-muted-foreground">-</span>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ rowIndex }">
|
<template #actions="{ rowIndex }">
|
||||||
<Button danger type="link" @click="handleDelete(rowIndex)">删除</Button>
|
<Button danger type="link" @click="handleDelete(rowIndex)">删除</Button>
|
||||||
|
|
|
||||||
|
|
@ -92,39 +92,42 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||||
{
|
{
|
||||||
field: 'name',
|
field: 'name',
|
||||||
title: '规则名称',
|
title: '规则名称',
|
||||||
minWidth: 180,
|
minWidth: 200,
|
||||||
|
align: 'left',
|
||||||
|
showOverflow: false,
|
||||||
slots: { default: 'name' },
|
slots: { default: 'name' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'triggers',
|
field: 'triggers',
|
||||||
title: '触发条件',
|
title: '触发条件',
|
||||||
minWidth: 260,
|
minWidth: 280,
|
||||||
|
align: 'left',
|
||||||
|
showOverflow: false,
|
||||||
slots: { default: 'triggers' },
|
slots: { default: 'triggers' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
title: '执行动作',
|
title: '执行动作',
|
||||||
minWidth: 220,
|
minWidth: 250,
|
||||||
|
align: 'left',
|
||||||
|
showOverflow: false,
|
||||||
slots: { default: 'actionsCol' },
|
slots: { default: 'actionsCol' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'status',
|
field: 'lastTriggeredTime',
|
||||||
title: '规则状态',
|
title: '最近触发',
|
||||||
width: 90,
|
width: 180,
|
||||||
cellRender: {
|
slots: { default: 'lastTriggeredTime' },
|
||||||
name: 'CellDict',
|
|
||||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'createTime',
|
field: 'createTime',
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
width: 160,
|
width: 180,
|
||||||
formatter: 'formatDateTime',
|
formatter: 'formatDateTime',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
width: 200,
|
width: 210,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
slots: { default: 'actions' },
|
slots: { default: 'actions' },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,8 @@ onMounted(() => {
|
||||||
<Select
|
<Select
|
||||||
v-model:value="localValue"
|
v-model:value="localValue"
|
||||||
placeholder="请选择告警配置"
|
placeholder="请选择告警配置"
|
||||||
filterable
|
show-search
|
||||||
clearable
|
allow-clear
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { Col, Form, Row, Select } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getConditionTypeOptions,
|
getConditionTypeOptions,
|
||||||
IoTDeviceStatusEnum,
|
IoTDeviceStatusEnum,
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
IotRuleSceneTriggerConditionTypeEnum,
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Col, Form, Row, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import ValueInput from '../inputs/value-input.vue';
|
import ValueInput from '../inputs/value-input.vue';
|
||||||
import DeviceSelector from '../selectors/device-selector.vue';
|
import DeviceSelector from '../selectors/device-selector.vue';
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IotRuleSceneTriggerTimeOperatorEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
|
|
@ -17,8 +18,6 @@ import {
|
||||||
TimePicker,
|
TimePicker,
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
|
|
||||||
import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
/** 当前时间条件配置组件 */
|
/** 当前时间条件配置组件 */
|
||||||
defineOptions({ name: 'CurrentTimeConditionConfig' });
|
defineOptions({ name: 'CurrentTimeConditionConfig' });
|
||||||
|
|
||||||
|
|
@ -225,7 +224,7 @@ watch(
|
||||||
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-sm text-secondary">无需设置时间值</div>
|
<div v-else class="text-sm text-muted-foreground">无需设置时间值</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IotRuleSceneActionTypeEnum,
|
||||||
|
IoTThingModelAccessModeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { isObject } from '@vben/utils';
|
import { isObject } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Col, Form, Row, Select, Tag } from 'ant-design-vue';
|
import { Col, Form, Row, Select, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
import { getThingModelTSL } from '#/api/iot/thingmodel';
|
||||||
import {
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
IotRuleSceneActionTypeEnum,
|
|
||||||
IoTThingModelAccessModeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import JsonParamsInput from '../inputs/json-params-input.vue';
|
import JsonParamsInput from '../inputs/json-params-input.vue';
|
||||||
import DeviceSelector from '../selectors/device-selector.vue';
|
import DeviceSelector from '../selectors/device-selector.vue';
|
||||||
|
|
@ -133,18 +133,13 @@ function handleServiceChange(serviceIdentifier?: any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 获取物模型 TSL 数据 */
|
||||||
* 获取物模型TSL数据
|
async function fetchThingModelTSL(productId: number) {
|
||||||
* @param productId 产品ID
|
|
||||||
* @returns 物模型TSL数据
|
|
||||||
*/
|
|
||||||
async function getThingModelTSL(productId: number): Promise<any> {
|
|
||||||
if (!productId) return null;
|
if (!productId) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await getThingModelListByProductId(productId);
|
return await getThingModelTSL(productId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取物模型TSL数据失败:', error);
|
console.error('获取物模型 TSL 数据失败:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +156,7 @@ async function loadThingModelProperties(productId: number) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loadingThingModel.value = true;
|
loadingThingModel.value = true;
|
||||||
const tslData = await getThingModelTSL(productId);
|
const tslData = await fetchThingModelTSL(productId);
|
||||||
|
|
||||||
// TODO DONE @AI:这里有 linter 报错
|
// TODO DONE @AI:这里有 linter 报错
|
||||||
if (!tslData?.properties) {
|
if (!tslData?.properties) {
|
||||||
|
|
@ -196,7 +191,7 @@ async function loadServiceList(productId: number) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loadingServices.value = true;
|
loadingServices.value = true;
|
||||||
const tslData = await getThingModelTSL(productId);
|
const tslData = await fetchThingModelTSL(productId);
|
||||||
|
|
||||||
// TODO DONE @AI:这里有 linter 报错
|
// TODO DONE @AI:这里有 linter 报错
|
||||||
if (!tslData?.services) {
|
if (!tslData?.services) {
|
||||||
|
|
@ -368,8 +363,8 @@ watch(
|
||||||
<Select
|
<Select
|
||||||
v-model:value="action.identifier"
|
v-model:value="action.identifier"
|
||||||
placeholder="请选择服务"
|
placeholder="请选择服务"
|
||||||
filterable
|
show-search
|
||||||
clearable
|
allow-clear
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:loading="loadingServices"
|
:loading="loadingServices"
|
||||||
@change="handleServiceChange"
|
@change="handleServiceChange"
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@ function removeConditionGroup() {
|
||||||
<MainConditionInnerConfig
|
<MainConditionInnerConfig
|
||||||
:model-value="trigger"
|
:model-value="trigger"
|
||||||
@update:model-value="updateCondition"
|
@update:model-value="updateCondition"
|
||||||
:trigger-type="(trigger.type as number)"
|
:trigger-type="trigger.type as number"
|
||||||
@trigger-type-change="handleTriggerTypeChange"
|
@trigger-type-change="handleTriggerTypeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,7 +228,7 @@ function removeConditionGroup() {
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(value) => updateSubGroup(subGroupIndex, value)
|
(value) => updateSubGroup(subGroupIndex, value)
|
||||||
"
|
"
|
||||||
:trigger-type="(trigger.type as number)"
|
:trigger-type="trigger.type as number"
|
||||||
:max-conditions="maxConditionsPerGroup"
|
:max-conditions="maxConditionsPerGroup"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,16 @@ import type { Trigger } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { Col, Form, Row, Select } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getTriggerTypeLabel,
|
getTriggerTypeLabel,
|
||||||
IoTDeviceStatusEnum,
|
IoTDeviceStatusEnum,
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
IotRuleSceneTriggerTypeEnum,
|
IotRuleSceneTriggerTypeEnum,
|
||||||
triggerTypeOptions,
|
triggerTypeOptions,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Col, Form, Row, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import JsonParamsInput from '../inputs/json-params-input.vue';
|
import JsonParamsInput from '../inputs/json-params-input.vue';
|
||||||
import ValueInput from '../inputs/value-input.vue';
|
import ValueInput from '../inputs/value-input.vue';
|
||||||
|
|
@ -368,10 +368,10 @@ function handlePropertyChange(propertyInfo: any) {
|
||||||
|
|
||||||
<!-- 其他触发类型的提示 -->
|
<!-- 其他触发类型的提示 -->
|
||||||
<div v-else class="py-5 text-center">
|
<div v-else class="py-5 text-center">
|
||||||
<p class="mb-1 text-sm text-secondary">
|
<p class="mb-1 text-sm text-muted-foreground">
|
||||||
当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
|
当前触发事件类型:{{ getTriggerTypeLabel(triggerType) }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-secondary">此触发类型暂不需要配置额外条件</p>
|
<p class="text-xs text-muted-foreground">此触发类型暂不需要配置额外条件</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,15 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
import { computed, nextTick } from 'vue';
|
import { computed, nextTick } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
|
IotRuleSceneTriggerConditionTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Button } from 'ant-design-vue';
|
import { Button } from 'ant-design-vue';
|
||||||
|
|
||||||
import {
|
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
|
||||||
IotRuleSceneTriggerConditionTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ConditionConfig from './condition-config.vue';
|
import ConditionConfig from './condition-config.vue';
|
||||||
|
|
||||||
/** 子条件组配置组件 */
|
/** 子条件组配置组件 */
|
||||||
|
|
@ -87,8 +86,8 @@ function updateCondition(index: number, condition: TriggerCondition) {
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-if="!subGroup || subGroup.length === 0" class="py-6 text-center">
|
<div v-if="!subGroup || subGroup.length === 0" class="py-6 text-center">
|
||||||
<div class="flex flex-col items-center gap-3">
|
<div class="flex flex-col items-center gap-3">
|
||||||
<IconifyIcon icon="lucide:plus" class="text-8 text-secondary" />
|
<IconifyIcon icon="lucide:plus" class="text-8 text-muted-foreground" />
|
||||||
<div class="text-secondary">
|
<div class="text-muted-foreground">
|
||||||
<p class="mb-1 text-base font-bold">暂无条件</p>
|
<p class="mb-1 text-base font-bold">暂无条件</p>
|
||||||
<p class="text-xs">点击下方按钮添加第一个条件</p>
|
<p class="text-xs">点击下方按钮添加第一个条件</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -119,7 +118,7 @@ function updateCondition(index: number, condition: TriggerCondition) {
|
||||||
>
|
>
|
||||||
{{ conditionIndex + 1 }}
|
{{ conditionIndex + 1 }}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-base font-bold text-primary">
|
<span class="text-base font-bold text-foreground">
|
||||||
条件 {{ conditionIndex + 1 }}
|
条件 {{ conditionIndex + 1 }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -159,7 +158,7 @@ function updateCondition(index: number, condition: TriggerCondition) {
|
||||||
<IconifyIcon icon="lucide:plus" />
|
<IconifyIcon icon="lucide:plus" />
|
||||||
继续添加条件
|
继续添加条件
|
||||||
</Button>
|
</Button>
|
||||||
<span class="mt-2 block text-xs text-secondary">
|
<span class="mt-2 block text-xs text-muted-foreground">
|
||||||
最多可添加 {{ maxConditions }} 个条件
|
最多可添加 {{ maxConditions }} 个条件
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,12 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
|
|
||||||
|
import { IotRuleSceneTriggerTypeEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Button, Tag } from 'ant-design-vue';
|
import { Button, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import { IotRuleSceneTriggerTypeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import SubConditionGroupConfig from './sub-condition-group-config.vue';
|
import SubConditionGroupConfig from './sub-condition-group-config.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
<!-- JSON参数输入组件 - 通用版本 -->
|
<!-- JSON参数输入组件 - 通用版本 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { JsonParamsInputType } from '#/views/iot/utils/constants';
|
import type { JsonParamsInputType } from '@vben/constants';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { Button, Input, Popover, Tag } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IoTDataSpecsDataTypeEnum,
|
IoTDataSpecsDataTypeEnum,
|
||||||
JSON_PARAMS_EXAMPLE_VALUES,
|
JSON_PARAMS_EXAMPLE_VALUES,
|
||||||
JSON_PARAMS_INPUT_CONSTANTS,
|
JSON_PARAMS_INPUT_CONSTANTS,
|
||||||
JSON_PARAMS_INPUT_ICONS,
|
JSON_PARAMS_INPUT_ICONS,
|
||||||
JsonParamsInputTypeEnum,
|
JsonParamsInputTypeEnum,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Button, Input, Popover, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
/** JSON参数输入组件 - 通用版本 */
|
/** JSON参数输入组件 - 通用版本 */
|
||||||
defineOptions({ name: 'JsonParamsInput' });
|
defineOptions({ name: 'JsonParamsInput' });
|
||||||
|
|
@ -429,28 +428,17 @@ watch(
|
||||||
<div class="absolute right-2 top-2">
|
<div class="absolute right-2 top-2">
|
||||||
<Popover
|
<Popover
|
||||||
placement="leftTop"
|
placement="leftTop"
|
||||||
:width="450"
|
:overlay-style="{ width: '450px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
:show-arrow="true"
|
:arrow="true"
|
||||||
:offset="8"
|
overlay-class-name="json-params-detail-popover"
|
||||||
popper-class="json-params-detail-popover"
|
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #content>
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
shape="circle"
|
|
||||||
size="small"
|
|
||||||
:title="JSON_PARAMS_INPUT_CONSTANTS.VIEW_EXAMPLE_TITLE"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ep:info-filled" />
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 弹出层内容 -->
|
<!-- 弹出层内容 -->
|
||||||
<div class="json-params-detail-content">
|
<div class="json-params-detail-content">
|
||||||
<div class="mb-4 flex items-center gap-2">
|
<div class="mb-4 flex items-center gap-2">
|
||||||
<IconifyIcon :icon="titleIcon" class="text-lg text-primary" />
|
<IconifyIcon :icon="titleIcon" class="text-lg text-primary" />
|
||||||
<span class="text-base font-bold text-primary">
|
<span class="text-base font-bold text-foreground">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -463,7 +451,7 @@ watch(
|
||||||
:icon="paramsIcon"
|
:icon="paramsIcon"
|
||||||
class="text-base text-primary"
|
class="text-base text-primary"
|
||||||
/>
|
/>
|
||||||
<span class="text-base font-bold text-primary">
|
<span class="text-base font-bold text-foreground">
|
||||||
{{ paramsLabel }}
|
{{ paramsLabel }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -474,7 +462,7 @@ watch(
|
||||||
class="flex items-center justify-between rounded-lg bg-card p-2"
|
class="flex items-center justify-between rounded-lg bg-card p-2"
|
||||||
>
|
>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-base font-bold text-primary">
|
<div class="text-base font-bold text-foreground">
|
||||||
{{ param.name }}
|
{{ param.name }}
|
||||||
<Tag
|
<Tag
|
||||||
v-if="param.required"
|
v-if="param.required"
|
||||||
|
|
@ -484,7 +472,7 @@ watch(
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-secondary">
|
<div class="text-xs text-muted-foreground">
|
||||||
{{ param.identifier }}
|
{{ param.identifier }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -492,7 +480,7 @@ watch(
|
||||||
<Tag :color="getParamTypeTag(param.dataType)">
|
<Tag :color="getParamTypeTag(param.dataType)">
|
||||||
{{ getParamTypeName(param.dataType) }}
|
{{ getParamTypeName(param.dataType) }}
|
||||||
</Tag>
|
</Tag>
|
||||||
<span class="text-xs text-secondary">
|
<span class="text-xs text-muted-foreground">
|
||||||
{{ getExampleValue(param) }}
|
{{ getExampleValue(param) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -500,11 +488,11 @@ watch(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-6 mt-3">
|
<div class="ml-6 mt-3">
|
||||||
<div class="mb-1 text-xs text-secondary">
|
<div class="mb-1 text-xs text-muted-foreground">
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
|
||||||
</div>
|
</div>
|
||||||
<pre
|
<pre
|
||||||
class="border-l-[3px] overflow-x-auto rounded-lg border-primary bg-card p-3 text-sm text-primary"
|
class="border-l-[3px] overflow-x-auto rounded-lg border-primary bg-card p-3 text-sm text-foreground"
|
||||||
>
|
>
|
||||||
<code>{{ generateExampleJson() }}</code>
|
<code>{{ generateExampleJson() }}</code>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
@ -514,13 +502,22 @@ watch(
|
||||||
<!-- 无参数提示 -->
|
<!-- 无参数提示 -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="py-4 text-center">
|
<div class="py-4 text-center">
|
||||||
<p class="text-sm text-secondary">
|
<p class="text-sm text-muted-foreground">
|
||||||
{{ emptyMessage }}
|
{{ emptyMessage }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
shape="circle"
|
||||||
|
size="small"
|
||||||
|
:title="JSON_PARAMS_INPUT_CONSTANTS.VIEW_EXAMPLE_TITLE"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="ep:info-filled" />
|
||||||
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -547,7 +544,7 @@ watch(
|
||||||
|
|
||||||
<!-- 快速填充按钮 -->
|
<!-- 快速填充按钮 -->
|
||||||
<div v-if="paramsList.length > 0" class="flex items-center gap-2">
|
<div v-if="paramsList.length > 0" class="flex items-center gap-2">
|
||||||
<span class="text-xs text-secondary">
|
<span class="text-xs text-muted-foreground">
|
||||||
{{ JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL }}
|
{{ JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL }}
|
||||||
</span>
|
</span>
|
||||||
<Button size="small" type="primary" plain @click="fillExampleJson">
|
<Button size="small" type="primary" plain @click="fillExampleJson">
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { DatePicker, Input, Select, Tag, Tooltip } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IoTDataSpecsDataTypeEnum,
|
IoTDataSpecsDataTypeEnum,
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { DatePicker, Input, Select, Tag, Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
/** 值输入组件 */
|
/** 值输入组件 */
|
||||||
defineOptions({ name: 'ValueInput' });
|
defineOptions({ name: 'ValueInput' });
|
||||||
|
|
@ -197,7 +197,7 @@ watch(
|
||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
style="width: auto !important"
|
style="width: auto !important"
|
||||||
/>
|
/>
|
||||||
<span class="whitespace-nowrap text-xs text-secondary"> 至 </span>
|
<span class="whitespace-nowrap text-xs text-muted-foreground"> 至 </span>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="rangeEnd"
|
v-model:value="rangeEnd"
|
||||||
:type="getInputType()"
|
:type="getInputType()"
|
||||||
|
|
@ -232,7 +232,7 @@ watch(
|
||||||
v-if="listPreview.length > 0"
|
v-if="listPreview.length > 0"
|
||||||
class="mt-2 flex flex-wrap items-center gap-1"
|
class="mt-2 flex flex-wrap items-center gap-1"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-secondary"> 解析结果: </span>
|
<span class="text-xs text-muted-foreground"> 解析结果: </span>
|
||||||
<Tag
|
<Tag
|
||||||
v-for="(item, index) in listPreview"
|
v-for="(item, index) in listPreview"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|
@ -282,7 +282,7 @@ watch(
|
||||||
:content="`单位:${propertyConfig.unit}`"
|
:content="`单位:${propertyConfig.unit}`"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<span class="px-1 text-xs text-secondary">
|
<span class="px-1 text-xs text-muted-foreground">
|
||||||
{{ propertyConfig.unit }}
|
{{ propertyConfig.unit }}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Action } from '#/api/iot/rule/scene';
|
import type { Action } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { Button, Card, Empty, Form, Select, Tag } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getActionTypeLabel,
|
getActionTypeLabel,
|
||||||
getActionTypeOptions,
|
getActionTypeOptions,
|
||||||
IotRuleSceneActionTypeEnum,
|
IotRuleSceneActionTypeEnum,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Button, Card, Empty, Form, Select, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import AlertConfig from '../configs/alert-config.vue';
|
import AlertConfig from '../configs/alert-config.vue';
|
||||||
import DeviceControlConfig from '../configs/device-control-config.vue';
|
import DeviceControlConfig from '../configs/device-control-config.vue';
|
||||||
|
|
@ -154,7 +153,7 @@ function onActionTypeChange(action: Action, type: number) {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="gap-[8px] flex items-center">
|
<div class="gap-[8px] flex items-center">
|
||||||
<IconifyIcon icon="ep:setting" class="text-[18px] text-primary" />
|
<IconifyIcon icon="ep:setting" class="text-[18px] text-primary" />
|
||||||
<span class="text-[16px] font-semibold text-primary"> 执行器配置 </span>
|
<span class="text-[16px] font-semibold text-foreground"> 执行器配置 </span>
|
||||||
<Tag color="default"> {{ actions.length }} 个执行器 </Tag>
|
<Tag color="default"> {{ actions.length }} 个执行器 </Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="gap-[8px] flex items-center">
|
<div class="gap-[8px] flex items-center">
|
||||||
|
|
@ -229,9 +228,9 @@ function onActionTypeChange(action: Action, type: number) {
|
||||||
<Select
|
<Select
|
||||||
:model-value="action.type"
|
:model-value="action.type"
|
||||||
@update:model-value="
|
@update:model-value="
|
||||||
(value: number) => updateActionType(index, value)
|
(value: any) => updateActionType(index, value as number)
|
||||||
"
|
"
|
||||||
@change="(value) => onActionTypeChange(action, value)"
|
@change="(value: any) => onActionTypeChange(action, value as number)"
|
||||||
placeholder="请选择执行类型"
|
placeholder="请选择执行类型"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
>
|
>
|
||||||
|
|
@ -275,10 +274,10 @@ function onActionTypeChange(action: Action, type: number) {
|
||||||
>
|
>
|
||||||
<div class="mb-2 flex items-center gap-2">
|
<div class="mb-2 flex items-center gap-2">
|
||||||
<IconifyIcon icon="ep:warning" class="text-base text-warning" />
|
<IconifyIcon icon="ep:warning" class="text-base text-warning" />
|
||||||
<span class="font-semibold text-sm text-primary">触发告警</span>
|
<span class="font-semibold text-sm text-foreground">触发告警</span>
|
||||||
<Tag color="warning">自动执行</Tag>
|
<Tag color="warning">自动执行</Tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs leading-relaxed text-secondary">
|
<div class="text-xs leading-relaxed text-muted-foreground">
|
||||||
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 ->
|
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 ->
|
||||||
告警配置] 管理。
|
告警配置] 管理。
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="gap-[8px] flex items-center">
|
<div class="gap-[8px] flex items-center">
|
||||||
<IconifyIcon icon="ep:info-filled" class="text-[18px] text-primary" />
|
<IconifyIcon icon="ep:info-filled" class="text-[18px] text-primary" />
|
||||||
<span class="text-[16px] font-semibold text-primary">基础信息</span>
|
<span class="text-[16px] font-semibold text-foreground">基础信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gap-[8px] flex items-center">
|
<div class="gap-[8px] flex items-center">
|
||||||
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
|
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
|
||||||
|
|
@ -48,7 +48,7 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
|
||||||
placeholder="请输入场景名称"
|
placeholder="请输入场景名称"
|
||||||
:maxlength="50"
|
:maxlength="50"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
clearable
|
allow-clear
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,17 @@ import type { Trigger, TriggerCondition } from '#/api/iot/rule/scene';
|
||||||
|
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTriggerTypeLabel,
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
isDeviceTrigger,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Button, Card, Empty, Form, Tag } from 'ant-design-vue';
|
import { Button, Card, Empty, Form, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import { CronTab } from '#/components/cron-tab';
|
import { CronTab } from '#/components/cron-tab';
|
||||||
import {
|
|
||||||
getTriggerTypeLabel,
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
isDeviceTrigger,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import DeviceTriggerConfig from '../configs/device-trigger-config.vue';
|
import DeviceTriggerConfig from '../configs/device-trigger-config.vue';
|
||||||
import TimerConditionGroupConfig from '../configs/timer-condition-group-config.vue';
|
import TimerConditionGroupConfig from '../configs/timer-condition-group-config.vue';
|
||||||
|
|
@ -66,7 +66,7 @@ function removeTrigger(index: number) {
|
||||||
/** 更新触发器类型 */
|
/** 更新触发器类型 */
|
||||||
function updateTriggerType(index: number, type: number) {
|
function updateTriggerType(index: number, type: number) {
|
||||||
triggers.value[index]!.type = type;
|
triggers.value[index]!.type = type;
|
||||||
onTriggerTypeChange(index, type);
|
onTriggerTypeChange(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新触发器设备配置 */
|
/** 更新触发器设备配置 */
|
||||||
|
|
@ -88,7 +88,7 @@ function updateTriggerConditionGroups(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 触发器类型切换后清空相关字段 */
|
/** 触发器类型切换后清空相关字段 */
|
||||||
function onTriggerTypeChange(index: number, _: number) {
|
function onTriggerTypeChange(index: number) {
|
||||||
const triggerItem = triggers.value[index]!;
|
const triggerItem = triggers.value[index]!;
|
||||||
triggerItem.productId = undefined;
|
triggerItem.productId = undefined;
|
||||||
triggerItem.deviceId = undefined;
|
triggerItem.deviceId = undefined;
|
||||||
|
|
@ -113,7 +113,7 @@ onMounted(() => {
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="gap-[8px] flex items-center">
|
<div class="gap-[8px] flex items-center">
|
||||||
<IconifyIcon icon="ep:lightning" class="text-[18px] text-primary" />
|
<IconifyIcon icon="ep:lightning" class="text-[18px] text-primary" />
|
||||||
<span class="text-[16px] font-semibold text-primary">触发器配置</span>
|
<span class="text-[16px] font-semibold text-foreground">触发器配置</span>
|
||||||
<Tag color="default"> {{ triggers.length }} 个触发器 </Tag>
|
<Tag color="default"> {{ triggers.length }} 个触发器 </Tag>
|
||||||
</div>
|
</div>
|
||||||
<Button type="primary" size="small" @click="addTrigger">
|
<Button type="primary" size="small" @click="addTrigger">
|
||||||
|
|
@ -196,7 +196,7 @@ onMounted(() => {
|
||||||
icon="lucide:timer"
|
icon="lucide:timer"
|
||||||
class="text-[18px] text-danger"
|
class="text-[18px] text-danger"
|
||||||
/>
|
/>
|
||||||
<span class="text-[14px] font-medium text-primary">
|
<span class="text-[14px] font-medium text-foreground">
|
||||||
定时触发配置
|
定时触发配置
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -232,8 +232,8 @@ onMounted(() => {
|
||||||
<Empty description="暂无触发器">
|
<Empty description="暂无触发器">
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="space-y-[8px]">
|
<div class="space-y-[8px]">
|
||||||
<p class="text-secondary">暂无触发器配置</p>
|
<p class="text-muted-foreground">暂无触发器配置</p>
|
||||||
<p class="text-[12px] text-primary">
|
<p class="text-[12px] text-muted-foreground">
|
||||||
请使用上方的"添加触发器"按钮来设置触发规则
|
请使用上方的"添加触发器"按钮来设置触发规则
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DEVICE_SELECTOR_OPTIONS, DICT_TYPE } from '@vben/constants';
|
||||||
|
|
||||||
import { Select } from 'ant-design-vue';
|
import { Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import { getDeviceListByProductId } from '#/api/iot/device/device';
|
import { getDeviceListByProductId } from '#/api/iot/device/device';
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { DictTag } from '#/components/dict-tag';
|
||||||
import { DEVICE_SELECTOR_OPTIONS } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
/** 设备选择器组件 */
|
/** 设备选择器组件 */
|
||||||
defineOptions({ name: 'DeviceSelector' });
|
defineOptions({ name: 'DeviceSelector' });
|
||||||
|
|
@ -26,13 +25,11 @@ const emit = defineEmits<{
|
||||||
const deviceLoading = ref(false); // 设备加载状态
|
const deviceLoading = ref(false); // 设备加载状态
|
||||||
const deviceList = ref<any[]>([]); // 设备列表
|
const deviceList = ref<any[]>([]); // 设备列表
|
||||||
|
|
||||||
/**
|
/** 处理选择变化事件 */
|
||||||
* 处理选择变化事件
|
// TODO @AI:是不是应该 value 加个设备编号?方法名是不是也要优化下?方法名是不是要优化?
|
||||||
* @param value 选中的设备ID
|
function handleChange(value: any) {
|
||||||
*/
|
emit('update:modelValue', value as number | undefined);
|
||||||
function handleChange(value?: number) {
|
emit('change', value as number | undefined);
|
||||||
emit('update:modelValue', value);
|
|
||||||
emit('change', value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,13 +43,12 @@ async function getDeviceList() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceLoading.value = true;
|
deviceLoading.value = true;
|
||||||
const res = await getDeviceListByProductId(props.productId);
|
const data = await getDeviceListByProductId(props.productId);
|
||||||
deviceList.value = res || [];
|
deviceList.value = [DEVICE_SELECTOR_OPTIONS.ALL_DEVICES, ...(data || [])];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取设备列表失败:', error);
|
console.error('获取设备列表失败:', error);
|
||||||
deviceList.value = [];
|
deviceList.value = [DEVICE_SELECTOR_OPTIONS.ALL_DEVICES];
|
||||||
} finally {
|
} finally {
|
||||||
deviceList.value.unshift(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES);
|
|
||||||
deviceLoading.value = false;
|
deviceLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -81,8 +77,8 @@ watch(
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
placeholder="请选择设备"
|
placeholder="请选择设备"
|
||||||
filterable
|
show-search
|
||||||
clearable
|
allow-clear
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:loading="deviceLoading"
|
:loading="deviceLoading"
|
||||||
:disabled="!productId"
|
:disabled="!productId"
|
||||||
|
|
@ -95,10 +91,10 @@ watch(
|
||||||
>
|
>
|
||||||
<div class="py-[4px] flex w-full items-center justify-between">
|
<div class="py-[4px] flex w-full items-center justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[14px] font-medium mb-[2px] text-primary">
|
<div class="text-[14px] font-medium mb-[2px] text-foreground">
|
||||||
{{ device.deviceName }}
|
{{ device.deviceName }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[12px] text-primary">
|
<div class="text-[12px] text-muted-foreground">
|
||||||
{{ device.deviceKey }}
|
{{ device.deviceKey }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { Select } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IoTDataSpecsDataTypeEnum,
|
IoTDataSpecsDataTypeEnum,
|
||||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Select } from 'ant-design-vue';
|
||||||
|
|
||||||
/** 操作符选择器组件 */
|
/** 操作符选择器组件 */
|
||||||
defineOptions({ name: 'OperatorSelector' });
|
defineOptions({ name: 'OperatorSelector' });
|
||||||
|
|
@ -252,7 +252,7 @@ watch(
|
||||||
>
|
>
|
||||||
<div class="py-[4px] flex w-full items-center justify-between">
|
<div class="py-[4px] flex w-full items-center justify-between">
|
||||||
<div class="gap-[8px] flex items-center">
|
<div class="gap-[8px] flex items-center">
|
||||||
<div class="text-[14px] font-medium text-primary">
|
<div class="text-[14px] font-medium text-foreground">
|
||||||
{{ operator.label }}
|
{{ operator.label }}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -261,7 +261,7 @@ watch(
|
||||||
{{ operator.symbol }}
|
{{ operator.symbol }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[12px] text-secondary">
|
<div class="text-[12px] text-muted-foreground">
|
||||||
{{ operator.description }}
|
{{ operator.description }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,8 @@ onMounted(() => {
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@change="(value: any) => handleChange(value)"
|
@change="(value: any) => handleChange(value)"
|
||||||
placeholder="请选择产品"
|
placeholder="请选择产品"
|
||||||
filterable
|
show-search
|
||||||
clearable
|
allow-clear
|
||||||
class="w-full"
|
class="w-full"
|
||||||
:loading="productLoading"
|
:loading="productLoading"
|
||||||
>
|
>
|
||||||
|
|
@ -71,10 +71,10 @@ onMounted(() => {
|
||||||
>
|
>
|
||||||
<div class="py-[4px] flex w-full items-center justify-between">
|
<div class="py-[4px] flex w-full items-center justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="text-[14px] font-medium mb-[2px] text-primary">
|
<div class="text-[14px] font-medium mb-[2px] text-foreground">
|
||||||
{{ product.name }}
|
{{ product.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-[12px] text-secondary">
|
<div class="text-[12px] text-muted-foreground">
|
||||||
{{ product.productKey }}
|
{{ product.productKey }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,21 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { IconifyIcon } from '@vben/icons';
|
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
|
||||||
import { Button, Popover, Select, Tag } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
|
||||||
import {
|
import {
|
||||||
getAccessModeLabel,
|
getAccessModeLabel,
|
||||||
getDataTypeName,
|
getDataTypeName,
|
||||||
getDataTypeTagColor,
|
|
||||||
getEventTypeLabel,
|
getEventTypeLabel,
|
||||||
getThingModelServiceCallTypeLabel,
|
getThingModelServiceCallTypeLabel,
|
||||||
IotRuleSceneTriggerTypeEnum,
|
IotRuleSceneTriggerTypeEnum,
|
||||||
IoTThingModelTypeEnum,
|
IoTThingModelTypeEnum,
|
||||||
THING_MODEL_GROUP_LABELS,
|
THING_MODEL_GROUP_LABELS,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { Button, Popover, Select, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { getThingModelTSL } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
/** 属性选择器组件 */
|
/** 属性选择器组件 */
|
||||||
defineOptions({ name: 'PropertySelector' });
|
defineOptions({ name: 'PropertySelector' });
|
||||||
|
|
@ -58,127 +57,91 @@ interface PropertySelectorItem {
|
||||||
|
|
||||||
const localValue = useVModel(props, 'modelValue', emit);
|
const localValue = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
const loading = ref(false); // 加载状态
|
const loading = ref(false);
|
||||||
const propertyList = ref<ThingModelApi.Property[]>([]); // 属性列表
|
const propertyList = ref<PropertySelectorItem[]>([]);
|
||||||
const thingModelTSL = ref<null | ThingModelApi.ThingModel>(null); // 物模型TSL数据
|
|
||||||
|
|
||||||
// 计算属性:属性分组
|
/** 触发类型 → 物模型类型 + 分组标签 */
|
||||||
const propertyGroups = computed(() => {
|
const TRIGGER_TYPE_TO_GROUP: Record<
|
||||||
const groups: { label: string; options: any[] }[] = [];
|
number,
|
||||||
|
{ label: string; modelType: number }
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
|
> = {
|
||||||
groups.push({
|
[IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: {
|
||||||
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||||
options: propertyList.value.filter(
|
modelType: IoTThingModelTypeEnum.PROPERTY,
|
||||||
(property: any) => property.type === IoTThingModelTypeEnum.PROPERTY,
|
},
|
||||||
),
|
[IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: {
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.EVENT,
|
label: THING_MODEL_GROUP_LABELS.EVENT,
|
||||||
options: propertyList.value.filter(
|
modelType: IoTThingModelTypeEnum.EVENT,
|
||||||
(property: any) => property.type === IoTThingModelTypeEnum.EVENT,
|
},
|
||||||
),
|
[IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: {
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
|
|
||||||
groups.push({
|
|
||||||
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
||||||
options: propertyList.value.filter(
|
modelType: IoTThingModelTypeEnum.SERVICE,
|
||||||
(property: any) => property.type === IoTThingModelTypeEnum.SERVICE,
|
},
|
||||||
),
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups.filter((group) => group.options.length > 0);
|
/** 属性分组:按触发类型筛选属性 / 事件 / 服务 */
|
||||||
|
const propertyGroups = computed(() => {
|
||||||
|
const config = TRIGGER_TYPE_TO_GROUP[props.triggerType];
|
||||||
|
if (!config) return [];
|
||||||
|
const options = propertyList.value.filter(
|
||||||
|
(item) => item.type === config.modelType,
|
||||||
|
);
|
||||||
|
return options.length > 0 ? [{ label: config.label, options }] : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性:当前选中的属性
|
/** 当前选中的属性 */
|
||||||
const selectedProperty = computed(() => {
|
const selectedProperty = computed(() =>
|
||||||
return propertyList.value.find((p) => p.identifier === localValue.value);
|
propertyList.value.find((p) => p.identifier === localValue.value),
|
||||||
});
|
);
|
||||||
|
|
||||||
/**
|
/** 处理选择变化事件 */
|
||||||
* 处理选择变化事件
|
|
||||||
* @param value 选中的属性标识符
|
|
||||||
*/
|
|
||||||
function handleChange(value: any) {
|
function handleChange(value: any) {
|
||||||
const property = propertyList.value.find((p) => p.identifier === value);
|
const property = propertyList.value.find((p) => p.identifier === value);
|
||||||
if (property) {
|
if (property) {
|
||||||
emit('change', {
|
emit('change', { type: property.dataType, config: property });
|
||||||
type: property.dataType,
|
|
||||||
config: property,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 获取物模型 TSL 数据 */
|
||||||
* 获取物模型TSL数据
|
async function loadThingModelTSL() {
|
||||||
*/
|
|
||||||
async function getThingModelTSL() {
|
|
||||||
if (!props.productId) {
|
if (!props.productId) {
|
||||||
thingModelTSL.value = null;
|
|
||||||
propertyList.value = [];
|
propertyList.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const tslData = await getThingModelListByProductId(props.productId);
|
const tsl = await getThingModelTSL(props.productId);
|
||||||
|
propertyList.value = parseThingModelData(tsl);
|
||||||
if (tslData) {
|
|
||||||
thingModelTSL.value = tslData;
|
|
||||||
parseThingModelData();
|
|
||||||
} else {
|
|
||||||
console.error('获取物模型TSL失败: 返回数据为空');
|
|
||||||
propertyList.value = [];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取物模型TSL失败:', error);
|
console.error('获取物模型 TSL 失败:', error);
|
||||||
propertyList.value = [];
|
propertyList.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析物模型 TSL 数据 */
|
/** 把 TSL 树展平为 PropertySelectorItem[] */
|
||||||
function parseThingModelData() {
|
function parseThingModelData(
|
||||||
const tsl = thingModelTSL.value;
|
tsl?: null | ThingModelApi.ThingModelTSL,
|
||||||
const properties: PropertySelectorItem[] = [];
|
): PropertySelectorItem[] {
|
||||||
|
if (!tsl) return [];
|
||||||
if (!tsl) {
|
const properties = (tsl.properties ?? []).map<PropertySelectorItem>(
|
||||||
propertyList.value = properties;
|
(prop) => ({
|
||||||
return;
|
identifier: prop.identifier!,
|
||||||
}
|
name: prop.name!,
|
||||||
// 解析属性
|
|
||||||
// TODO @AI:这里的 linter 报错
|
|
||||||
if (tsl.properties && Array.isArray(tsl.properties)) {
|
|
||||||
tsl.properties.forEach((prop) => {
|
|
||||||
properties.push({
|
|
||||||
identifier: prop.identifier,
|
|
||||||
name: prop.name,
|
|
||||||
description: prop.description,
|
description: prop.description,
|
||||||
dataType: prop.dataType,
|
dataType: prop.dataType!,
|
||||||
type: IoTThingModelTypeEnum.PROPERTY,
|
type: IoTThingModelTypeEnum.PROPERTY,
|
||||||
accessMode: prop.accessMode,
|
accessMode: prop.accessMode,
|
||||||
required: prop.required,
|
required: prop.required,
|
||||||
unit: getPropertyUnit(prop),
|
unit: prop.dataSpecs?.unit,
|
||||||
range: getPropertyRange(prop),
|
range: getPropertyRange(prop),
|
||||||
property: prop,
|
property: prop,
|
||||||
});
|
}),
|
||||||
});
|
);
|
||||||
}
|
const events = (tsl.events ?? []).map<PropertySelectorItem>((event) => ({
|
||||||
|
identifier: event.identifier!,
|
||||||
// 解析事件
|
name: event.name!,
|
||||||
// TODO @AI:这里的 linter 报错
|
|
||||||
if (tsl.events && Array.isArray(tsl.events)) {
|
|
||||||
tsl.events.forEach((event) => {
|
|
||||||
properties.push({
|
|
||||||
identifier: event.identifier,
|
|
||||||
name: event.name,
|
|
||||||
description: event.description,
|
description: event.description,
|
||||||
dataType: 'struct',
|
dataType: 'struct',
|
||||||
type: IoTThingModelTypeEnum.EVENT,
|
type: IoTThingModelTypeEnum.EVENT,
|
||||||
|
|
@ -186,16 +149,10 @@ function parseThingModelData() {
|
||||||
required: event.required,
|
required: event.required,
|
||||||
outputParams: event.outputParams,
|
outputParams: event.outputParams,
|
||||||
event,
|
event,
|
||||||
});
|
}));
|
||||||
});
|
const services = (tsl.services ?? []).map<PropertySelectorItem>((service) => ({
|
||||||
}
|
identifier: service.identifier!,
|
||||||
|
name: service.name!,
|
||||||
// 解析服务
|
|
||||||
if (tsl.services && Array.isArray(tsl.services)) {
|
|
||||||
tsl.services.forEach((service) => {
|
|
||||||
properties.push({
|
|
||||||
identifier: service.identifier,
|
|
||||||
name: service.name,
|
|
||||||
description: service.description,
|
description: service.description,
|
||||||
dataType: 'struct',
|
dataType: 'struct',
|
||||||
type: IoTThingModelTypeEnum.SERVICE,
|
type: IoTThingModelTypeEnum.SERVICE,
|
||||||
|
|
@ -204,51 +161,21 @@ function parseThingModelData() {
|
||||||
inputParams: service.inputParams,
|
inputParams: service.inputParams,
|
||||||
outputParams: service.outputParams,
|
outputParams: service.outputParams,
|
||||||
service,
|
service,
|
||||||
});
|
}));
|
||||||
});
|
return [...properties, ...events, ...services];
|
||||||
}
|
|
||||||
propertyList.value = properties;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 获取属性取值范围:数值型给 min~max;枚举 / 布尔给选项列表 */
|
||||||
* 获取属性单位
|
function getPropertyRange(property: ThingModelApi.Property): string | undefined {
|
||||||
* @param property 属性对象
|
|
||||||
* @returns 属性单位
|
|
||||||
*/
|
|
||||||
function getPropertyUnit(property: any) {
|
|
||||||
if (!property) return undefined;
|
|
||||||
|
|
||||||
// 数值型数据的单位
|
|
||||||
if (property.dataSpecs && property.dataSpecs.unit) {
|
|
||||||
return property.dataSpecs.unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取属性范围描述
|
|
||||||
* @param property 属性对象
|
|
||||||
* @returns 属性范围描述
|
|
||||||
*/
|
|
||||||
function getPropertyRange(property: any) {
|
|
||||||
if (!property) return undefined;
|
|
||||||
|
|
||||||
// 数值型数据的范围
|
|
||||||
if (property.dataSpecs) {
|
|
||||||
const specs = property.dataSpecs;
|
const specs = property.dataSpecs;
|
||||||
if (specs.min !== undefined && specs.max !== undefined) {
|
if (specs && specs.min !== undefined && specs.max !== undefined) {
|
||||||
return `${specs.min}~${specs.max}`;
|
return `${specs.min}~${specs.max}`;
|
||||||
}
|
}
|
||||||
}
|
if (property.dataSpecsList?.length) {
|
||||||
|
|
||||||
// 枚举型和布尔型数据的选项
|
|
||||||
if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
|
|
||||||
return property.dataSpecsList
|
return property.dataSpecsList
|
||||||
.map((item: any) => `${item.name}(${item.value})`)
|
.map((item: any) => `${item.name}(${item.value})`)
|
||||||
.join(', ');
|
.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +183,7 @@ function getPropertyRange(property: any) {
|
||||||
watch(
|
watch(
|
||||||
() => props.productId,
|
() => props.productId,
|
||||||
() => {
|
() => {
|
||||||
getThingModelTSL();
|
loadThingModelTSL();
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
@ -275,13 +202,13 @@ watch(
|
||||||
<Select
|
<Select
|
||||||
v-model:value="localValue"
|
v-model:value="localValue"
|
||||||
placeholder="请选择监控项"
|
placeholder="请选择监控项"
|
||||||
filterable
|
show-search
|
||||||
clearable
|
allow-clear
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
class="!w-[150px]"
|
class="!w-[150px]"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
>
|
>
|
||||||
<Select.OptionGroup
|
<Select.OptGroup
|
||||||
v-for="group in propertyGroups"
|
v-for="group in propertyGroups"
|
||||||
:key="group.label"
|
:key="group.label"
|
||||||
:label="group.label"
|
:label="group.label"
|
||||||
|
|
@ -293,60 +220,45 @@ watch(
|
||||||
:value="property.identifier"
|
:value="property.identifier"
|
||||||
>
|
>
|
||||||
<div class="py-[2px] flex w-full items-center justify-between">
|
<div class="py-[2px] flex w-full items-center justify-between">
|
||||||
<span class="text-[14px] font-medium flex-1 truncate text-primary">
|
<span class="text-[14px] font-medium flex-1 truncate text-foreground">
|
||||||
{{ property.name }}
|
{{ property.name }}
|
||||||
</span>
|
</span>
|
||||||
<Tag
|
<Tag class="ml-[8px] flex-shrink-0">
|
||||||
:color="getDataTypeTagColor(property.dataType)"
|
|
||||||
class="ml-[8px] flex-shrink-0"
|
|
||||||
>
|
|
||||||
{{ property.identifier }}
|
{{ property.identifier }}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
</Select.OptionGroup>
|
</Select.OptGroup>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<!-- 属性详情弹出层 -->
|
<!-- 属性详情弹出层 -->
|
||||||
<Popover
|
<Popover
|
||||||
v-if="selectedProperty"
|
v-if="selectedProperty"
|
||||||
placement="rightTop"
|
placement="rightTop"
|
||||||
:width="350"
|
:overlay-style="{ width: '350px' }"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
:show-arrow="true"
|
:arrow="true"
|
||||||
:offset="8"
|
overlay-class-name="property-detail-popover"
|
||||||
popper-class="property-detail-popover"
|
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #content>
|
||||||
<Button
|
|
||||||
type="link"
|
|
||||||
shape="circle"
|
|
||||||
size="small"
|
|
||||||
class="flex-shrink-0"
|
|
||||||
title="查看属性详情"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ep:info-filled" />
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 弹出层内容 -->
|
<!-- 弹出层内容 -->
|
||||||
<div class="property-detail-content">
|
<div class="property-detail-content">
|
||||||
<div class="gap-[8px] mb-[12px] flex items-center">
|
<div class="gap-[8px] mb-[12px] flex items-center">
|
||||||
<IconifyIcon icon="ep:info-filled" class="text-[16px] text-info" />
|
<IconifyIcon icon="ep:info-filled" class="text-[16px] text-info" />
|
||||||
<span class="text-[14px] font-medium text-primary">
|
<span class="text-[14px] font-medium text-foreground">
|
||||||
{{ selectedProperty.name }}
|
{{ selectedProperty.name }}
|
||||||
</span>
|
</span>
|
||||||
<Tag :color="getDataTypeTagColor(selectedProperty.dataType)">
|
<Tag>
|
||||||
{{ getDataTypeName(selectedProperty.dataType) }}
|
{{ getDataTypeName(selectedProperty.dataType) }}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-[8px] ml-[24px]">
|
<div class="space-y-[8px] ml-[24px]">
|
||||||
<div class="gap-[8px] flex items-start">
|
<div class="gap-[8px] flex items-start">
|
||||||
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-secondary">
|
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-muted-foreground">
|
||||||
标识符:
|
标识符:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[12px] flex-1 text-primary">
|
<span class="text-[12px] flex-1 text-foreground">
|
||||||
{{ selectedProperty.identifier }}
|
{{ selectedProperty.identifier }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -355,28 +267,28 @@ watch(
|
||||||
v-if="selectedProperty.description"
|
v-if="selectedProperty.description"
|
||||||
class="gap-[8px] flex items-start"
|
class="gap-[8px] flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-secondary">
|
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-muted-foreground">
|
||||||
描述:
|
描述:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[12px] flex-1 text-primary">
|
<span class="text-[12px] flex-1 text-foreground">
|
||||||
{{ selectedProperty.description }}
|
{{ selectedProperty.description }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedProperty.unit" class="gap-[8px] flex items-start">
|
<div v-if="selectedProperty.unit" class="gap-[8px] flex items-start">
|
||||||
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-secondary">
|
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-muted-foreground">
|
||||||
单位:
|
单位:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[12px] flex-1 text-primary">
|
<span class="text-[12px] flex-1 text-foreground">
|
||||||
{{ selectedProperty.unit }}
|
{{ selectedProperty.unit }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedProperty.range" class="gap-[8px] flex items-start">
|
<div v-if="selectedProperty.range" class="gap-[8px] flex items-start">
|
||||||
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-secondary">
|
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-muted-foreground">
|
||||||
取值范围:
|
取值范围:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[12px] flex-1 text-primary">
|
<span class="text-[12px] flex-1 text-foreground">
|
||||||
{{ selectedProperty.range }}
|
{{ selectedProperty.range }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -389,10 +301,10 @@ watch(
|
||||||
"
|
"
|
||||||
class="gap-[8px] flex items-start"
|
class="gap-[8px] flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-secondary">
|
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-muted-foreground">
|
||||||
访问模式:
|
访问模式:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[12px] flex-1 text-primary">
|
<span class="text-[12px] flex-1 text-foreground">
|
||||||
{{ getAccessModeLabel(selectedProperty.accessMode) }}
|
{{ getAccessModeLabel(selectedProperty.accessMode) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -404,10 +316,10 @@ watch(
|
||||||
"
|
"
|
||||||
class="gap-[8px] flex items-start"
|
class="gap-[8px] flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-secondary">
|
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-muted-foreground">
|
||||||
事件类型:
|
事件类型:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[12px] flex-1 text-primary">
|
<span class="text-[12px] flex-1 text-foreground">
|
||||||
{{ getEventTypeLabel(selectedProperty.eventType) }}
|
{{ getEventTypeLabel(selectedProperty.eventType) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -419,15 +331,25 @@ watch(
|
||||||
"
|
"
|
||||||
class="gap-[8px] flex items-start"
|
class="gap-[8px] flex items-start"
|
||||||
>
|
>
|
||||||
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-secondary">
|
<span class="text-[12px] min-w-[60px] flex-shrink-0 text-muted-foreground">
|
||||||
调用类型:
|
调用类型:
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[12px] flex-1 text-primary">
|
<span class="text-[12px] flex-1 text-foreground">
|
||||||
{{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
|
{{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
shape="circle"
|
||||||
|
size="small"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
title="查看属性详情"
|
||||||
|
>
|
||||||
|
<IconifyIcon icon="ep:info-filled" />
|
||||||
|
</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,13 @@ import type { RuleSceneApi } from '#/api/iot/rule/scene';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { Page, useVbenDrawer } from '@vben/common-ui';
|
||||||
import { CommonStatusEnum } from '@vben/constants';
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
getActionTypeLabel,
|
||||||
|
getTriggerTypeLabel,
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatDateTime } from '@vben/utils';
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
|
@ -17,14 +23,9 @@ import {
|
||||||
getSceneRulePage,
|
getSceneRulePage,
|
||||||
updateSceneRuleStatus,
|
updateSceneRuleStatus,
|
||||||
} from '#/api/iot/rule/scene';
|
} from '#/api/iot/rule/scene';
|
||||||
|
import { DictTag } from '#/components/dict-tag';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { CronUtils } from '#/utils/cron';
|
import { CronUtils } from '#/utils/cron';
|
||||||
import {
|
|
||||||
getActionTypeLabel,
|
|
||||||
getTriggerTypeLabel,
|
|
||||||
IotRuleSceneActionTypeEnum,
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
import Form from './modules/form.vue';
|
import Form from './modules/form.vue';
|
||||||
|
|
@ -105,109 +106,50 @@ function hasTimerTrigger(row: RuleSceneApi.SceneRule): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 触发器列表项(用于列内多 tag 渲染) */
|
/** 触发条件摘要文本(拼接所有触发器) */
|
||||||
interface TriggerCellItem {
|
function getTriggerSummary(row: RuleSceneApi.SceneRule): string {
|
||||||
color: string;
|
if (!row.triggers?.length) return '无触发器';
|
||||||
label: string;
|
return row.triggers
|
||||||
meta?: string;
|
.map((trigger) => {
|
||||||
}
|
|
||||||
|
|
||||||
/** 动作列表项 */
|
|
||||||
interface ActionCellItem {
|
|
||||||
color: string;
|
|
||||||
label: string;
|
|
||||||
meta?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 触发器 → tag 颜色(按 5 种类型区分) */
|
|
||||||
function colorOfTrigger(type?: number): string {
|
|
||||||
switch (type) {
|
|
||||||
case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST: {
|
|
||||||
return 'orange';
|
|
||||||
}
|
|
||||||
case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST: {
|
|
||||||
return 'blue';
|
|
||||||
}
|
|
||||||
case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE: {
|
|
||||||
return 'purple';
|
|
||||||
}
|
|
||||||
case IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE: {
|
|
||||||
return 'cyan';
|
|
||||||
}
|
|
||||||
case IotRuleSceneTriggerTypeEnum.TIMER: {
|
|
||||||
return 'gold';
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 动作 → tag 颜色(按 4 种类型区分) */
|
|
||||||
function colorOfAction(type?: number): string {
|
|
||||||
switch (type) {
|
|
||||||
case IotRuleSceneActionTypeEnum.ALERT_RECOVER: {
|
|
||||||
return 'green';
|
|
||||||
}
|
|
||||||
case IotRuleSceneActionTypeEnum.ALERT_TRIGGER: {
|
|
||||||
return 'red';
|
|
||||||
}
|
|
||||||
case IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET: {
|
|
||||||
return 'blue';
|
|
||||||
}
|
|
||||||
case IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE: {
|
|
||||||
return 'purple';
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return 'default';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 触发器列:每个触发器一项 */
|
|
||||||
function getTriggerCellItems(row: RuleSceneApi.SceneRule): TriggerCellItem[] {
|
|
||||||
if (!row.triggers?.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return row.triggers.map((trigger) => {
|
|
||||||
const type = trigger.type ?? 0;
|
const type = trigger.type ?? 0;
|
||||||
let label = getTriggerTypeLabel(type);
|
let description = getTriggerTypeLabel(type);
|
||||||
if (
|
if (
|
||||||
(type === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
(type === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
||||||
type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||||
type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) &&
|
type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) &&
|
||||||
trigger.identifier
|
trigger.identifier
|
||||||
) {
|
) {
|
||||||
label += ` · ${trigger.identifier}`;
|
description += ` (${trigger.identifier})`;
|
||||||
} else if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
} else if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||||
label += ` · ${CronUtils.format(trigger.cronExpression || '')}`;
|
description = `${getTriggerTypeLabel(type)} (${CronUtils.format(trigger.cronExpression || '')})`;
|
||||||
}
|
}
|
||||||
const meta = trigger.deviceId
|
if (trigger.deviceId) {
|
||||||
? `设备 #${trigger.deviceId}`
|
description += ` [设备 ID: ${trigger.deviceId}]`;
|
||||||
: (trigger.productId
|
} else if (trigger.productId) {
|
||||||
? `产品 #${trigger.productId}`
|
description += ` [产品 ID: ${trigger.productId}]`;
|
||||||
: '');
|
}
|
||||||
return { color: colorOfTrigger(type), label, meta };
|
return description;
|
||||||
});
|
})
|
||||||
|
.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 动作列:每个动作一项 */
|
/** 执行动作摘要文本(拼接所有动作) */
|
||||||
function getActionCellItems(row: RuleSceneApi.SceneRule): ActionCellItem[] {
|
function getActionSummary(row: RuleSceneApi.SceneRule): string {
|
||||||
if (!row.actions?.length) {
|
if (!row.actions?.length) return '无执行器';
|
||||||
return [];
|
return row.actions
|
||||||
|
.map((action) => {
|
||||||
|
let description = getActionTypeLabel(action.type ?? 0);
|
||||||
|
if (action.deviceId) {
|
||||||
|
description += ` [设备 ID: ${action.deviceId}]`;
|
||||||
|
} else if (action.productId) {
|
||||||
|
description += ` [产品 ID: ${action.productId}]`;
|
||||||
}
|
}
|
||||||
return row.actions.map((action) => {
|
if (action.alertConfigId) {
|
||||||
const type = action.type ?? 0;
|
description += ` [告警配置 ID: ${action.alertConfigId}]`;
|
||||||
const label = getActionTypeLabel(type);
|
}
|
||||||
const meta = action.deviceId
|
return description;
|
||||||
? `设备 #${action.deviceId}`
|
})
|
||||||
: action.productId
|
.join(', ');
|
||||||
? `产品 #${action.productId}`
|
|
||||||
: action.alertConfigId
|
|
||||||
? `告警 #${action.alertConfigId}`
|
|
||||||
: '';
|
|
||||||
return { color: colorOfAction(type), label, meta };
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 取定时触发器的 CRON 频率描述 */
|
/** 取定时触发器的 CRON 频率描述 */
|
||||||
|
|
@ -304,7 +246,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ statistics.total }}
|
{{ statistics.total }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-secondary">总规则数</div>
|
<div class="text-xs text-muted-foreground">总规则数</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -321,7 +263,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ statistics.enabled }}
|
{{ statistics.enabled }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-secondary">启用规则</div>
|
<div class="text-xs text-muted-foreground">启用规则</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -338,7 +280,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ statistics.disabled }}
|
{{ statistics.disabled }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-secondary">禁用规则</div>
|
<div class="text-xs text-muted-foreground">禁用规则</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -355,7 +297,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
<div class="text-xl font-semibold">
|
<div class="text-xl font-semibold">
|
||||||
{{ statistics.timerRules }}
|
{{ statistics.timerRules }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-secondary">定时规则</div>
|
<div class="text-xs text-muted-foreground">定时规则</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -375,65 +317,51 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- 规则名称列:名称 + 状态 + 描述 -->
|
<!-- 规则名称列:名称 + 状态 tag inline + 描述 -->
|
||||||
<template #name="{ row }">
|
<template #name="{ row }">
|
||||||
<div class="gap-2 flex items-center">
|
<div class="flex items-center gap-2">
|
||||||
<span class="font-medium">{{ row.name }}</span>
|
<span class="font-medium text-foreground">{{ row.name }}</span>
|
||||||
|
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
v-if="row.description"
|
v-if="row.description"
|
||||||
:title="row.description"
|
:title="row.description"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<div class="text-xs text-secondary mt-1 truncate max-w-[160px]">
|
<div class="mt-1 max-w-[200px] truncate text-xs text-muted-foreground">
|
||||||
{{ row.description }}
|
{{ row.description }}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
<!-- 触发条件列:按触发器各显示一项 -->
|
<!-- 触发条件列:单 tag 汇总 + 定时触发额外信息 -->
|
||||||
<template #triggers="{ row }">
|
<template #triggers="{ row }">
|
||||||
<div v-if="getTriggerCellItems(row).length > 0" class="flex flex-col gap-1">
|
<div class="space-y-1">
|
||||||
<div
|
<Tag color="processing" class="m-0">{{ getTriggerSummary(row) }}</Tag>
|
||||||
v-for="(item, i) in getTriggerCellItems(row)"
|
|
||||||
:key="`trigger-${i}`"
|
|
||||||
class="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<Tag :color="item.color" class="m-0">{{ item.label }}</Tag>
|
|
||||||
<span v-if="item.meta" class="text-xs text-secondary">
|
|
||||||
{{ item.meta }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
v-if="hasTimerTrigger(row)"
|
v-if="hasTimerTrigger(row)"
|
||||||
:title="getCronExpression(row)"
|
:title="getCronExpression(row)"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<span class="text-xs text-secondary">
|
<div class="text-xs text-muted-foreground">
|
||||||
<IconifyIcon icon="lucide:clock" class="mr-1 inline" />
|
<IconifyIcon icon="lucide:clock" class="mr-1 inline" />
|
||||||
{{ getCronFrequency(row) }}
|
{{ getCronFrequency(row) }}
|
||||||
<template v-if="getNextExecutionTime(row)">
|
<template v-if="getNextExecutionTime(row)">
|
||||||
· 下次 {{ formatDateTime(getNextExecutionTime(row) as Date) }}
|
· 下次 {{ formatDateTime(getNextExecutionTime(row) as Date) }}
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<span v-else class="text-xs text-secondary">无触发器</span>
|
|
||||||
</template>
|
</template>
|
||||||
<!-- 执行动作列:按动作各显示一项 -->
|
<!-- 执行动作列:单 tag 汇总 -->
|
||||||
<template #actionsCol="{ row }">
|
<template #actionsCol="{ row }">
|
||||||
<div v-if="getActionCellItems(row).length > 0" class="flex flex-col gap-1">
|
<Tag color="success" class="m-0">{{ getActionSummary(row) }}</Tag>
|
||||||
<div
|
</template>
|
||||||
v-for="(item, i) in getActionCellItems(row)"
|
<!-- 最近触发列 -->
|
||||||
:key="`action-${i}`"
|
<template #lastTriggeredTime="{ row }">
|
||||||
class="flex items-center gap-1"
|
<span v-if="row.lastTriggeredTime">
|
||||||
>
|
{{ formatDateTime(row.lastTriggeredTime) }}
|
||||||
<Tag :color="item.color" class="m-0">{{ item.label }}</Tag>
|
|
||||||
<span v-if="item.meta" class="text-xs text-secondary">
|
|
||||||
{{ item.meta }}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span v-else class="text-muted-foreground">未触发</span>
|
||||||
</div>
|
|
||||||
<span v-else class="text-xs text-secondary">无动作</span>
|
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<TableAction
|
<TableAction
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ import type { IotSceneRule, RuleSceneApi } from '#/api/iot/rule/scene';
|
||||||
import { computed, nextTick, reactive, ref } from 'vue';
|
import { computed, nextTick, reactive, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenDrawer } from '@vben/common-ui';
|
import { useVbenDrawer } from '@vben/common-ui';
|
||||||
import { CommonStatusEnum } from '@vben/constants';
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
IotRuleSceneActionTypeEnum,
|
||||||
|
IotRuleSceneTriggerTypeEnum,
|
||||||
|
isDeviceTrigger,
|
||||||
|
} from '@vben/constants';
|
||||||
|
|
||||||
import { Form, message } from 'ant-design-vue';
|
import { Form, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
|
@ -14,11 +19,6 @@ import {
|
||||||
updateSceneRule,
|
updateSceneRule,
|
||||||
} from '#/api/iot/rule/scene';
|
} from '#/api/iot/rule/scene';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import {
|
|
||||||
IotRuleSceneActionTypeEnum,
|
|
||||||
IotRuleSceneTriggerTypeEnum,
|
|
||||||
isDeviceTrigger,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ActionSection from '../form/sections/action-section.vue';
|
import ActionSection from '../form/sections/action-section.vue';
|
||||||
import BasicInfoSection from '../form/sections/basic-info-section.vue';
|
import BasicInfoSection from '../form/sections/basic-info-section.vue';
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, getDataTypeOptionsLabel } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { getDataTypeOptionsLabel } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
/** 列表的搜索表单 */
|
/** 列表的搜索表单 */
|
||||||
export function useGridFormSchema(): VbenFormSchema[] {
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, inject } from 'vue';
|
import { computed, inject } from 'vue';
|
||||||
|
|
||||||
import { Page, useVbenModal } from '@vben/common-ui';
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
|
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import { useGridColumns, useGridFormSchema } from './data';
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
import { DataDefinition } from './modules/components';
|
import { DataDefinition } from './modules/components';
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,31 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { Tooltip } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getEventTypeLabel,
|
getEventTypeLabel,
|
||||||
getThingModelServiceCallTypeLabel,
|
getThingModelServiceCallTypeLabel,
|
||||||
IoTDataSpecsDataTypeEnum,
|
IoTDataSpecsDataTypeEnum,
|
||||||
IoTThingModelTypeEnum,
|
IoTThingModelTypeEnum,
|
||||||
} from '#/views/iot/utils/constants';
|
} from '@vben/constants';
|
||||||
|
|
||||||
|
import { Tooltip } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||||
const NUMBER_TYPES = new Set<string>([
|
const NUMBER_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
]);
|
]);
|
||||||
const PLACEHOLDER_TYPES = new Set<string>([
|
const PLACEHOLDER_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
|
||||||
IoTDataSpecsDataTypeEnum.DATE,
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
|
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||||
]);
|
]);
|
||||||
const LIST_TYPES = new Set<string>([
|
const LIST_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.BOOL,
|
IoTDataSpecsDataTypeEnum.BOOL,
|
||||||
IoTDataSpecsDataTypeEnum.ENUM,
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props = defineProps<{ data: ThingModelApi.ThingModel }>();
|
|
||||||
|
|
||||||
const formattedDataSpecsList = computed(() => {
|
const formattedDataSpecsList = computed(() => {
|
||||||
if (!props.data.property?.dataSpecsList?.length) {
|
if (!props.data.property?.dataSpecsList?.length) {
|
||||||
return '';
|
return '';
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,30 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDataTypeOptions,
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Form, Input, Radio } from 'ant-design-vue';
|
import { Form, Input, Radio } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||||
import {
|
|
||||||
getDataTypeOptions,
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ThingModelStructDataSpecs from './struct.vue';
|
import ThingModelStructDataSpecs from './struct.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
/** 数组元素禁止选择的类型 */
|
/** 数组元素禁止选择的类型 */
|
||||||
const EXCLUDED_CHILD_TYPES = new Set<string>([
|
const EXCLUDED_CHILD_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.ENUM,
|
|
||||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||||
IoTDataSpecsDataTypeEnum.DATE,
|
IoTDataSpecsDataTypeEnum.DATE,
|
||||||
|
IoTDataSpecsDataTypeEnum.ENUM,
|
||||||
]);
|
]);
|
||||||
const childDataTypeOptions = getDataTypeOptions().filter(
|
const childDataTypeOptions = getDataTypeOptions().filter(
|
||||||
(item) => !EXCLUDED_CHILD_TYPES.has(item.value),
|
(item) => !EXCLUDED_CHILD_TYPES.has(item.value),
|
||||||
);
|
);
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: any }>();
|
|
||||||
const emits = defineEmits(['update:modelValue']);
|
|
||||||
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
|
const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>;
|
||||||
|
|
||||||
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
|
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import type { Ref } from 'vue';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ThingModelProperty from '../property.vue';
|
import ThingModelProperty from '../property.vue';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ import type { Ref } from 'vue';
|
||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IoTThingModelEventTypeEnum,
|
||||||
|
IoTThingModelParamDirectionEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Form, Radio } from 'ant-design-vue';
|
import { Form, Radio } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||||
import {
|
|
||||||
IoTThingModelEventTypeEnum,
|
|
||||||
IoTThingModelParamDirectionEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ThingModelInputOutputParam from './input-output-param.vue';
|
import ThingModelInputOutputParam from './input-output-param.vue';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, inject, ref } from 'vue';
|
import { computed, inject, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import {
|
||||||
|
DICT_TYPE,
|
||||||
|
IOT_PROVIDE_KEY,
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { $t } from '@vben/locales';
|
import { $t } from '@vben/locales';
|
||||||
import { cloneDeep, isEmpty } from '@vben/utils';
|
import { cloneDeep, isEmpty } from '@vben/utils';
|
||||||
|
|
@ -20,11 +25,6 @@ import {
|
||||||
ThingModelFormRules,
|
ThingModelFormRules,
|
||||||
updateThingModel,
|
updateThingModel,
|
||||||
} from '#/api/iot/thingmodel';
|
} from '#/api/iot/thingmodel';
|
||||||
import {
|
|
||||||
IOT_PROVIDE_KEY,
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ThingModelEvent from './event.vue';
|
import ThingModelEvent from './event.vue';
|
||||||
import ThingModelProperty from './property.vue';
|
import ThingModelProperty from './property.vue';
|
||||||
|
|
@ -51,12 +51,14 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
// 提交表单
|
||||||
const data = cloneDeep(formData.value);
|
const data = cloneDeep(formData.value);
|
||||||
data.productId = product!.value.id;
|
data.productId = product!.value.id;
|
||||||
data.productKey = product!.value.productKey;
|
data.productKey = product!.value.productKey;
|
||||||
fillExtraAttributes(data);
|
fillExtraAttributes(data);
|
||||||
|
try {
|
||||||
await (data.id ? updateThingModel(data) : createThingModel(data));
|
await (data.id ? updateThingModel(data) : createThingModel(data));
|
||||||
|
// 关闭并提示
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
|
@ -68,9 +70,10 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 每次打开都先重置到空白,避免上一次的状态残留
|
// 每次打开都重置;避免上一次的状态残留
|
||||||
formData.value = buildEmptyFormData();
|
formData.value = buildEmptyFormData();
|
||||||
formRef.value?.clearValidate?.();
|
formRef.value?.clearValidate?.();
|
||||||
|
// 加载数据
|
||||||
const data = modalApi.getData<{ id?: number }>();
|
const data = modalApi.getData<{ id?: number }>();
|
||||||
if (!data?.id) {
|
if (!data?.id) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -78,6 +81,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
const result = await getThingModel(data.id);
|
const result = await getThingModel(data.id);
|
||||||
|
// 设置到 values
|
||||||
formData.value = normalizeFormData(result);
|
formData.value = normalizeFormData(result);
|
||||||
} finally {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
|
|
@ -136,14 +140,31 @@ function normalizeFormData(result: ThingModelApi.ThingModel): ThingModelApi.Thin
|
||||||
|
|
||||||
/** 按功能类型将子表单数据回写到顶层,并清理无关分支 */
|
/** 按功能类型将子表单数据回写到顶层,并清理无关分支 */
|
||||||
function fillExtraAttributes(data: any) {
|
function fillExtraAttributes(data: any) {
|
||||||
if (data.type === IoTThingModelTypeEnum.PROPERTY) {
|
switch (data.type) {
|
||||||
|
case IoTThingModelTypeEnum.EVENT: {
|
||||||
|
removeDataSpecs(data.event);
|
||||||
|
data.dataType = data.event.dataType;
|
||||||
|
data.event.identifier = data.identifier;
|
||||||
|
data.event.name = data.name;
|
||||||
|
if (isEmpty(data.event.outputParams)) {
|
||||||
|
delete data.event.outputParams;
|
||||||
|
}
|
||||||
|
delete data.property;
|
||||||
|
delete data.service;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IoTThingModelTypeEnum.PROPERTY: {
|
||||||
removeDataSpecs(data.property);
|
removeDataSpecs(data.property);
|
||||||
data.dataType = data.property.dataType;
|
data.dataType = data.property.dataType;
|
||||||
data.property.identifier = data.identifier;
|
data.property.identifier = data.identifier;
|
||||||
data.property.name = data.name;
|
data.property.name = data.name;
|
||||||
delete data.service;
|
delete data.service;
|
||||||
delete data.event;
|
delete data.event;
|
||||||
} else if (data.type === IoTThingModelTypeEnum.SERVICE) {
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case IoTThingModelTypeEnum.SERVICE: {
|
||||||
removeDataSpecs(data.service);
|
removeDataSpecs(data.service);
|
||||||
data.dataType = data.service.dataType;
|
data.dataType = data.service.dataType;
|
||||||
data.service.identifier = data.identifier;
|
data.service.identifier = data.identifier;
|
||||||
|
|
@ -156,16 +177,10 @@ function fillExtraAttributes(data: any) {
|
||||||
}
|
}
|
||||||
delete data.property;
|
delete data.property;
|
||||||
delete data.event;
|
delete data.event;
|
||||||
} else if (data.type === IoTThingModelTypeEnum.EVENT) {
|
|
||||||
removeDataSpecs(data.event);
|
break;
|
||||||
data.dataType = data.event.dataType;
|
|
||||||
data.event.identifier = data.identifier;
|
|
||||||
data.event.name = data.name;
|
|
||||||
if (isEmpty(data.event.outputParams)) {
|
|
||||||
delete data.event.outputParams;
|
|
||||||
}
|
}
|
||||||
delete data.property;
|
// No default
|
||||||
delete data.service;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import type { Ref } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ThingModelProperty from './property.vue';
|
import ThingModelProperty from './property.vue';
|
||||||
|
|
||||||
|
|
@ -31,6 +31,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
if (!thingModelParams.value) {
|
if (!thingModelParams.value) {
|
||||||
thingModelParams.value = [];
|
thingModelParams.value = [];
|
||||||
}
|
}
|
||||||
|
// 组装表单
|
||||||
const data = formData.value;
|
const data = formData.value;
|
||||||
const item = {
|
const item = {
|
||||||
identifier: data.identifier,
|
identifier: data.identifier,
|
||||||
|
|
@ -48,7 +49,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
? undefined
|
? undefined
|
||||||
: data.property.dataSpecsList,
|
: data.property.dataSpecsList,
|
||||||
};
|
};
|
||||||
// 按 identifier 去重,存在则更新,否则追加
|
// 按 identifier 去重;存在则更新,否则追加
|
||||||
const existingIndex = thingModelParams.value.findIndex(
|
const existingIndex = thingModelParams.value.findIndex(
|
||||||
(spec) => spec.identifier === data.identifier,
|
(spec) => spec.identifier === data.identifier,
|
||||||
);
|
);
|
||||||
|
|
@ -57,6 +58,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
} else {
|
} else {
|
||||||
thingModelParams.value[existingIndex] = item;
|
thingModelParams.value[existingIndex] = item;
|
||||||
}
|
}
|
||||||
|
// 关闭
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
},
|
},
|
||||||
onOpenChange(isOpen: boolean) {
|
onOpenChange(isOpen: boolean) {
|
||||||
|
|
@ -65,10 +67,12 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
}
|
}
|
||||||
formData.value = buildEmptyFormData();
|
formData.value = buildEmptyFormData();
|
||||||
paramFormRef.value?.clearValidate?.();
|
paramFormRef.value?.clearValidate?.();
|
||||||
|
// 加载数据
|
||||||
const data = modalApi.getData<any>();
|
const data = modalApi.getData<any>();
|
||||||
if (isEmpty(data)) {
|
if (isEmpty(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 设置到 values
|
||||||
formData.value = {
|
formData.value = {
|
||||||
identifier: data.identifier ?? '',
|
identifier: data.identifier ?? '',
|
||||||
name: data.name ?? '',
|
name: data.name ?? '',
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,17 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
import { computed, watch } from 'vue';
|
import { computed, watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getDataTypeOptions,
|
||||||
|
IoTDataSpecsDataTypeEnum,
|
||||||
|
IoTThingModelAccessModeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Form, Input, Radio, Select } from 'ant-design-vue';
|
import { Form, Input, Radio, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ThingModelFormRules, validateBoolName } from '#/api/iot/thingmodel';
|
import { ThingModelFormRules, validateBoolName } from '#/api/iot/thingmodel';
|
||||||
import {
|
|
||||||
getDataTypeOptions,
|
|
||||||
IoTDataSpecsDataTypeEnum,
|
|
||||||
IoTThingModelAccessModeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ThingModelArrayDataSpecs,
|
ThingModelArrayDataSpecs,
|
||||||
|
|
@ -25,6 +25,12 @@ import {
|
||||||
ThingModelStructDataSpecs,
|
ThingModelStructDataSpecs,
|
||||||
} from './data-specs';
|
} from './data-specs';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
isParams?: boolean;
|
||||||
|
isStructDataSpecs?: boolean;
|
||||||
|
modelValue: any;
|
||||||
|
}>();
|
||||||
|
const emits = defineEmits(['update:modelValue']);
|
||||||
/** 嵌套在结构体里时,禁止再选数组 / 结构体(最多支持两层嵌套) */
|
/** 嵌套在结构体里时,禁止再选数组 / 结构体(最多支持两层嵌套) */
|
||||||
const NESTED_EXCLUDED_TYPES = new Set<string>([
|
const NESTED_EXCLUDED_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||||
|
|
@ -35,17 +41,11 @@ const STRUCT_CHILD_OPTIONS = getDataTypeOptions().filter(
|
||||||
);
|
);
|
||||||
/** 数值型数据类型集合 */
|
/** 数值型数据类型集合 */
|
||||||
const NUMERIC_TYPES = new Set<string>([
|
const NUMERIC_TYPES = new Set<string>([
|
||||||
IoTDataSpecsDataTypeEnum.INT,
|
|
||||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||||
|
IoTDataSpecsDataTypeEnum.INT,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
isParams?: boolean;
|
|
||||||
isStructDataSpecs?: boolean;
|
|
||||||
modelValue: any;
|
|
||||||
}>();
|
|
||||||
const emits = defineEmits(['update:modelValue']);
|
|
||||||
const property = useVModel(
|
const property = useVModel(
|
||||||
props,
|
props,
|
||||||
'modelValue',
|
'modelValue',
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ import type { Ref } from 'vue';
|
||||||
|
|
||||||
import { watch } from 'vue';
|
import { watch } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IoTThingModelParamDirectionEnum,
|
||||||
|
IoTThingModelServiceCallTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { isEmpty } from '@vben/utils';
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core';
|
import { useVModel } from '@vueuse/core';
|
||||||
import { Form, Radio } from 'ant-design-vue';
|
import { Form, Radio } from 'ant-design-vue';
|
||||||
|
|
||||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||||
import {
|
|
||||||
IoTThingModelParamDirectionEnum,
|
|
||||||
IoTThingModelServiceCallTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ThingModelInputOutputParam from './input-output-param.vue';
|
import ThingModelInputOutputParam from './input-output-param.vue';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import type { IotProductApi } from '#/api/iot/product/product';
|
||||||
import { computed, inject, ref, watch } from 'vue';
|
import { computed, inject, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||||
|
|
||||||
import { Radio, Textarea } from 'ant-design-vue';
|
import { Radio, Textarea } from 'ant-design-vue';
|
||||||
|
|
||||||
import { getThingModelTSL } from '#/api/iot/thingmodel';
|
import { getThingModelTSL } from '#/api/iot/thingmodel';
|
||||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
|
const product = inject<Ref<IotProductApi.Product>>(IOT_PROVIDE_KEY.PRODUCT);
|
||||||
|
|
||||||
|
|
@ -25,7 +25,9 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
}
|
}
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
|
// 加载数据
|
||||||
thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0);
|
thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0);
|
||||||
|
// 设置到 values
|
||||||
tslString.value = JSON.stringify(thingModelTSL.value, null, 2);
|
tslString.value = JSON.stringify(thingModelTSL.value, null, 2);
|
||||||
} finally {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
|
|
@ -38,12 +40,12 @@ const formattedTSL = computed(() =>
|
||||||
JSON.stringify(thingModelTSL.value, null, 2),
|
JSON.stringify(thingModelTSL.value, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 编辑器内容变化时,同步到数据对象 */
|
/** 编辑器内容变化时,同步到数据对象;编辑过程中 JSON 可能是中间态,解析失败保留原值 */
|
||||||
watch(tslString, (newValue) => {
|
watch(tslString, (newValue) => {
|
||||||
try {
|
try {
|
||||||
thingModelTSL.value = JSON.parse(newValue);
|
thingModelTSL.value = JSON.parse(newValue);
|
||||||
} catch {
|
} catch {
|
||||||
// JSON 解析失败时保持原值
|
// 中间态忽略
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -57,14 +59,12 @@ watch(tslString, (newValue) => {
|
||||||
<Radio.Button value="editor">编辑器视图</Radio.Button>
|
<Radio.Button value="editor">编辑器视图</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</div>
|
</div>
|
||||||
<!-- 代码视图:只读展示 -->
|
<!-- 代码视图:只读展示(pre / code 必须紧贴,避免显示出空白) -->
|
||||||
<div
|
<div
|
||||||
v-if="viewMode === 'view'"
|
v-if="viewMode === 'view'"
|
||||||
class="max-h-[600px] overflow-y-auto rounded border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
|
class="max-h-[600px] overflow-y-auto rounded border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
<pre
|
<pre class="m-0 whitespace-pre-wrap break-words font-mono text-[13px] leading-normal"><code>{{ formattedTSL }}</code></pre>
|
||||||
class="m-0 whitespace-pre-wrap break-words font-mono text-[13px] leading-normal"
|
|
||||||
><code>{{ formattedTSL }}</code></pre>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 编辑器视图:可编辑 -->
|
<!-- 编辑器视图:可编辑 -->
|
||||||
<Textarea
|
<Textarea
|
||||||
|
|
|
||||||
|
|
@ -1,653 +0,0 @@
|
||||||
// TODO @AI:感觉这块,放到 biz-iot-enum 里好点。
|
|
||||||
|
|
||||||
import { isEmpty } from '@vben/utils';
|
|
||||||
|
|
||||||
/** IoT 依赖注入 KEY */
|
|
||||||
export const IOT_PROVIDE_KEY = {
|
|
||||||
PRODUCT: 'IOT_PRODUCT',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** IoT 设备状态枚举 */
|
|
||||||
export enum DeviceStateEnum {
|
|
||||||
INACTIVE = 0, // 未激活
|
|
||||||
ONLINE = 1, // 在线
|
|
||||||
OFFLINE = 2, // 离线
|
|
||||||
}
|
|
||||||
|
|
||||||
/** IoT 产品物模型类型枚举类 */
|
|
||||||
export const IoTThingModelTypeEnum = {
|
|
||||||
PROPERTY: 1, // 属性
|
|
||||||
SERVICE: 2, // 服务
|
|
||||||
EVENT: 3, // 事件
|
|
||||||
};
|
|
||||||
|
|
||||||
// IoT 产品物模型服务调用方式枚举
|
|
||||||
export const IoTThingModelServiceCallTypeEnum = {
|
|
||||||
ASYNC: {
|
|
||||||
label: '异步',
|
|
||||||
value: 'async',
|
|
||||||
},
|
|
||||||
SYNC: {
|
|
||||||
label: '同步',
|
|
||||||
value: 'sync',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export const getThingModelServiceCallTypeLabel = (
|
|
||||||
value: string,
|
|
||||||
): string | undefined =>
|
|
||||||
Object.values(IoTThingModelServiceCallTypeEnum).find(
|
|
||||||
(type) => type.value === value,
|
|
||||||
)?.label;
|
|
||||||
|
|
||||||
// IoT 产品物模型事件类型枚举
|
|
||||||
export const IoTThingModelEventTypeEnum = {
|
|
||||||
INFO: {
|
|
||||||
label: '信息',
|
|
||||||
value: 'info',
|
|
||||||
},
|
|
||||||
ALERT: {
|
|
||||||
label: '告警',
|
|
||||||
value: 'alert',
|
|
||||||
},
|
|
||||||
ERROR: {
|
|
||||||
label: '故障',
|
|
||||||
value: 'error',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export const getEventTypeLabel = (value: string): string | undefined =>
|
|
||||||
Object.values(IoTThingModelEventTypeEnum).find((type) => type.value === value)
|
|
||||||
?.label;
|
|
||||||
|
|
||||||
// IoT 产品物模型参数是输入参数还是输出参数
|
|
||||||
export const IoTThingModelParamDirectionEnum = {
|
|
||||||
INPUT: 'input', // 输入参数
|
|
||||||
OUTPUT: 'output', // 输出参数
|
|
||||||
};
|
|
||||||
|
|
||||||
// IoT 产品物模型访问模式枚举类
|
|
||||||
export const IoTThingModelAccessModeEnum = {
|
|
||||||
READ_WRITE: {
|
|
||||||
label: '读写',
|
|
||||||
value: 'rw',
|
|
||||||
},
|
|
||||||
READ_ONLY: {
|
|
||||||
label: '只读',
|
|
||||||
value: 'r',
|
|
||||||
},
|
|
||||||
WRITE_ONLY: {
|
|
||||||
label: '只写',
|
|
||||||
value: 'w',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 获取访问模式标签 */
|
|
||||||
export const getAccessModeLabel = (value: string): string => {
|
|
||||||
const mode = Object.values(IoTThingModelAccessModeEnum).find(
|
|
||||||
(mode) => mode.value === value,
|
|
||||||
);
|
|
||||||
return mode?.label || value;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 属性值的数据类型 */
|
|
||||||
export const IoTDataSpecsDataTypeEnum = {
|
|
||||||
INT: 'int',
|
|
||||||
FLOAT: 'float',
|
|
||||||
DOUBLE: 'double',
|
|
||||||
ENUM: 'enum',
|
|
||||||
BOOL: 'bool',
|
|
||||||
TEXT: 'text',
|
|
||||||
DATE: 'date',
|
|
||||||
STRUCT: 'struct',
|
|
||||||
ARRAY: 'array',
|
|
||||||
};
|
|
||||||
|
|
||||||
const DATA_TYPE_OPTIONS = Object.freeze([
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.INT, label: '整数型' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.FLOAT, label: '单精度浮点型' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.DOUBLE, label: '双精度浮点型' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.ENUM, label: '枚举型' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.BOOL, label: '布尔型' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.TEXT, label: '文本型' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.DATE, label: '时间型' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.STRUCT, label: '结构体' },
|
|
||||||
{ value: IoTDataSpecsDataTypeEnum.ARRAY, label: '数组' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const getDataTypeOptions = () => DATA_TYPE_OPTIONS;
|
|
||||||
|
|
||||||
/** 获得物体模型数据类型配置项名称 */
|
|
||||||
export const getDataTypeOptionsLabel = (value: string) => {
|
|
||||||
if (isEmpty(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
const dataType = getDataTypeOptions().find(
|
|
||||||
(option) => option.value === value,
|
|
||||||
);
|
|
||||||
return dataType && `${dataType.value}(${dataType.label})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 获取数据类型显示名称(用于属性选择器) */
|
|
||||||
export const getDataTypeName = (dataType: string): string => {
|
|
||||||
const typeMap: Record<string, string> = {
|
|
||||||
[IoTDataSpecsDataTypeEnum.INT]: '整数',
|
|
||||||
[IoTDataSpecsDataTypeEnum.FLOAT]: '浮点数',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DOUBLE]: '双精度',
|
|
||||||
[IoTDataSpecsDataTypeEnum.TEXT]: '字符串',
|
|
||||||
[IoTDataSpecsDataTypeEnum.BOOL]: '布尔值',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ENUM]: '枚举',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DATE]: '日期',
|
|
||||||
[IoTDataSpecsDataTypeEnum.STRUCT]: '结构体',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ARRAY]: '数组',
|
|
||||||
};
|
|
||||||
return typeMap[dataType] || dataType;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 获取数据类型标签颜色(antd Tag `color`) */
|
|
||||||
export const getDataTypeTagColor = (
|
|
||||||
dataType: string,
|
|
||||||
): 'default' | 'error' | 'processing' | 'success' | 'warning' => {
|
|
||||||
const tagMap: Record<
|
|
||||||
string,
|
|
||||||
'default' | 'error' | 'processing' | 'success' | 'warning'
|
|
||||||
> = {
|
|
||||||
[IoTDataSpecsDataTypeEnum.INT]: 'processing',
|
|
||||||
[IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
|
|
||||||
[IoTDataSpecsDataTypeEnum.TEXT]: 'default',
|
|
||||||
[IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ENUM]: 'error',
|
|
||||||
[IoTDataSpecsDataTypeEnum.DATE]: 'processing',
|
|
||||||
[IoTDataSpecsDataTypeEnum.STRUCT]: 'default',
|
|
||||||
[IoTDataSpecsDataTypeEnum.ARRAY]: 'warning',
|
|
||||||
};
|
|
||||||
return tagMap[dataType] || 'default';
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 物模型组标签常量 */
|
|
||||||
export const THING_MODEL_GROUP_LABELS = {
|
|
||||||
PROPERTY: '设备属性',
|
|
||||||
EVENT: '设备事件',
|
|
||||||
SERVICE: '设备服务',
|
|
||||||
};
|
|
||||||
|
|
||||||
// IoT OTA 任务设备范围枚举
|
|
||||||
export const IoTOtaTaskDeviceScopeEnum = {
|
|
||||||
ALL: {
|
|
||||||
label: '全部设备',
|
|
||||||
value: 1,
|
|
||||||
},
|
|
||||||
SELECT: {
|
|
||||||
label: '指定设备',
|
|
||||||
value: 2,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// IoT OTA 任务状态枚举
|
|
||||||
export const IoTOtaTaskStatusEnum = {
|
|
||||||
IN_PROGRESS: {
|
|
||||||
label: '进行中',
|
|
||||||
value: 10,
|
|
||||||
},
|
|
||||||
END: {
|
|
||||||
label: '已结束',
|
|
||||||
value: 20,
|
|
||||||
},
|
|
||||||
CANCELED: {
|
|
||||||
label: '已取消',
|
|
||||||
value: 30,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// IoT OTA 升级记录状态枚举
|
|
||||||
export const IoTOtaTaskRecordStatusEnum = {
|
|
||||||
PENDING: {
|
|
||||||
label: '待推送',
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
PUSHED: {
|
|
||||||
label: '已推送',
|
|
||||||
value: 10,
|
|
||||||
},
|
|
||||||
UPGRADING: {
|
|
||||||
label: '升级中',
|
|
||||||
value: 20,
|
|
||||||
},
|
|
||||||
SUCCESS: {
|
|
||||||
label: '升级成功',
|
|
||||||
value: 30,
|
|
||||||
},
|
|
||||||
FAILURE: {
|
|
||||||
label: '升级失败',
|
|
||||||
value: 40,
|
|
||||||
},
|
|
||||||
CANCELED: {
|
|
||||||
label: '升级取消',
|
|
||||||
value: 50,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== 场景联动规则相关常量 ==========
|
|
||||||
|
|
||||||
/** IoT 场景联动触发器类型枚举 */
|
|
||||||
export const IotRuleSceneTriggerTypeEnum = {
|
|
||||||
DEVICE_STATE_UPDATE: 1, // 设备上下线变更
|
|
||||||
DEVICE_PROPERTY_POST: 2, // 物模型属性上报
|
|
||||||
DEVICE_EVENT_POST: 3, // 设备事件上报
|
|
||||||
DEVICE_SERVICE_INVOKE: 4, // 设备服务调用
|
|
||||||
TIMER: 100, // 定时触发
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 触发器类型选项配置 */
|
|
||||||
export const triggerTypeOptions = [
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
|
|
||||||
label: '设备状态变更',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
|
||||||
label: '设备属性上报',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
|
|
||||||
label: '设备事件上报',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
|
|
||||||
label: '设备服务调用',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerTypeEnum.TIMER,
|
|
||||||
label: '定时触发',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** 判断是否为设备触发器类型 */
|
|
||||||
export function isDeviceTrigger(type: number): boolean {
|
|
||||||
const deviceTriggerTypes = [
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
|
|
||||||
IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
|
|
||||||
] as number[];
|
|
||||||
return deviceTriggerTypes.includes(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== 场景联动规则执行器相关常量 ==========
|
|
||||||
|
|
||||||
/** IoT 场景联动执行器类型枚举 */
|
|
||||||
export const IotRuleSceneActionTypeEnum = {
|
|
||||||
DEVICE_PROPERTY_SET: 1, // 设备属性设置
|
|
||||||
DEVICE_SERVICE_INVOKE: 2, // 设备服务调用
|
|
||||||
ALERT_TRIGGER: 100, // 告警触发
|
|
||||||
ALERT_RECOVER: 101, // 告警恢复
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 执行器类型选项配置 */
|
|
||||||
export const getActionTypeOptions = () => [
|
|
||||||
{
|
|
||||||
value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
|
|
||||||
label: '设备属性设置',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
|
|
||||||
label: '设备服务调用',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
|
|
||||||
label: '触发告警',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneActionTypeEnum.ALERT_RECOVER,
|
|
||||||
label: '恢复告警',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** 获取执行器类型标签 */
|
|
||||||
export const getActionTypeLabel = (type: number): string => {
|
|
||||||
const option = getActionTypeOptions().find((opt) => opt.value === type);
|
|
||||||
return option?.label || '未知类型';
|
|
||||||
};
|
|
||||||
|
|
||||||
/** IoT 场景联动触发条件参数操作符枚举 */
|
|
||||||
export const IotRuleSceneTriggerConditionParameterOperatorEnum = {
|
|
||||||
EQUALS: { name: '等于', value: '=' }, // 等于
|
|
||||||
NOT_EQUALS: { name: '不等于', value: '!=' }, // 不等于
|
|
||||||
GREATER_THAN: { name: '大于', value: '>' }, // 大于
|
|
||||||
GREATER_THAN_OR_EQUALS: { name: '大于等于', value: '>=' }, // 大于等于
|
|
||||||
LESS_THAN: { name: '小于', value: '<' }, // 小于
|
|
||||||
LESS_THAN_OR_EQUALS: { name: '小于等于', value: '<=' }, // 小于等于
|
|
||||||
IN: { name: '在...之中', value: 'in' }, // 在...之中
|
|
||||||
NOT_IN: { name: '不在...之中', value: 'not in' }, // 不在...之中
|
|
||||||
BETWEEN: { name: '在...之间', value: 'between' }, // 在...之间
|
|
||||||
NOT_BETWEEN: { name: '不在...之间', value: 'not between' }, // 不在...之间
|
|
||||||
LIKE: { name: '字符串匹配', value: 'like' }, // 字符串匹配
|
|
||||||
NOT_NULL: { name: '非空', value: 'not null' }, // 非空
|
|
||||||
};
|
|
||||||
|
|
||||||
/** IoT 场景联动触发条件类型枚举 */
|
|
||||||
export const IotRuleSceneTriggerConditionTypeEnum = {
|
|
||||||
DEVICE_STATUS: 1, // 设备状态
|
|
||||||
DEVICE_PROPERTY: 2, // 设备属性
|
|
||||||
CURRENT_TIME: 3, // 当前时间
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 获取条件类型选项 */
|
|
||||||
export const getConditionTypeOptions = () => [
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS,
|
|
||||||
label: '设备状态',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY,
|
|
||||||
label: '设备属性',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME,
|
|
||||||
label: '当前时间',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** 设备状态枚举 - 统一的设备状态管理 */
|
|
||||||
export const IoTDeviceStatusEnum = {
|
|
||||||
// 在线状态
|
|
||||||
ONLINE: {
|
|
||||||
label: '在线',
|
|
||||||
value: 'online',
|
|
||||||
tagType: 'success',
|
|
||||||
},
|
|
||||||
OFFLINE: {
|
|
||||||
label: '离线',
|
|
||||||
value: 'offline',
|
|
||||||
tagType: 'danger',
|
|
||||||
},
|
|
||||||
// 启用状态
|
|
||||||
ENABLED: {
|
|
||||||
label: '正常',
|
|
||||||
value: 0,
|
|
||||||
value2: 'enabled',
|
|
||||||
tagType: 'success',
|
|
||||||
},
|
|
||||||
DISABLED: {
|
|
||||||
label: '禁用',
|
|
||||||
value: 1,
|
|
||||||
value2: 'disabled',
|
|
||||||
tagType: 'danger',
|
|
||||||
},
|
|
||||||
// 激活状态
|
|
||||||
ACTIVATED: {
|
|
||||||
label: '已激活',
|
|
||||||
value2: 'activated',
|
|
||||||
tagType: 'success',
|
|
||||||
},
|
|
||||||
NOT_ACTIVATED: {
|
|
||||||
label: '未激活',
|
|
||||||
value2: 'not_activated',
|
|
||||||
tagType: 'info',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 设备选择器特殊选项 */
|
|
||||||
export const DEVICE_SELECTOR_OPTIONS = {
|
|
||||||
ALL_DEVICES: {
|
|
||||||
id: 0,
|
|
||||||
deviceName: '全部设备',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** IoT 场景联动触发时间操作符枚举 */
|
|
||||||
export const IotRuleSceneTriggerTimeOperatorEnum = {
|
|
||||||
BEFORE_TIME: { name: '在时间之前', value: 'before_time' }, // 在时间之前
|
|
||||||
AFTER_TIME: { name: '在时间之后', value: 'after_time' }, // 在时间之后
|
|
||||||
BETWEEN_TIME: { name: '在时间之间', value: 'between_time' }, // 在时间之间
|
|
||||||
AT_TIME: { name: '在指定时间', value: 'at_time' }, // 在指定时间
|
|
||||||
BEFORE_TODAY: { name: '在今日之前', value: 'before_today' }, // 在今日之前
|
|
||||||
AFTER_TODAY: { name: '在今日之后', value: 'after_today' }, // 在今日之后
|
|
||||||
TODAY: { name: '在今日之间', value: 'today' }, // 在今日之间
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 获取触发器类型标签 */
|
|
||||||
export const getTriggerTypeLabel = (type: number): string => {
|
|
||||||
const option = triggerTypeOptions.find((item) => item.value === type);
|
|
||||||
return option?.label || '未知类型';
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== JSON 参数输入组件相关常量 ==========
|
|
||||||
|
|
||||||
/** JSON 参数输入组件类型枚举 */
|
|
||||||
export const JsonParamsInputTypeEnum = {
|
|
||||||
SERVICE: 'service',
|
|
||||||
EVENT: 'event',
|
|
||||||
PROPERTY: 'property',
|
|
||||||
CUSTOM: 'custom',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** JSON 参数输入组件类型 */
|
|
||||||
export type JsonParamsInputType =
|
|
||||||
(typeof JsonParamsInputTypeEnum)[keyof typeof JsonParamsInputTypeEnum];
|
|
||||||
|
|
||||||
/** JSON 参数输入组件文本常量 */
|
|
||||||
export const JSON_PARAMS_INPUT_CONSTANTS = {
|
|
||||||
// 基础文本
|
|
||||||
PLACEHOLDER: '请输入JSON格式的参数',
|
|
||||||
JSON_FORMAT_CORRECT: 'JSON 格式正确',
|
|
||||||
QUICK_FILL_LABEL: '快速填充:',
|
|
||||||
EXAMPLE_DATA_BUTTON: '示例数据',
|
|
||||||
CLEAR_BUTTON: '清空',
|
|
||||||
VIEW_EXAMPLE_TITLE: '查看参数示例',
|
|
||||||
COMPLETE_JSON_FORMAT: '完整 JSON 格式:',
|
|
||||||
REQUIRED_TAG: '必填',
|
|
||||||
|
|
||||||
// 错误信息
|
|
||||||
PARAMS_MUST_BE_OBJECT: '参数必须是一个有效的 JSON 对象',
|
|
||||||
PARAM_REQUIRED_ERROR: (paramName: string) => `参数 ${paramName} 为必填项`,
|
|
||||||
JSON_FORMAT_ERROR: (error: string) => `JSON格式错误: ${error}`,
|
|
||||||
UNKNOWN_ERROR: '未知错误',
|
|
||||||
|
|
||||||
// 类型相关标题
|
|
||||||
TITLES: {
|
|
||||||
SERVICE: (name?: string) => `${name || '服务'} - 输入参数示例`,
|
|
||||||
EVENT: (name?: string) => `${name || '事件'} - 输出参数示例`,
|
|
||||||
PROPERTY: '属性设置 - 参数示例',
|
|
||||||
CUSTOM: (name?: string) => `${name || '自定义'} - 参数示例`,
|
|
||||||
DEFAULT: '参数示例',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 参数标签
|
|
||||||
PARAMS_LABELS: {
|
|
||||||
SERVICE: '输入参数',
|
|
||||||
EVENT: '输出参数',
|
|
||||||
PROPERTY: '属性参数',
|
|
||||||
CUSTOM: '参数列表',
|
|
||||||
DEFAULT: '参数',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 空状态消息
|
|
||||||
EMPTY_MESSAGES: {
|
|
||||||
SERVICE: '此服务无需输入参数',
|
|
||||||
EVENT: '此事件无输出参数',
|
|
||||||
PROPERTY: '无可设置的属性',
|
|
||||||
CUSTOM: '无参数配置',
|
|
||||||
DEFAULT: '无参数',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 无配置消息
|
|
||||||
NO_CONFIG_MESSAGES: {
|
|
||||||
SERVICE: '请先选择服务',
|
|
||||||
EVENT: '请先选择事件',
|
|
||||||
PROPERTY: '请先选择产品',
|
|
||||||
CUSTOM: '请先进行配置',
|
|
||||||
DEFAULT: '请先进行配置',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** JSON 参数输入组件图标常量 */
|
|
||||||
export const JSON_PARAMS_INPUT_ICONS = {
|
|
||||||
// 标题图标
|
|
||||||
TITLE_ICONS: {
|
|
||||||
SERVICE: 'ep:service',
|
|
||||||
EVENT: 'ep:bell',
|
|
||||||
PROPERTY: 'ep:edit',
|
|
||||||
CUSTOM: 'ep:document',
|
|
||||||
DEFAULT: 'ep:document',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 参数图标
|
|
||||||
PARAMS_ICONS: {
|
|
||||||
SERVICE: 'ep:edit',
|
|
||||||
EVENT: 'ep:upload',
|
|
||||||
PROPERTY: 'ep:setting',
|
|
||||||
CUSTOM: 'ep:list',
|
|
||||||
DEFAULT: 'ep:edit',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 状态图标
|
|
||||||
STATUS_ICONS: {
|
|
||||||
ERROR: 'ep:warning',
|
|
||||||
SUCCESS: 'ep:circle-check',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/** JSON 参数输入组件示例值常量 */
|
|
||||||
export const JSON_PARAMS_EXAMPLE_VALUES: Record<string, any> = {
|
|
||||||
[IoTDataSpecsDataTypeEnum.INT]: { display: '25', value: 25 },
|
|
||||||
[IoTDataSpecsDataTypeEnum.FLOAT]: { display: '25.5', value: 25.5 },
|
|
||||||
[IoTDataSpecsDataTypeEnum.DOUBLE]: { display: '25.5', value: 25.5 },
|
|
||||||
[IoTDataSpecsDataTypeEnum.BOOL]: { display: 'false', value: false },
|
|
||||||
[IoTDataSpecsDataTypeEnum.TEXT]: { display: '"auto"', value: 'auto' },
|
|
||||||
[IoTDataSpecsDataTypeEnum.ENUM]: { display: '"option1"', value: 'option1' },
|
|
||||||
[IoTDataSpecsDataTypeEnum.STRUCT]: { display: '{}', value: {} },
|
|
||||||
[IoTDataSpecsDataTypeEnum.ARRAY]: { display: '[]', value: [] },
|
|
||||||
DEFAULT: { display: '""', value: '' },
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== Modbus 通用常量 ==========
|
|
||||||
|
|
||||||
/** Modbus 模式枚举 */
|
|
||||||
export const ModbusModeEnum = {
|
|
||||||
POLLING: 1, // 云端轮询
|
|
||||||
ACTIVE_REPORT: 2, // 主动上报
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Modbus 帧格式枚举 */
|
|
||||||
export const ModbusFrameFormatEnum = {
|
|
||||||
MODBUS_TCP: 1, // Modbus TCP
|
|
||||||
MODBUS_RTU: 2, // Modbus RTU
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Modbus 功能码枚举 */
|
|
||||||
export const ModbusFunctionCodeEnum = {
|
|
||||||
READ_COILS: 1, // 读线圈
|
|
||||||
READ_DISCRETE_INPUTS: 2, // 读离散输入
|
|
||||||
READ_HOLDING_REGISTERS: 3, // 读保持寄存器
|
|
||||||
READ_INPUT_REGISTERS: 4, // 读输入寄存器
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Modbus 功能码选项 */
|
|
||||||
export const ModbusFunctionCodeOptions = [
|
|
||||||
{ value: 1, label: '01 - 读线圈 (Coils)', description: '可读写布尔值' },
|
|
||||||
{
|
|
||||||
value: 2,
|
|
||||||
label: '02 - 读离散输入 (Discrete Inputs)',
|
|
||||||
description: '只读布尔值',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 3,
|
|
||||||
label: '03 - 读保持寄存器 (Holding Registers)',
|
|
||||||
description: '可读写 16 位数据',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 4,
|
|
||||||
label: '04 - 读输入寄存器 (Input Registers)',
|
|
||||||
description: '只读 16 位数据',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Modbus 原始数据类型枚举 */
|
|
||||||
export const ModbusRawDataTypeEnum = {
|
|
||||||
INT16: 'INT16',
|
|
||||||
UINT16: 'UINT16',
|
|
||||||
INT32: 'INT32',
|
|
||||||
UINT32: 'UINT32',
|
|
||||||
FLOAT: 'FLOAT',
|
|
||||||
DOUBLE: 'DOUBLE',
|
|
||||||
BOOLEAN: 'BOOLEAN',
|
|
||||||
STRING: 'STRING',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Modbus 原始数据类型选项 */
|
|
||||||
export const ModbusRawDataTypeOptions = [
|
|
||||||
{
|
|
||||||
value: 'INT16',
|
|
||||||
label: 'INT16',
|
|
||||||
description: '有符号16位整数',
|
|
||||||
registerCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'UINT16',
|
|
||||||
label: 'UINT16',
|
|
||||||
description: '无符号16位整数',
|
|
||||||
registerCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'INT32',
|
|
||||||
label: 'INT32',
|
|
||||||
description: '有符号32位整数',
|
|
||||||
registerCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'UINT32',
|
|
||||||
label: 'UINT32',
|
|
||||||
description: '无符号32位整数',
|
|
||||||
registerCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'FLOAT',
|
|
||||||
label: 'FLOAT',
|
|
||||||
description: '32位浮点数',
|
|
||||||
registerCount: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'DOUBLE',
|
|
||||||
label: 'DOUBLE',
|
|
||||||
description: '64位浮点数',
|
|
||||||
registerCount: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'BOOLEAN',
|
|
||||||
label: 'BOOLEAN',
|
|
||||||
description: '布尔值',
|
|
||||||
registerCount: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'STRING',
|
|
||||||
label: 'STRING',
|
|
||||||
description: '字符串',
|
|
||||||
registerCount: 0,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Modbus 字节序选项 - 16位 */
|
|
||||||
export const ModbusByteOrder16Options = [
|
|
||||||
{ value: 'AB', label: 'AB', description: '大端序' },
|
|
||||||
{ value: 'BA', label: 'BA', description: '小端序' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Modbus 字节序选项 - 32位 */
|
|
||||||
export const ModbusByteOrder32Options = [
|
|
||||||
{ value: 'ABCD', label: 'ABCD', description: '大端序' },
|
|
||||||
{ value: 'CDAB', label: 'CDAB', description: '大端字交换' },
|
|
||||||
{ value: 'DCBA', label: 'DCBA', description: '小端序' },
|
|
||||||
{ value: 'BADC', label: 'BADC', description: '小端字交换' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/** 根据数据类型获取字节序选项 */
|
|
||||||
export const getByteOrderOptions = (rawDataType: string) => {
|
|
||||||
if (['FLOAT', 'INT32', 'UINT32'].includes(rawDataType)) {
|
|
||||||
return ModbusByteOrder32Options;
|
|
||||||
}
|
|
||||||
if (rawDataType === 'DOUBLE') {
|
|
||||||
// 64 位暂时复用 32 位字节序
|
|
||||||
return ModbusByteOrder32Options;
|
|
||||||
}
|
|
||||||
return ModbusByteOrder16Options;
|
|
||||||
};
|
|
||||||
|
|
@ -22,6 +22,9 @@ VITE_APP_DOCALERT_ENABLE=true
|
||||||
# 百度统计
|
# 百度统计
|
||||||
VITE_APP_BAIDU_CODE = b79d8f49e2d38b26503b92810b740f45
|
VITE_APP_BAIDU_CODE = b79d8f49e2d38b26503b92810b740f45
|
||||||
|
|
||||||
|
# 百度地图
|
||||||
|
VITE_BAIDU_MAP_KEY=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN
|
||||||
|
|
||||||
# GoView域名
|
# GoView域名
|
||||||
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
VITE_GOVIEW_URL='http://127.0.0.1:3000'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
|
const routes: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
path: '/iot',
|
||||||
|
name: 'IoTCenter',
|
||||||
|
meta: {
|
||||||
|
title: 'IoT 物联网',
|
||||||
|
icon: 'lucide:cpu',
|
||||||
|
keepAlive: true,
|
||||||
|
hideInMenu: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'product/detail/:id',
|
||||||
|
name: 'IoTProductDetail',
|
||||||
|
meta: {
|
||||||
|
title: '产品详情',
|
||||||
|
activePath: '/iot/device/product',
|
||||||
|
},
|
||||||
|
component: () => import('#/views/iot/product/product/detail/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'device/detail/:id',
|
||||||
|
name: 'IoTDeviceDetail',
|
||||||
|
meta: {
|
||||||
|
title: '设备详情',
|
||||||
|
activePath: '/iot/device/device',
|
||||||
|
},
|
||||||
|
component: () => import('#/views/iot/device/device/detail/index.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ota/firmware/detail/:id',
|
||||||
|
name: 'IoTOtaFirmwareDetail',
|
||||||
|
meta: {
|
||||||
|
title: '固件详情',
|
||||||
|
activePath: '/iot/ota',
|
||||||
|
},
|
||||||
|
component: () =>
|
||||||
|
import('#/views/iot/ota/firmware/detail/index.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
|
|
@ -120,14 +120,15 @@ export function useGridColumns(): VxeTableGridOptions<AlertRecordApi.AlertRecord
|
||||||
title: '产品名称',
|
title: '产品名称',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
formatter: ({ cellValue }) =>
|
formatter: ({ cellValue }) =>
|
||||||
productList.find((p) => p.id === cellValue)?.name || '-',
|
productList.find((product) => product.id === cellValue)?.name || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceId',
|
field: 'deviceId',
|
||||||
title: '设备名称',
|
title: '设备名称',
|
||||||
minWidth: 120,
|
minWidth: 120,
|
||||||
formatter: ({ cellValue }) =>
|
formatter: ({ cellValue }) =>
|
||||||
deviceList.find((d) => d.id === cellValue)?.deviceName || '-',
|
deviceList.find((device) => device.id === cellValue)?.deviceName ||
|
||||||
|
'-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'deviceMessage',
|
field: 'deviceMessage',
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
||||||
api: getSimpleDeviceGroupList,
|
api: getSimpleDeviceGroupList,
|
||||||
labelField: 'name',
|
labelField: 'name',
|
||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
mode: 'multiple',
|
multiple: true,
|
||||||
placeholder: '请选择设备分组',
|
placeholder: '请选择设备分组',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -168,7 +168,7 @@ export function useGroupFormSchema(): VbenFormSchema[] {
|
||||||
api: getSimpleDeviceGroupList,
|
api: getSimpleDeviceGroupList,
|
||||||
labelField: 'name',
|
labelField: 'name',
|
||||||
valueField: 'id',
|
valueField: 'id',
|
||||||
mode: 'multiple',
|
multiple: true,
|
||||||
placeholder: '请选择设备分组',
|
placeholder: '请选择设备分组',
|
||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
|
|
|
||||||
|
|
@ -30,17 +30,13 @@ const props = defineProps<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/** 查询参数 */
|
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
method: undefined,
|
method: undefined,
|
||||||
upstream: undefined,
|
upstream: undefined,
|
||||||
});
|
}); // 查询参数
|
||||||
|
|
||||||
// TODO @AI:变量的注释,写在 // 尾注释。别的模块也看看。
|
const autoRefresh = ref(false); // 自动刷新开关
|
||||||
/** 自动刷新开关 */
|
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||||
const autoRefresh = ref(false);
|
|
||||||
/** 自动刷新定时器 */
|
|
||||||
let autoRefreshTimer: any = null;
|
|
||||||
|
|
||||||
/** 消息方法选项 */
|
/** 消息方法选项 */
|
||||||
const methodOptions = computed(() => {
|
const methodOptions = computed(() => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ import type { IotDeviceModbusConfigApi } from '#/api/iot/device/modbus/config';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
ModbusFrameFormatEnum,
|
||||||
|
ModbusModeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
@ -14,10 +19,6 @@ import { useVbenForm, z } from '#/adapter/form';
|
||||||
import { saveModbusConfig } from '#/api/iot/device/modbus/config';
|
import { saveModbusConfig } from '#/api/iot/device/modbus/config';
|
||||||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import {
|
|
||||||
ModbusFrameFormatEnum,
|
|
||||||
ModbusModeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import type { DescriptionItemSchema } from '#/components/description';
|
||||||
import { computed, h, onMounted, ref } from 'vue';
|
import { computed, h, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { confirm, useVbenModal } from '@vben/common-ui';
|
import { confirm, useVbenModal } from '@vben/common-ui';
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, ModbusFunctionCodeOptions } from '@vben/constants';
|
||||||
|
|
||||||
import { ElButton, ElMessage } from 'element-plus';
|
import { ElButton, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
|
@ -25,7 +25,6 @@ import {
|
||||||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||||
import { useDescription } from '#/components/description';
|
import { useDescription } from '#/components/description';
|
||||||
import { DictTag } from '#/components/dict-tag';
|
import { DictTag } from '#/components/dict-tag';
|
||||||
import { ModbusFunctionCodeOptions } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import DeviceModbusConfigForm from './modbus-config-form.vue';
|
import DeviceModbusConfigForm from './modbus-config-form.vue';
|
||||||
import DeviceModbusPointForm from './modbus-point-form.vue';
|
import DeviceModbusPointForm from './modbus-point-form.vue';
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,14 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, h, ref } from 'vue';
|
import { computed, h, ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
|
import {
|
||||||
|
CommonStatusEnum,
|
||||||
|
DICT_TYPE,
|
||||||
|
getByteOrderOptions,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
ModbusFunctionCodeOptions,
|
||||||
|
ModbusRawDataTypeOptions,
|
||||||
|
} from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
@ -19,12 +26,6 @@ import {
|
||||||
updateModbusPoint,
|
updateModbusPoint,
|
||||||
} from '#/api/iot/device/modbus/point';
|
} from '#/api/iot/device/modbus/point';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import {
|
|
||||||
getByteOrderOptions,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
ModbusFunctionCodeOptions,
|
|
||||||
ModbusRawDataTypeOptions,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { ContentWrap } from '@vben/common-ui';
|
import { ContentWrap } from '@vben/common-ui';
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import {
|
||||||
|
DeviceStateEnum,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,10 +28,6 @@ import {
|
||||||
} from 'element-plus';
|
} from 'element-plus';
|
||||||
|
|
||||||
import { sendDeviceMessage } from '#/api/iot/device/device';
|
import { sendDeviceMessage } from '#/api/iot/device/device';
|
||||||
import {
|
|
||||||
DeviceStateEnum,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import DataDefinition from '../../../../thingmodel/modules/components/data-definition.vue';
|
import DataDefinition from '../../../../thingmodel/modules/components/data-definition.vue';
|
||||||
import DeviceDetailsMessage from './message.vue';
|
import DeviceDetailsMessage from './message.vue';
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, onMounted, reactive, watch } from 'vue';
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import {
|
||||||
|
getEventTypeLabel,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatDateTime } from '@vben/utils';
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
|
@ -20,10 +24,6 @@ import {
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||||
import {
|
|
||||||
getEventTypeLabel,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,10 @@ import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
import { formatDate, formatDateTime } from '@vben/utils';
|
import { formatDate } from '@vben/utils';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
|
|
@ -26,13 +27,11 @@ import {
|
||||||
|
|
||||||
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
||||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
defineProps<{ deviceId: number }>();
|
defineProps<{ deviceId: number }>();
|
||||||
|
|
||||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const exporting = ref(false);
|
|
||||||
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
||||||
const total = ref(0); // 总数据量
|
const total = ref(0); // 总数据量
|
||||||
|
|
@ -297,54 +296,6 @@ function handleRefresh() {
|
||||||
getList();
|
getList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 导出数据 */
|
|
||||||
async function handleExport() {
|
|
||||||
if (list.value.length === 0) {
|
|
||||||
ElMessage.warning('暂无数据可导出');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
exporting.value = true;
|
|
||||||
try {
|
|
||||||
// 构建CSV内容
|
|
||||||
const headers = ['序号', '时间', '属性值'];
|
|
||||||
const csvContent = [
|
|
||||||
headers.join(','),
|
|
||||||
...list.value.map((item, index) => {
|
|
||||||
return [
|
|
||||||
index + 1,
|
|
||||||
formatDateTime(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);
|
|
||||||
|
|
||||||
ElMessage.success('导出成功');
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('导出失败');
|
|
||||||
} finally {
|
|
||||||
exporting.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 关闭弹窗 */
|
/** 关闭弹窗 */
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
|
|
@ -394,16 +345,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||||
刷新
|
刷新
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|
||||||
<!-- 导出按钮 -->
|
|
||||||
<ElButton
|
|
||||||
:disabled="list.length === 0"
|
|
||||||
:loading="exporting"
|
|
||||||
@click="handleExport"
|
|
||||||
>
|
|
||||||
<IconifyIcon icon="ant-design:export-outlined" class="mr-1" />
|
|
||||||
导出
|
|
||||||
</ElButton>
|
|
||||||
|
|
||||||
<!-- 视图切换 -->
|
<!-- 视图切换 -->
|
||||||
<ElButtonGroup class="ml-auto">
|
<ElButtonGroup class="ml-auto">
|
||||||
<ElButton
|
<ElButton
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
||||||
import { computed, onMounted, reactive, watch } from 'vue';
|
import { computed, onMounted, reactive, watch } from 'vue';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
import {
|
||||||
|
getThingModelServiceCallTypeLabel,
|
||||||
|
IotDeviceMessageMethodEnum,
|
||||||
|
IoTThingModelTypeEnum,
|
||||||
|
} from '@vben/constants';
|
||||||
import { IconifyIcon } from '@vben/icons';
|
import { IconifyIcon } from '@vben/icons';
|
||||||
import { formatDateTime } from '@vben/utils';
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
|
|
@ -20,10 +24,6 @@ import {
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||||
import {
|
|
||||||
getThingModelServiceCallTypeLabel,
|
|
||||||
IoTThingModelTypeEnum,
|
|
||||||
} from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="device-card-view">
|
<div>
|
||||||
<!-- 设备卡片列表 -->
|
<!-- 设备卡片列表 -->
|
||||||
<div v-loading="loading" class="min-h-96">
|
<div v-loading="loading" class="min-h-96">
|
||||||
<ElRow v-if="list.length > 0" :gutter="16">
|
<ElRow v-if="list.length > 0" :gutter="16">
|
||||||
|
|
@ -119,29 +119,38 @@ onMounted(() => {
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
>
|
>
|
||||||
<ElCard
|
<ElCard
|
||||||
class="device-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
body-class="!p-4 !flex !flex-col !h-full"
|
||||||
|
class="h-full overflow-hidden rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<!-- 顶部标题区域 -->
|
<!-- 顶部标题区域 -->
|
||||||
<div class="mb-3 flex items-center">
|
<div class="mb-3 flex items-center">
|
||||||
<div class="device-icon">
|
<div
|
||||||
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
|
>
|
||||||
<IconifyIcon icon="mdi:chip" class="text-xl" />
|
<IconifyIcon icon="mdi:chip" class="text-xl" />
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 min-w-0 flex-1">
|
<div class="ml-3 min-w-0 flex-1">
|
||||||
<div class="device-title">{{ item.deviceName }}</div>
|
<div
|
||||||
|
class="truncate text-[15px] font-semibold leading-9 dark:text-white/85"
|
||||||
|
>
|
||||||
|
{{ item.deviceName }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DictTag
|
<DictTag
|
||||||
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
||||||
:value="item.state"
|
:value="item.state"
|
||||||
class="status-tag"
|
class="text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="mb-3 flex items-start">
|
<div class="mb-3 flex items-start">
|
||||||
<div class="info-list flex-1">
|
<div class="flex-1">
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">所属产品</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
所属产品
|
||||||
|
</span>
|
||||||
<a
|
<a
|
||||||
class="info-value cursor-pointer text-primary"
|
class="cursor-pointer truncate font-medium text-primary"
|
||||||
@click="
|
@click="
|
||||||
(e) => {
|
(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -152,25 +161,33 @@ onMounted(() => {
|
||||||
{{ getProductName(item.productId) }}
|
{{ getProductName(item.productId) }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">设备类型</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
设备类型
|
||||||
|
</span>
|
||||||
<DictTag
|
<DictTag
|
||||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||||
:value="item.deviceType"
|
:value="item.deviceType"
|
||||||
class="info-tag m-0"
|
class="m-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="flex items-center text-[13px]">
|
||||||
<span class="info-label">Deviceid</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
Deviceid
|
||||||
|
</span>
|
||||||
<ElTooltip :content="String(item.id)" placement="top">
|
<ElTooltip :content="String(item.id)" placement="top">
|
||||||
<span class="info-value device-id cursor-pointer">
|
<span
|
||||||
|
class="inline-block max-w-[150px] cursor-pointer truncate align-middle font-mono text-xs opacity-85 dark:text-white/75"
|
||||||
|
>
|
||||||
{{ item.id }}
|
{{ item.id }}
|
||||||
</span>
|
</span>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 设备图片 -->
|
<!-- 设备图片 -->
|
||||||
<div class="device-image">
|
<div
|
||||||
|
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
||||||
|
>
|
||||||
<ElImage
|
<ElImage
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
:src="item.picUrl"
|
:src="item.picUrl"
|
||||||
|
|
@ -185,11 +202,13 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按钮组 -->
|
<!-- 按钮组 -->
|
||||||
<div class="action-buttons">
|
<div
|
||||||
|
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||||
|
>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="hasAccessByCodes(['iot:device:update'])"
|
v-if="hasAccessByCodes(['iot:device:update'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-edit"
|
class="!h-8 flex-1 rounded-md !border-[#1890ff] !text-[13px] !text-[#1890ff] transition-all duration-200 hover:!bg-[#1890ff] hover:!text-white"
|
||||||
@click="emit('edit', item)"
|
@click="emit('edit', item)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||||
|
|
@ -198,7 +217,7 @@ onMounted(() => {
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="hasAccessByCodes(['iot:device:query'])"
|
v-if="hasAccessByCodes(['iot:device:query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-detail"
|
class="!h-8 flex-1 rounded-md !border-[#52c41a] !text-[13px] !text-[#52c41a] transition-all duration-200 hover:!bg-[#52c41a] hover:!text-white"
|
||||||
@click="emit('detail', item.id!)"
|
@click="emit('detail', item.id!)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||||
|
|
@ -207,7 +226,7 @@ onMounted(() => {
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="hasAccessByCodes(['iot:device:message-query'])"
|
v-if="hasAccessByCodes(['iot:device:message-query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-data"
|
class="!h-8 flex-1 rounded-md !border-[#fa8c16] !text-[13px] !text-[#fa8c16] transition-all duration-200 hover:!bg-[#fa8c16] hover:!text-white"
|
||||||
@click="emit('model', item.id!)"
|
@click="emit('model', item.id!)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:database" class="mr-1" />
|
<IconifyIcon icon="lucide:database" class="mr-1" />
|
||||||
|
|
@ -222,7 +241,7 @@ onMounted(() => {
|
||||||
<ElButton
|
<ElButton
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="h-8 rounded-md p-0 text-[13px] transition-all duration-200 !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|
@ -250,188 +269,3 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.device-card-view {
|
|
||||||
.device-card {
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备图标
|
|
||||||
.device-icon {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备标题
|
|
||||||
.device-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 36px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态标签
|
|
||||||
.status-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设备图片
|
|
||||||
.device-image {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
color: #1890ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 信息列表
|
|
||||||
.info-list {
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 8px;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.text-primary {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-id {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮组
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 12px;
|
|
||||||
margin-top: auto;
|
|
||||||
border-top: 1px solid var(--el-border-color);
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
flex: 1;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&.action-btn-edit {
|
|
||||||
color: #1890ff;
|
|
||||||
border-color: #1890ff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-detail {
|
|
||||||
color: #52c41a;
|
|
||||||
border-color: #52c41a;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #52c41a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-data {
|
|
||||||
color: #fa8c16;
|
|
||||||
border-color: #fa8c16;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #fa8c16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-delete {
|
|
||||||
flex: 0 0 32px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 夜间模式适配
|
|
||||||
html.dark {
|
|
||||||
.device-card-view {
|
|
||||||
.device-card {
|
|
||||||
.device-title {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-list {
|
|
||||||
.info-label {
|
|
||||||
color: rgb(255 255 255 / 65%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-id {
|
|
||||||
color: rgb(255 255 255 / 75%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-image {
|
|
||||||
color: #69c0ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,7 @@ export function getMessageTrendChartOptions(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 设备状态仪表盘图表配置 */
|
||||||
* 设备状态仪表盘图表配置
|
|
||||||
*/
|
|
||||||
export function getDeviceStateGaugeChartOptions(
|
export function getDeviceStateGaugeChartOptions(
|
||||||
value: number,
|
value: number,
|
||||||
max: number,
|
max: number,
|
||||||
|
|
@ -129,9 +127,7 @@ export function getDeviceStateGaugeChartOptions(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 设备数量饼图配置 */
|
||||||
* 设备数量饼图配置
|
|
||||||
*/
|
|
||||||
export function getDeviceCountPieChartOptions(
|
export function getDeviceCountPieChartOptions(
|
||||||
data: Array<{ name: string; value: number }>,
|
data: Array<{ name: string; value: number }>,
|
||||||
): any {
|
): any {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ async function loadData() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO @AI:antd 这里,也要加下 /** */
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData();
|
loadData();
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ const { renderEcharts } = useEcharts(deviceCountChartRef);
|
||||||
|
|
||||||
/** 是否有数据 */
|
/** 是否有数据 */
|
||||||
const hasData = computed(() => {
|
const hasData = computed(() => {
|
||||||
// TODO @AI:即使只有一行,还是希望 return 可以换行;
|
if (!props.statsData) {
|
||||||
if (!props.statsData) return false;
|
return false;
|
||||||
|
}
|
||||||
const categories = Object.entries(
|
const categories = Object.entries(
|
||||||
props.statsData.productCategoryDeviceCounts || {},
|
props.statsData.productCategoryDeviceCounts || {},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { IotDeviceApi } from '#/api/iot/device/device';
|
import type { IotDeviceApi } from '#/api/iot/device/device';
|
||||||
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictLabel } from '@vben/hooks';
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
|
||||||
import { ElCard, ElEmpty } from 'element-plus';
|
import { ElCard, ElEmpty } from 'element-plus';
|
||||||
|
|
||||||
import { getDeviceLocationList } from '#/api/iot/device/device';
|
import { getDeviceLocationList } from '#/api/iot/device/device';
|
||||||
import { loadBaiduMapSdk } from '#/components/map';
|
import { loadBaiduMapSdk } from '#/components/map';
|
||||||
import { DeviceStateEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
defineOptions({ name: 'DeviceMapCard' });
|
defineOptions({ name: 'DeviceMapCard' });
|
||||||
|
|
||||||
|
|
@ -152,6 +151,8 @@ async function init() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await loadBaiduMapSdk();
|
await loadBaiduMapSdk();
|
||||||
|
// 等待 v-show 容器渲染完成;SDK 缓存命中时上一行会同步 resolve,DOM 来不及切换
|
||||||
|
await nextTick();
|
||||||
initMap();
|
initMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { useDescription } from '#/components/description';
|
||||||
|
|
||||||
import { useDetailSchema } from '../../data';
|
import { useDetailSchema } from '../../data';
|
||||||
|
|
||||||
/** IoT OTA 固件基本信息 */ // TODO @AI:是不是要去掉折行注释哈?
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
firmware?: IoTOtaFirmwareApi.Firmware;
|
firmware?: IoTOtaFirmwareApi.Firmware;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
import type { DescriptionItemSchema } from '#/components/description';
|
import type { DescriptionItemSchema } from '#/components/description';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, IoTOtaTaskDeviceScopeEnum } from '@vben/constants';
|
||||||
import { getDictLabel, getDictOptions } from '@vben/hooks';
|
import { getDictLabel, getDictOptions } from '@vben/hooks';
|
||||||
import { formatDateTime } from '@vben/utils';
|
import { formatDateTime } from '@vben/utils';
|
||||||
|
|
||||||
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
/** 任务详情的描述字段 */
|
/** 任务详情的描述字段 */
|
||||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||||
|
|
@ -77,7 +76,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||||
label: '选择设备',
|
label: '选择设备',
|
||||||
component: 'Select',
|
component: 'Select',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
mode: 'multiple',
|
multiple: true,
|
||||||
placeholder: '请选择设备',
|
placeholder: '请选择设备',
|
||||||
showSearch: true,
|
showSearch: true,
|
||||||
filterOption: true,
|
filterOption: true,
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
import { IoTOtaTaskStatusEnum } from '@vben/constants';
|
||||||
|
|
||||||
import { ElInput, ElMessage } from 'element-plus';
|
import { ElInput, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
import { cancelOtaTask, getOtaTaskPage } from '#/api/iot/ota/task';
|
import { cancelOtaTask, getOtaTaskPage } from '#/api/iot/ota/task';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import { useGridColumns } from '../data';
|
import { useGridColumns } from '../data';
|
||||||
import OtaTaskDetail from './detail.vue';
|
import OtaTaskDetail from './detail.vue';
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
import { DICT_TYPE } from '@vben/constants';
|
import { DICT_TYPE, IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||||
import { getDictLabel } from '@vben/hooks';
|
import { getDictLabel } from '@vben/hooks';
|
||||||
|
|
||||||
import { ElCard, ElCol, ElRow } from 'element-plus';
|
import { ElCard, ElCol, ElRow } from 'element-plus';
|
||||||
|
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import type { IoTOtaTaskRecordApi } from '#/api/iot/ota/task/record';
|
||||||
|
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||||
|
|
||||||
import { ElCard, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
import { ElCard, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||||
|
|
||||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
|
@ -12,7 +14,6 @@ import {
|
||||||
getOtaTaskRecordPage,
|
getOtaTaskRecordPage,
|
||||||
} from '#/api/iot/ota/task/record';
|
} from '#/api/iot/ota/task/record';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import { useGridColumns } from '../data';
|
import { useGridColumns } from '../data';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import { onMounted, provide, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||||
|
|
||||||
import { ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
import { ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||||
|
|
||||||
import { getDeviceCount } from '#/api/iot/device/device';
|
import { getDeviceCount } from '#/api/iot/device/device';
|
||||||
import { getProduct } from '#/api/iot/product/product';
|
import { getProduct } from '#/api/iot/product/product';
|
||||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
|
||||||
|
|
||||||
import ProductDetailsHeader from './modules/header.vue';
|
import ProductDetailsHeader from './modules/header.vue';
|
||||||
import ProductDetailsInfo from './modules/info.vue';
|
import ProductDetailsInfo from './modules/info.vue';
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,9 @@ const showProductSecret = ref(false); // 是否显示产品密钥
|
||||||
|
|
||||||
/** 格式化日期 */
|
/** 格式化日期 */
|
||||||
function formatDate(date?: Date | string) {
|
function formatDate(date?: Date | string) {
|
||||||
// TODO @AI:即使单行,也需要 return 换行;
|
if (!date) {
|
||||||
if (!date) return '-';
|
return '-';
|
||||||
|
}
|
||||||
return new Date(date).toLocaleString('zh-CN');
|
return new Date(date).toLocaleString('zh-CN');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="product-card-view">
|
<div>
|
||||||
<!-- 产品卡片列表 -->
|
<!-- 产品卡片列表 -->
|
||||||
<div v-loading="loading" class="min-h-96">
|
<div v-loading="loading" class="min-h-96">
|
||||||
<ElRow v-if="list.length > 0" :gutter="16">
|
<ElRow v-if="list.length > 0" :gutter="16">
|
||||||
|
|
@ -103,52 +103,68 @@ onMounted(() => {
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
>
|
>
|
||||||
<ElCard
|
<ElCard
|
||||||
body-class="!p-4"
|
body-class="!p-4 !flex !flex-col !h-full"
|
||||||
class="product-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
class="h-full overflow-hidden rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||||
>
|
>
|
||||||
<!-- 顶部标题区域 -->
|
<!-- 顶部标题区域 -->
|
||||||
<div class="mb-3 flex items-center">
|
<div class="mb-3 flex items-center">
|
||||||
<div class="product-icon">
|
<div
|
||||||
|
class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff] to-[#1890ff] text-white"
|
||||||
|
>
|
||||||
<IconifyIcon
|
<IconifyIcon
|
||||||
:icon="item.icon || 'lucide:box'"
|
:icon="item.icon || 'lucide:box'"
|
||||||
class="text-xl"
|
class="text-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-3 min-w-0 flex-1">
|
<div class="ml-3 min-w-0 flex-1">
|
||||||
<div class="product-title">{{ item.name }}</div>
|
<div
|
||||||
|
class="truncate text-[15px] font-semibold leading-9 dark:text-white/85"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="mb-3 flex items-start">
|
<div class="mb-3 flex items-start">
|
||||||
<div class="info-list flex-1">
|
<div class="flex-1">
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">产品分类</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
<span class="info-value text-primary">
|
产品分类
|
||||||
|
</span>
|
||||||
|
<span class="truncate font-medium text-primary">
|
||||||
{{ getCategoryName(item.categoryId) }}
|
{{ getCategoryName(item.categoryId) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="mb-2 flex items-center text-[13px]">
|
||||||
<span class="info-label">产品类型</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
产品类型
|
||||||
|
</span>
|
||||||
<DictTag
|
<DictTag
|
||||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||||
:value="item.deviceType"
|
:value="item.deviceType"
|
||||||
class="info-tag m-0"
|
class="m-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="flex items-center text-[13px]">
|
||||||
<span class="info-label">产品标识</span>
|
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||||
|
产品标识
|
||||||
|
</span>
|
||||||
<ElTooltip
|
<ElTooltip
|
||||||
:content="item.productKey || item.id"
|
:content="item.productKey || item.id"
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<span class="info-value product-key cursor-pointer">
|
<span
|
||||||
|
class="inline-block max-w-[150px] cursor-pointer truncate align-middle font-mono text-xs opacity-85 dark:text-white/75"
|
||||||
|
>
|
||||||
{{ item.productKey || item.id }}
|
{{ item.productKey || item.id }}
|
||||||
</span>
|
</span>
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 产品图片 -->
|
<!-- 产品图片 -->
|
||||||
<div class="product-image">
|
<div
|
||||||
|
class="flex size-20 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-[#40a9ff15] to-[#1890ff15] text-[#1890ff] dark:from-[#40a9ff25] dark:to-[#1890ff25] dark:text-[#69c0ff]"
|
||||||
|
>
|
||||||
<ElImage
|
<ElImage
|
||||||
v-if="item.picUrl"
|
v-if="item.picUrl"
|
||||||
:src="item.picUrl"
|
:src="item.picUrl"
|
||||||
|
|
@ -163,11 +179,13 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 按钮组 -->
|
<!-- 按钮组 -->
|
||||||
<div class="action-buttons">
|
<div
|
||||||
|
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||||
|
>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="hasAccessByCodes(['iot:product:update'])"
|
v-if="hasAccessByCodes(['iot:product:update'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-edit"
|
class="!h-8 flex-1 rounded-md !border-[#1890ff] !text-[13px] !text-[#1890ff] transition-all duration-200 hover:!bg-[#1890ff] hover:!text-white"
|
||||||
@click="emit('edit', item)"
|
@click="emit('edit', item)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||||
|
|
@ -176,7 +194,7 @@ onMounted(() => {
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="hasAccessByCodes(['iot:product:query'])"
|
v-if="hasAccessByCodes(['iot:product:query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-detail"
|
class="!h-8 flex-1 rounded-md !border-[#52c41a] !text-[13px] !text-[#52c41a] transition-all duration-200 hover:!bg-[#52c41a] hover:!text-white"
|
||||||
@click="emit('detail', item.id)"
|
@click="emit('detail', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||||
|
|
@ -185,7 +203,7 @@ onMounted(() => {
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="hasAccessByCodes(['iot:thing-model:query'])"
|
v-if="hasAccessByCodes(['iot:thing-model:query'])"
|
||||||
size="small"
|
size="small"
|
||||||
class="action-btn action-btn-model"
|
class="!h-8 flex-1 rounded-md !border-[#fa8c16] !text-[13px] !text-[#fa8c16] transition-all duration-200 hover:!bg-[#fa8c16] hover:!text-white"
|
||||||
@click="emit('thingModel', item.id)"
|
@click="emit('thingModel', item.id)"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
||||||
|
|
@ -201,7 +219,7 @@ onMounted(() => {
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
disabled
|
disabled
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="h-8 rounded-md p-0 text-[13px] transition-all duration-200 !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|
@ -215,7 +233,7 @@ onMounted(() => {
|
||||||
<ElButton
|
<ElButton
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
class="action-btn action-btn-delete !w-8"
|
class="h-8 rounded-md p-0 text-[13px] transition-all duration-200 !w-8"
|
||||||
>
|
>
|
||||||
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
<IconifyIcon icon="lucide:trash-2" class="text-sm" />
|
||||||
</ElButton>
|
</ElButton>
|
||||||
|
|
@ -244,182 +262,3 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.product-card-view {
|
|
||||||
.product-card {
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 产品图标
|
|
||||||
.product-icon {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
color: white;
|
|
||||||
background: linear-gradient(135deg, #40a9ff 0%, #1890ff 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 产品标题
|
|
||||||
.product-title {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 36px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 信息列表
|
|
||||||
.info-list {
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 8px;
|
|
||||||
opacity: 0.65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
&.text-primary {
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-key {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 150px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
vertical-align: middle;
|
|
||||||
white-space: nowrap;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-tag {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 产品图片
|
|
||||||
.product-image {
|
|
||||||
display: flex;
|
|
||||||
flex-shrink: 0;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
color: #1890ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff15 0%, #1890ff15 100%);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按钮组
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
padding-top: 12px;
|
|
||||||
margin-top: auto;
|
|
||||||
border-top: 1px solid var(--el-border-color);
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
flex: 1;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 13px;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
|
|
||||||
&.action-btn-edit {
|
|
||||||
color: #1890ff;
|
|
||||||
border-color: #1890ff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-detail {
|
|
||||||
color: #52c41a;
|
|
||||||
border-color: #52c41a;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #52c41a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-model {
|
|
||||||
color: #fa8c16;
|
|
||||||
border-color: #fa8c16;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
background: #fa8c16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.action-btn-delete {
|
|
||||||
flex: 0 0 32px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 夜间模式适配
|
|
||||||
html.dark {
|
|
||||||
.product-card-view {
|
|
||||||
.product-card {
|
|
||||||
.product-title {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-list {
|
|
||||||
.info-label {
|
|
||||||
color: rgb(255 255 255 / 65%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: rgb(255 255 255 / 85%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-key {
|
|
||||||
color: rgb(255 255 255 / 75%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-image {
|
|
||||||
color: #69c0ff;
|
|
||||||
background: linear-gradient(135deg, #40a9ff25 0%, #1890ff25 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ElTabPane, ElTabs } from 'element-plus';
|
||||||
|
|
||||||
|
import DataRuleList from './rule/index.vue';
|
||||||
|
import DataSinkList from './sink/index.vue';
|
||||||
|
|
||||||
|
// TODO DONE @AI:下面的"/** IoT 数据流转:规则 / 目的 */"需要注释么?基线无文件级注释,已删除
|
||||||
|
const activeTabName = ref('rule');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<ElTabs v-model="activeTabName">
|
||||||
|
<ElTabPane name="rule" label="规则">
|
||||||
|
<DataRuleList />
|
||||||
|
</ElTabPane>
|
||||||
|
<ElTabPane name="sink" label="目的">
|
||||||
|
<DataSinkList />
|
||||||
|
</ElTabPane>
|
||||||
|
</ElTabs>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { DataRuleApi } from '#/api/iot/rule/data/rule';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { getDataSinkSimpleList } from '#/api/iot/rule/data/sink';
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '规则名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入规则名称',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '规则状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规则表单 Schema */
|
||||||
|
export function useRuleFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
component: 'Input',
|
||||||
|
fieldName: 'id',
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: [''],
|
||||||
|
show: () => false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '规则名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入规则名称',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'description',
|
||||||
|
label: '规则描述',
|
||||||
|
component: 'Textarea',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入规则描述',
|
||||||
|
rows: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '规则状态',
|
||||||
|
component: 'RadioGroup',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
},
|
||||||
|
defaultValue: 0,
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'sinkIds',
|
||||||
|
label: '数据目的',
|
||||||
|
component: 'ApiSelect',
|
||||||
|
componentProps: {
|
||||||
|
api: getDataSinkSimpleList,
|
||||||
|
labelField: 'name',
|
||||||
|
valueField: 'id',
|
||||||
|
multiple: true,
|
||||||
|
clearable: true,
|
||||||
|
placeholder: '请选择数据目的',
|
||||||
|
},
|
||||||
|
rules: 'required',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 数据源配置(行编辑表)的字段 */
|
||||||
|
export function useSourceConfigColumns(): VxeTableGridOptions['columns'] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
field: 'productId',
|
||||||
|
title: '产品',
|
||||||
|
minWidth: 200,
|
||||||
|
slots: { default: 'productId' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'deviceId',
|
||||||
|
title: '设备',
|
||||||
|
minWidth: 200,
|
||||||
|
slots: { default: 'deviceId' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'method',
|
||||||
|
title: '消息',
|
||||||
|
minWidth: 200,
|
||||||
|
slots: { default: 'method' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'identifier',
|
||||||
|
title: '标识符',
|
||||||
|
minWidth: 250,
|
||||||
|
slots: { default: 'identifier' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<DataRuleApi.DataRule>['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '规则编号',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '规则名称',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
title: '规则描述',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '规则状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sourceConfigs',
|
||||||
|
title: '数据源',
|
||||||
|
minWidth: 100,
|
||||||
|
formatter: ({ cellValue }) => `${cellValue?.length || 0} 个`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sinkIds',
|
||||||
|
title: '数据目的',
|
||||||
|
minWidth: 100,
|
||||||
|
formatter: ({ cellValue }) => `${cellValue?.length || 0} 个`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 160,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { DataRuleApi } from '#/api/iot/rule/data/rule';
|
||||||
|
|
||||||
|
import { Page, useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ElLoading, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { deleteDataRule, getDataRulePage } from '#/api/iot/rule/data/rule';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useGridColumns, useGridFormSchema } from './data';
|
||||||
|
import Form from './modules/form.vue';
|
||||||
|
|
||||||
|
const [FormModal, formModalApi] = useVbenModal({
|
||||||
|
connectedComponent: Form,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 刷新表格 */
|
||||||
|
function handleRefresh() {
|
||||||
|
gridApi.query();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 创建规则 */
|
||||||
|
function handleCreate() {
|
||||||
|
formModalApi.setData(null).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑规则 */
|
||||||
|
function handleEdit(row: DataRuleApi.DataRule) {
|
||||||
|
formModalApi.setData({ id: row.id }).open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除规则 */
|
||||||
|
async function handleDelete(row: DataRuleApi.DataRule) {
|
||||||
|
const loadingInstance = ElLoading.service({
|
||||||
|
text: $t('ui.actionMessage.deleting', [row.name]),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await deleteDataRule(row.id!);
|
||||||
|
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
|
||||||
|
handleRefresh();
|
||||||
|
} finally {
|
||||||
|
loadingInstance.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
formOptions: {
|
||||||
|
schema: useGridFormSchema(),
|
||||||
|
},
|
||||||
|
gridOptions: {
|
||||||
|
columns: useGridColumns(),
|
||||||
|
height: 'auto',
|
||||||
|
keepSource: true,
|
||||||
|
proxyConfig: {
|
||||||
|
ajax: {
|
||||||
|
query: async ({ page }, formValues) => {
|
||||||
|
return await getDataRulePage({
|
||||||
|
pageNo: page.currentPage,
|
||||||
|
pageSize: page.pageSize,
|
||||||
|
...formValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowConfig: {
|
||||||
|
keyField: 'id',
|
||||||
|
isHover: true,
|
||||||
|
},
|
||||||
|
toolbarConfig: {
|
||||||
|
refresh: true,
|
||||||
|
search: true,
|
||||||
|
},
|
||||||
|
} as VxeTableGridOptions<DataRuleApi.DataRule>,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Page auto-content-height>
|
||||||
|
<FormModal @success="handleRefresh" />
|
||||||
|
<Grid table-title="数据规则列表">
|
||||||
|
<template #toolbar-tools>
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('ui.actionTitle.create', ['规则']),
|
||||||
|
type: 'primary',
|
||||||
|
icon: ACTION_ICON.ADD,
|
||||||
|
auth: ['iot:data-rule:create'],
|
||||||
|
onClick: handleCreate,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ row }">
|
||||||
|
<TableAction
|
||||||
|
:actions="[
|
||||||
|
{
|
||||||
|
label: $t('common.edit'),
|
||||||
|
type: 'primary',
|
||||||
|
link: true,
|
||||||
|
icon: ACTION_ICON.EDIT,
|
||||||
|
auth: ['iot:data-rule:update'],
|
||||||
|
onClick: handleEdit.bind(null, row),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: $t('common.delete'),
|
||||||
|
type: 'danger',
|
||||||
|
link: true,
|
||||||
|
icon: ACTION_ICON.DELETE,
|
||||||
|
auth: ['iot:data-rule:delete'],
|
||||||
|
popConfirm: {
|
||||||
|
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
|
||||||
|
confirm: handleDelete.bind(null, row),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DataRuleApi } from '#/api/iot/rule/data/rule';
|
||||||
|
|
||||||
|
import { computed, nextTick, ref } from 'vue';
|
||||||
|
|
||||||
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
|
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
import { useVbenForm } from '#/adapter/form';
|
||||||
|
import {
|
||||||
|
createDataRule,
|
||||||
|
getDataRule,
|
||||||
|
updateDataRule,
|
||||||
|
} from '#/api/iot/rule/data/rule';
|
||||||
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
import { useRuleFormSchema } from '../data';
|
||||||
|
import SourceConfigForm from './source-config-form.vue';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const formData = ref<DataRuleApi.DataRule>();
|
||||||
|
const sourceConfigRef = ref<InstanceType<typeof SourceConfigForm>>();
|
||||||
|
const getTitle = computed(() => {
|
||||||
|
return formData.value?.id
|
||||||
|
? $t('ui.actionTitle.edit', ['数据规则'])
|
||||||
|
: $t('ui.actionTitle.create', ['数据规则']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 100,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
|
schema: useRuleFormSchema(),
|
||||||
|
showDefaultActions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [Modal, modalApi] = useVbenModal({
|
||||||
|
async onConfirm() {
|
||||||
|
const { valid } = await formApi.validate();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 校验数据源配置
|
||||||
|
await sourceConfigRef.value?.validate();
|
||||||
|
modalApi.lock();
|
||||||
|
// 提交表单
|
||||||
|
const data = (await formApi.getValues()) as DataRuleApi.DataRule;
|
||||||
|
data.sourceConfigs = sourceConfigRef.value?.getData() || [];
|
||||||
|
try {
|
||||||
|
await (formData.value?.id ? updateDataRule(data) : createDataRule(data));
|
||||||
|
// 关闭并提示
|
||||||
|
await modalApi.close();
|
||||||
|
emit('success');
|
||||||
|
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onOpenChange(isOpen: boolean) {
|
||||||
|
if (!isOpen) {
|
||||||
|
formData.value = undefined;
|
||||||
|
sourceConfigRef.value?.setData([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 加载数据
|
||||||
|
const data = modalApi.getData<DataRuleApi.DataRule>();
|
||||||
|
if (!data || !data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalApi.lock();
|
||||||
|
try {
|
||||||
|
formData.value = await getDataRule(data.id);
|
||||||
|
// 设置到 values
|
||||||
|
await formApi.setValues(formData.value);
|
||||||
|
// 设置数据源配置
|
||||||
|
await nextTick();
|
||||||
|
sourceConfigRef.value?.setData(formData.value.sourceConfigs || []);
|
||||||
|
} finally {
|
||||||
|
modalApi.unlock();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal class="w-4/5" :title="getTitle">
|
||||||
|
<Form class="mx-4" />
|
||||||
|
<div class="mx-4 mt-4">
|
||||||
|
<div class="mb-2 text-sm font-medium">数据源配置</div>
|
||||||
|
<SourceConfigForm ref="sourceConfigRef" />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@vben/constants';
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
|
||||||
|
import { ElButton, ElMessage, ElOption, ElSelect } from 'element-plus';
|
||||||
|
|
||||||
|
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||||
|
import { getSimpleDeviceList } from '#/api/iot/device/device';
|
||||||
|
import { getSimpleProductList } from '#/api/iot/product/product';
|
||||||
|
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||||
|
|
||||||
|
import { useSourceConfigColumns } from '../data';
|
||||||
|
|
||||||
|
const formData = ref<any[]>([]);
|
||||||
|
const productList = ref<any[]>([]); // 产品列表
|
||||||
|
const deviceList = ref<any[]>([]); // 设备列表
|
||||||
|
const thingModelCache = ref<Map<number, any[]>>(new Map()); // 缓存物模型数据,key 为 productId
|
||||||
|
|
||||||
|
/** 上行消息方法列表 */
|
||||||
|
const upstreamMethods = computed(() => {
|
||||||
|
return Object.values(IotDeviceMessageMethodEnum).filter(
|
||||||
|
(item) => item.upstream,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 根据产品 ID 过滤设备 */
|
||||||
|
function getFilteredDevices(productId: number) {
|
||||||
|
if (!productId) return [];
|
||||||
|
return deviceList.value.filter(
|
||||||
|
(device: any) => device.productId === productId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断是否需要显示标识符选择器 */
|
||||||
|
function shouldShowIdentifierSelect(row: any) {
|
||||||
|
return [
|
||||||
|
IotDeviceMessageMethodEnum.EVENT_POST.method,
|
||||||
|
IotDeviceMessageMethodEnum.PROPERTY_POST.method,
|
||||||
|
].includes(row.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取物模型选项 */
|
||||||
|
function getThingModelOptions(row: any) {
|
||||||
|
if (!row.productId || !shouldShowIdentifierSelect(row)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const thingModels: any[] = thingModelCache.value.get(row.productId) || [];
|
||||||
|
let filteredModels: any[] = [];
|
||||||
|
if (row.method === IotDeviceMessageMethodEnum.EVENT_POST.method) {
|
||||||
|
filteredModels = thingModels.filter(
|
||||||
|
(item: any) => item.type === IoTThingModelTypeEnum.EVENT,
|
||||||
|
);
|
||||||
|
} else if (row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method) {
|
||||||
|
filteredModels = thingModels.filter(
|
||||||
|
(item: any) => item.type === IoTThingModelTypeEnum.PROPERTY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return filteredModels.map((item: any) => ({
|
||||||
|
label: `${item.name} (${item.identifier})`,
|
||||||
|
value: item.identifier,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载产品列表 */
|
||||||
|
async function loadProductList() {
|
||||||
|
try {
|
||||||
|
productList.value = await getSimpleProductList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载产品列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载设备列表 */
|
||||||
|
async function loadDeviceList() {
|
||||||
|
try {
|
||||||
|
deviceList.value = await getSimpleDeviceList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载设备列表失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载物模型数据 */
|
||||||
|
async function loadThingModel(productId: number) {
|
||||||
|
if (thingModelCache.value.has(productId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const thingModels = await getThingModelListByProductId(productId);
|
||||||
|
thingModelCache.value.set(productId, thingModels);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载物模型失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 产品变化时清空设备 / 消息 / 标识符 */
|
||||||
|
function handleProductChange(rowIndex: number) {
|
||||||
|
const row = formData.value[rowIndex];
|
||||||
|
row.deviceId = 0;
|
||||||
|
row.method = undefined;
|
||||||
|
row.identifier = undefined;
|
||||||
|
row.identifierLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 消息方法变化时清空标识符 + 按需加载物模型 */
|
||||||
|
async function handleMethodChange(rowIndex: number) {
|
||||||
|
const row = formData.value[rowIndex];
|
||||||
|
row.identifier = undefined;
|
||||||
|
if (shouldShowIdentifierSelect(row) && row.productId) {
|
||||||
|
row.identifierLoading = true;
|
||||||
|
await loadThingModel(row.productId);
|
||||||
|
row.identifierLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 表格配置 */
|
||||||
|
const [Grid, gridApi] = useVbenVxeGrid({
|
||||||
|
gridOptions: {
|
||||||
|
columns: useSourceConfigColumns(),
|
||||||
|
data: formData.value,
|
||||||
|
minHeight: 160,
|
||||||
|
border: true,
|
||||||
|
showOverflow: false,
|
||||||
|
rowConfig: { isHover: true, height: 64 },
|
||||||
|
pagerConfig: { enabled: false },
|
||||||
|
toolbarConfig: { enabled: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 同步 formData 到 vxe-grid */
|
||||||
|
async function reloadGrid() {
|
||||||
|
await nextTick();
|
||||||
|
await gridApi.grid?.reloadData(formData.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新增一行数据源 */
|
||||||
|
async function handleAdd() {
|
||||||
|
formData.value.push({
|
||||||
|
productId: undefined,
|
||||||
|
deviceId: undefined,
|
||||||
|
method: undefined,
|
||||||
|
identifier: undefined,
|
||||||
|
identifierLoading: false,
|
||||||
|
});
|
||||||
|
await reloadGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除一行数据源 */
|
||||||
|
async function handleDelete(rowIndex: number) {
|
||||||
|
formData.value.splice(rowIndex, 1);
|
||||||
|
await reloadGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验全部行;返回 Promise,失败时 reject 第一条错误信息 */
|
||||||
|
function validate() {
|
||||||
|
for (let i = 0; i < formData.value.length; i++) {
|
||||||
|
const row = formData.value[i];
|
||||||
|
if (!row.productId) {
|
||||||
|
ElMessage.error(`第 ${i + 1} 行:产品不能为空`);
|
||||||
|
return Promise.reject(new Error('产品不能为空'));
|
||||||
|
}
|
||||||
|
if (row.deviceId === undefined || row.deviceId === null) {
|
||||||
|
ElMessage.error(`第 ${i + 1} 行:设备不能为空`);
|
||||||
|
return Promise.reject(new Error('设备不能为空'));
|
||||||
|
}
|
||||||
|
if (!row.method) {
|
||||||
|
ElMessage.error(`第 ${i + 1} 行:消息方法不能为空`);
|
||||||
|
return Promise.reject(new Error('消息方法不能为空'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 取当前所有行的值 */
|
||||||
|
function getData() {
|
||||||
|
return formData.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置初始数据 */
|
||||||
|
async function setData(data: any[]) {
|
||||||
|
formData.value = (data || []).map((item) => ({
|
||||||
|
...item,
|
||||||
|
identifierLoading: false,
|
||||||
|
}));
|
||||||
|
// 为已有数据预加载物模型;并行加载,不阻塞 reloadGrid
|
||||||
|
await Promise.all(
|
||||||
|
(data || [])
|
||||||
|
.filter((item) => item.productId && shouldShowIdentifierSelect(item))
|
||||||
|
.map((item) => loadThingModel(item.productId)),
|
||||||
|
);
|
||||||
|
await reloadGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([loadProductList(), loadDeviceList()]);
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({ validate, getData, setData });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Grid>
|
||||||
|
<template #productId="{ rowIndex }">
|
||||||
|
<ElSelect
|
||||||
|
v-model="formData[rowIndex].productId"
|
||||||
|
placeholder="请选择产品"
|
||||||
|
filterable
|
||||||
|
class="w-full"
|
||||||
|
@change="() => handleProductChange(rowIndex)"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="p in productList"
|
||||||
|
:key="p.id"
|
||||||
|
:label="p.name"
|
||||||
|
:value="p.id"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</template>
|
||||||
|
<template #deviceId="{ rowIndex }">
|
||||||
|
<ElSelect
|
||||||
|
v-model="formData[rowIndex].deviceId"
|
||||||
|
placeholder="请选择设备"
|
||||||
|
filterable
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<ElOption label="全部设备" :value="0" />
|
||||||
|
<ElOption
|
||||||
|
v-for="d in getFilteredDevices(formData[rowIndex].productId)"
|
||||||
|
:key="d.id"
|
||||||
|
:label="d.deviceName"
|
||||||
|
:value="d.id"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</template>
|
||||||
|
<template #method="{ rowIndex }">
|
||||||
|
<ElSelect
|
||||||
|
v-model="formData[rowIndex].method"
|
||||||
|
placeholder="请选择消息"
|
||||||
|
filterable
|
||||||
|
class="w-full"
|
||||||
|
@change="() => handleMethodChange(rowIndex)"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="m in upstreamMethods"
|
||||||
|
:key="m.method"
|
||||||
|
:label="m.name"
|
||||||
|
:value="m.method"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
</template>
|
||||||
|
<template #identifier="{ rowIndex }">
|
||||||
|
<ElSelect
|
||||||
|
v-if="shouldShowIdentifierSelect(formData[rowIndex])"
|
||||||
|
v-model="formData[rowIndex].identifier"
|
||||||
|
placeholder="请选择标识符"
|
||||||
|
filterable
|
||||||
|
:loading="formData[rowIndex].identifierLoading"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<ElOption
|
||||||
|
v-for="option in getThingModelOptions(formData[rowIndex])"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</ElSelect>
|
||||||
|
<span v-else class="text-xs text-muted-foreground">-</span>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ rowIndex }">
|
||||||
|
<ElButton type="danger" link @click="handleDelete(rowIndex)">
|
||||||
|
删除
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</Grid>
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<ElButton type="primary" @click="handleAdd">
|
||||||
|
<IconifyIcon icon="ant-design:plus-outlined" class="mr-1" />
|
||||||
|
添加数据源
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { ElButton, ElInput } from 'element-plus';
|
||||||
|
|
||||||
|
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 项列表
|
||||||
|
|
||||||
|
/** 添加项目 */
|
||||||
|
function addItem() {
|
||||||
|
items.value.push({ key: '', value: '' });
|
||||||
|
updateModelValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 移除项目 */
|
||||||
|
function removeItem(index: number) {
|
||||||
|
items.value.splice(index, 1);
|
||||||
|
updateModelValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 更新 modelValue */
|
||||||
|
function 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>
|
||||||
|
<div v-for="(item, index) in items" :key="index" class="mb-2 flex w-full">
|
||||||
|
<ElInput v-model="item.key" class="mr-2" placeholder="键" />
|
||||||
|
<ElInput v-model="item.value" placeholder="值" />
|
||||||
|
<ElButton class="ml-2" type="danger" link @click="removeItem(index)">
|
||||||
|
<IconifyIcon icon="ant-design:delete-outlined" />
|
||||||
|
删除
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<ElButton type="primary" link @click="addItem">
|
||||||
|
<IconifyIcon icon="ant-design:plus-outlined" />
|
||||||
|
{{ addButtonText }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// TODO DONE @AI:参考别的模块,是不是要写下 function 注释?参考 system user index;rule/data 其它的模块,也是处理下;
|
||||||
|
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
import { IconifyIcon } from '@vben/icons';
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useClipboard, useVModel } from '@vueuse/core';
|
||||||
|
import { ElButton, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const TABLE_SQL = `CREATE TABLE iot_device_message_sink (
|
||||||
|
id VARCHAR(64) NOT NULL COMMENT '消息ID',
|
||||||
|
device_id BIGINT NOT NULL COMMENT '设备编号',
|
||||||
|
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户编号',
|
||||||
|
method VARCHAR(128) COMMENT '请求方法',
|
||||||
|
report_time DATETIME COMMENT '上报时间',
|
||||||
|
data TEXT COMMENT '完整消息JSON',
|
||||||
|
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (id) USING BTREE,
|
||||||
|
INDEX idx_create_time (create_time ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'IoT 设备消息流转目标表';`;
|
||||||
|
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const showSqlTip = ref(false);
|
||||||
|
const copied = ref(false);
|
||||||
|
const { copy } = useClipboard();
|
||||||
|
|
||||||
|
async function handleCopySql() {
|
||||||
|
await copy(TABLE_SQL);
|
||||||
|
copied.value = true;
|
||||||
|
ElMessage.success('建表 SQL 已复制到剪贴板');
|
||||||
|
setTimeout(() => (copied.value = false), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.DATABASE}`,
|
||||||
|
jdbcUrl: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
tableName: 'iot_device_message_sink',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.jdbcUrl"
|
||||||
|
:rules="[{ required: true, message: 'JDBC 连接地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="JDBC 地址"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.jdbcUrl"
|
||||||
|
placeholder="请输入 JDBC 连接地址,如:jdbc:mysql://localhost:3306/iot_data"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.username"
|
||||||
|
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
|
||||||
|
label="用户名"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.username" placeholder="请输入数据库用户名" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.password"
|
||||||
|
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
|
||||||
|
label="密码"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入数据库密码"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.tableName"
|
||||||
|
:rules="[{ required: true, message: '目标表名不能为空', trigger: 'blur' }]"
|
||||||
|
label="目标表名"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<ElInput
|
||||||
|
v-model="config.tableName"
|
||||||
|
placeholder="目标表名"
|
||||||
|
class="w-[240px]"
|
||||||
|
/>
|
||||||
|
<ElButton type="primary" link @click="showSqlTip = !showSqlTip">
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="showSqlTip ? 'lucide:chevron-up' : 'lucide:file-text'"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
|
{{ showSqlTip ? '收起表结构提示' : '查看表结构提示' }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</ElFormItem>
|
||||||
|
<div
|
||||||
|
v-if="showSqlTip"
|
||||||
|
class="mt-2 overflow-hidden rounded border border-gray-200 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between bg-gray-100 px-3 py-2 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-600 dark:text-gray-300">
|
||||||
|
目标数据库需包含以下结构的表,才能正常接收数据流转的消息
|
||||||
|
</span>
|
||||||
|
<ElButton size="small" @click="handleCopySql">
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="copied ? 'lucide:check' : 'lucide:copy'"
|
||||||
|
class="mr-1"
|
||||||
|
/>
|
||||||
|
{{ copied ? '已复制' : '复制 SQL' }}
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
class="m-0 overflow-x-auto bg-gray-50 p-3 font-mono text-[12px] leading-normal text-gray-800 dark:bg-gray-900 dark:text-gray-200"
|
||||||
|
><code>{{ TABLE_SQL }}</code></pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
<!--suppress HttpUrlsUsage -->
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { ElFormItem, ElInput, ElOption, ElSelect } from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
import KeyValueEditor from './components/key-value-editor.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
const urlPrefix = ref<'http://' | 'https://'>('http://');
|
||||||
|
const urlPath = ref('');
|
||||||
|
const fullUrl = computed(() =>
|
||||||
|
urlPath.value ? urlPrefix.value + urlPath.value : '',
|
||||||
|
);
|
||||||
|
|
||||||
|
watch([urlPrefix, urlPath], () => {
|
||||||
|
config.value.url = fullUrl.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
if (config.value.url?.startsWith('https://')) {
|
||||||
|
urlPrefix.value = 'https://';
|
||||||
|
urlPath.value = config.value.url.slice(8);
|
||||||
|
} else if (config.value.url?.startsWith('http://')) {
|
||||||
|
urlPrefix.value = 'http://';
|
||||||
|
urlPath.value = config.value.url.slice(7);
|
||||||
|
} else {
|
||||||
|
urlPath.value = config.value.url ?? '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.HTTP}`,
|
||||||
|
url: '',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {},
|
||||||
|
query: {},
|
||||||
|
body: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.url"
|
||||||
|
:rules="[{ required: true, message: '请求地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="请求地址"
|
||||||
|
>
|
||||||
|
<ElInput v-model="urlPath" placeholder="请输入请求地址">
|
||||||
|
<template #prepend>
|
||||||
|
<ElSelect v-model="urlPrefix" class="w-[100px]">
|
||||||
|
<ElOption label="http://" value="http://" />
|
||||||
|
<ElOption label="https://" value="https://" />
|
||||||
|
</ElSelect>
|
||||||
|
</template>
|
||||||
|
</ElInput>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.method"
|
||||||
|
:rules="[{ required: true, message: '请求方法不能为空', trigger: 'change' }]"
|
||||||
|
label="请求方法"
|
||||||
|
>
|
||||||
|
<ElSelect v-model="config.method" placeholder="请选择请求方法">
|
||||||
|
<ElOption label="GET" value="GET" />
|
||||||
|
<ElOption label="POST" value="POST" />
|
||||||
|
<ElOption label="PUT" value="PUT" />
|
||||||
|
<ElOption label="DELETE" value="DELETE" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="请求头">
|
||||||
|
<KeyValueEditor v-model="config.headers" add-button-text="添加请求头" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="请求参数">
|
||||||
|
<KeyValueEditor v-model="config.query" add-button-text="添加参数" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem label="请求体">
|
||||||
|
<ElInput
|
||||||
|
v-model="config.body"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入内容"
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export { default as DatabaseConfigForm } from './database-config-form.vue';
|
||||||
|
export { default as HttpConfigForm } from './http-config-form.vue';
|
||||||
|
export { default as KafkaMqConfigForm } from './kafka-mq-config-form.vue';
|
||||||
|
export { default as MqttConfigForm } from './mqtt-config-form.vue';
|
||||||
|
export { default as RabbitMqConfigForm } from './rabbit-mq-config-form.vue';
|
||||||
|
export { default as RedisStreamConfigForm } from './redis-stream-config-form.vue';
|
||||||
|
export { default as RocketMqConfigForm } from './rocket-mq-config-form.vue';
|
||||||
|
export { default as TcpConfigForm } from './tcp-config-form.vue';
|
||||||
|
export { default as WebSocketConfigForm } from './websocket-config-form.vue';
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { ElFormItem, ElInput, ElSwitch } from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.KAFKA}`,
|
||||||
|
bootstrapServers: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
ssl: false,
|
||||||
|
topic: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.bootstrapServers"
|
||||||
|
:rules="[{ required: true, message: '服务地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="服务地址"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.bootstrapServers"
|
||||||
|
placeholder="请输入服务地址,如:localhost:9092"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.username"
|
||||||
|
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
|
||||||
|
label="用户名"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.username" placeholder="请输入用户名" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.password"
|
||||||
|
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
|
||||||
|
label="密码"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.ssl" label="启用 SSL">
|
||||||
|
<ElSwitch v-model="config.ssl" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.topic"
|
||||||
|
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
|
||||||
|
label="主题"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { ElFormItem, ElInput } from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.MQTT}`,
|
||||||
|
url: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
clientId: '',
|
||||||
|
topic: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.url"
|
||||||
|
:rules="[{ required: true, message: '服务地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="服务地址"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.url"
|
||||||
|
placeholder="请输入 MQTT 服务地址,如:mqtt://localhost:1883"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.username"
|
||||||
|
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
|
||||||
|
label="用户名"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.username" placeholder="请输入用户名" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.password"
|
||||||
|
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
|
||||||
|
label="密码"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.clientId"
|
||||||
|
:rules="[{ required: true, message: '客户端 ID 不能为空', trigger: 'blur' }]"
|
||||||
|
label="客户端 ID"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.clientId" placeholder="请输入客户端 ID" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.topic"
|
||||||
|
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
|
||||||
|
label="主题"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.RABBITMQ}`,
|
||||||
|
host: '',
|
||||||
|
port: 5672,
|
||||||
|
virtualHost: '/',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
exchange: '',
|
||||||
|
routingKey: '',
|
||||||
|
queue: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.host"
|
||||||
|
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="主机地址"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.host" placeholder="请输入主机地址,如:localhost" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.port"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: '端口不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65_535,
|
||||||
|
message: '端口号范围 1-65535',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
label="端口"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.port"
|
||||||
|
:max="65535"
|
||||||
|
:min="1"
|
||||||
|
placeholder="请输入端口"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.virtualHost"
|
||||||
|
:rules="[{ required: true, message: '虚拟主机不能为空', trigger: 'blur' }]"
|
||||||
|
label="虚拟主机"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.virtualHost" placeholder="请输入虚拟主机" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.username"
|
||||||
|
:rules="[{ required: true, message: '用户名不能为空', trigger: 'blur' }]"
|
||||||
|
label="用户名"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.username" placeholder="请输入用户名" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.password"
|
||||||
|
:rules="[{ required: true, message: '密码不能为空', trigger: 'blur' }]"
|
||||||
|
label="密码"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.exchange"
|
||||||
|
:rules="[{ required: true, message: '交换机不能为空', trigger: 'blur' }]"
|
||||||
|
label="交换机"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.exchange" placeholder="请输入交换机" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.routingKey"
|
||||||
|
:rules="[{ required: true, message: '路由键不能为空', trigger: 'blur' }]"
|
||||||
|
label="路由键"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.routingKey" placeholder="请输入路由键" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.queue"
|
||||||
|
:rules="[{ required: true, message: '队列不能为空', trigger: 'blur' }]"
|
||||||
|
label="队列"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.queue" placeholder="请输入队列" />
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { ElFormItem, ElInput, ElInputNumber } from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.REDIS_STREAM}`,
|
||||||
|
host: '',
|
||||||
|
port: 6379,
|
||||||
|
password: '',
|
||||||
|
database: 0,
|
||||||
|
topic: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.host"
|
||||||
|
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="主机地址"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.host" placeholder="请输入主机地址,如:localhost" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.port"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: '端口不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65_535,
|
||||||
|
message: '端口号范围 1-65535',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
label="端口"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.port"
|
||||||
|
:max="65535"
|
||||||
|
:min="1"
|
||||||
|
placeholder="请输入端口"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.password" label="密码">
|
||||||
|
<ElInput
|
||||||
|
v-model="config.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.database"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: '数据库索引不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
message: '数据库索引必须是非负整数',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
label="数据库"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.database"
|
||||||
|
:max="15"
|
||||||
|
:min="0"
|
||||||
|
placeholder="请输入数据库索引"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.topic"
|
||||||
|
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
|
||||||
|
label="主题"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import { ElFormItem, ElInput } from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.ROCKETMQ}`,
|
||||||
|
nameServer: '',
|
||||||
|
accessKey: '',
|
||||||
|
secretKey: '',
|
||||||
|
group: '',
|
||||||
|
topic: '',
|
||||||
|
tags: '',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.nameServer"
|
||||||
|
:rules="[{ required: true, message: 'NameServer 地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="NameServer"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.nameServer"
|
||||||
|
placeholder="请输入 NameServer 地址,如:127.0.0.1:9876"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.accessKey"
|
||||||
|
:rules="[{ required: true, message: 'AccessKey 不能为空', trigger: 'blur' }]"
|
||||||
|
label="AccessKey"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.accessKey" placeholder="请输入 AccessKey" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.secretKey"
|
||||||
|
:rules="[{ required: true, message: 'SecretKey 不能为空', trigger: 'blur' }]"
|
||||||
|
label="SecretKey"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.secretKey"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入 SecretKey"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.group"
|
||||||
|
:rules="[{ required: true, message: '消费组不能为空', trigger: 'blur' }]"
|
||||||
|
label="消费组"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.group" placeholder="请输入消费组" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.topic"
|
||||||
|
:rules="[{ required: true, message: '主题不能为空', trigger: 'blur' }]"
|
||||||
|
label="主题"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.topic" placeholder="请输入主题" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.tags" label="标签">
|
||||||
|
<ElInput v-model="config.tags" placeholder="请输入标签" />
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import {
|
||||||
|
ElFormItem,
|
||||||
|
ElInput,
|
||||||
|
ElInputNumber,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
ElSwitch,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.TCP}`,
|
||||||
|
host: '',
|
||||||
|
port: 8080,
|
||||||
|
connectTimeoutMs: 5000,
|
||||||
|
readTimeoutMs: 10_000,
|
||||||
|
ssl: false,
|
||||||
|
sslCertPath: '',
|
||||||
|
dataFormat: 'JSON',
|
||||||
|
heartbeatIntervalMs: 30_000,
|
||||||
|
reconnectIntervalMs: 5000,
|
||||||
|
maxReconnectAttempts: 3,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.host"
|
||||||
|
:rules="[{ required: true, message: '主机地址不能为空', trigger: 'blur' }]"
|
||||||
|
label="服务器地址"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.host"
|
||||||
|
placeholder="请输入 TCP 服务器地址,如:localhost"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.port"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: '端口不能为空', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
max: 65_535,
|
||||||
|
message: '端口号范围 1-65535',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
label="端口"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.port"
|
||||||
|
:max="65535"
|
||||||
|
:min="1"
|
||||||
|
placeholder="请输入端口"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.connectTimeoutMs"
|
||||||
|
:rules="[{ required: true, message: '连接超时时间不能为空', trigger: 'blur' }]"
|
||||||
|
label="连接超时(ms)"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.connectTimeoutMs"
|
||||||
|
:min="1000"
|
||||||
|
:step="1000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.readTimeoutMs"
|
||||||
|
:rules="[{ required: true, message: '读取超时时间不能为空', trigger: 'blur' }]"
|
||||||
|
label="读取超时(ms)"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.readTimeoutMs"
|
||||||
|
:min="1000"
|
||||||
|
:step="1000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.ssl" label="启用 SSL">
|
||||||
|
<ElSwitch v-model="config.ssl" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
v-if="config.ssl"
|
||||||
|
prop="config.sslCertPath"
|
||||||
|
label="SSL 证书路径"
|
||||||
|
>
|
||||||
|
<ElInput v-model="config.sslCertPath" placeholder="请输入 SSL 证书路径" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.dataFormat"
|
||||||
|
:rules="[{ required: true, message: '数据格式不能为空', trigger: 'change' }]"
|
||||||
|
label="数据格式"
|
||||||
|
>
|
||||||
|
<ElSelect v-model="config.dataFormat" placeholder="请选择数据格式">
|
||||||
|
<ElOption label="JSON" value="JSON" />
|
||||||
|
<ElOption label="BINARY" value="BINARY" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.heartbeatIntervalMs" label="心跳间隔(ms)">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.heartbeatIntervalMs"
|
||||||
|
:min="0"
|
||||||
|
:step="1000"
|
||||||
|
placeholder="0 表示不启用心跳"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.reconnectIntervalMs" label="重连间隔(ms)">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.reconnectIntervalMs"
|
||||||
|
:min="1000"
|
||||||
|
:step="1000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.maxReconnectAttempts" label="最大重连次数">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.maxReconnectAttempts"
|
||||||
|
:min="0"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { useVModel } from '@vueuse/core';
|
||||||
|
import {
|
||||||
|
ElFormItem,
|
||||||
|
ElInput,
|
||||||
|
ElInputNumber,
|
||||||
|
ElOption,
|
||||||
|
ElSelect,
|
||||||
|
ElSwitch,
|
||||||
|
} from 'element-plus';
|
||||||
|
|
||||||
|
import { IotDataSinkTypeEnum } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: any }>();
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const config = useVModel(props, 'modelValue', emit);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!isEmpty(config.value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.value = {
|
||||||
|
type: `${IotDataSinkTypeEnum.WEBSOCKET}`,
|
||||||
|
serverUrl: '',
|
||||||
|
connectTimeoutMs: 5000,
|
||||||
|
sendTimeoutMs: 10_000,
|
||||||
|
heartbeatIntervalMs: 30_000,
|
||||||
|
heartbeatMessage: '{"type":"heartbeat"}',
|
||||||
|
subprotocols: '',
|
||||||
|
customHeaders: '',
|
||||||
|
verifySslCert: true,
|
||||||
|
dataFormat: 'JSON',
|
||||||
|
reconnectIntervalMs: 5000,
|
||||||
|
maxReconnectAttempts: 3,
|
||||||
|
enableCompression: false,
|
||||||
|
sendRetryCount: 1,
|
||||||
|
sendRetryIntervalMs: 1000,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.serverUrl"
|
||||||
|
:rules="[
|
||||||
|
{ required: true, message: 'WebSocket 服务器地址不能为空', trigger: 'blur' },
|
||||||
|
]"
|
||||||
|
label="服务器地址"
|
||||||
|
>
|
||||||
|
<ElInput
|
||||||
|
v-model="config.serverUrl"
|
||||||
|
placeholder="请输入 WebSocket 地址,如:ws://localhost:8080/ws"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.connectTimeoutMs"
|
||||||
|
:rules="[{ required: true, message: '连接超时时间不能为空', trigger: 'blur' }]"
|
||||||
|
label="连接超时(ms)"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.connectTimeoutMs"
|
||||||
|
:min="1000"
|
||||||
|
:step="1000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.sendTimeoutMs"
|
||||||
|
:rules="[{ required: true, message: '发送超时时间不能为空', trigger: 'blur' }]"
|
||||||
|
label="发送超时(ms)"
|
||||||
|
>
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.sendTimeoutMs"
|
||||||
|
:min="1000"
|
||||||
|
:step="1000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.heartbeatIntervalMs" label="心跳间隔(ms)">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.heartbeatIntervalMs"
|
||||||
|
:min="0"
|
||||||
|
:step="1000"
|
||||||
|
placeholder="0 表示不启用心跳"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.heartbeatMessage" label="心跳消息">
|
||||||
|
<ElInput
|
||||||
|
v-model="config.heartbeatMessage"
|
||||||
|
placeholder="请输入心跳消息内容(JSON 格式)"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.subprotocols" label="子协议">
|
||||||
|
<ElInput
|
||||||
|
v-model="config.subprotocols"
|
||||||
|
placeholder="请输入子协议列表,多个用逗号分隔"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.customHeaders" label="自定义请求头">
|
||||||
|
<ElInput
|
||||||
|
v-model="config.customHeaders"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="请输入自定义请求头(JSON 格式)"
|
||||||
|
:rows="3"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.verifySslCert" label="验证 SSL 证书">
|
||||||
|
<ElSwitch v-model="config.verifySslCert" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem
|
||||||
|
prop="config.dataFormat"
|
||||||
|
:rules="[{ required: true, message: '数据格式不能为空', trigger: 'change' }]"
|
||||||
|
label="数据格式"
|
||||||
|
>
|
||||||
|
<ElSelect v-model="config.dataFormat" placeholder="请选择数据格式">
|
||||||
|
<ElOption label="JSON" value="JSON" />
|
||||||
|
<ElOption label="TEXT" value="TEXT" />
|
||||||
|
</ElSelect>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.reconnectIntervalMs" label="重连间隔(ms)">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.reconnectIntervalMs"
|
||||||
|
:min="1000"
|
||||||
|
:step="1000"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.maxReconnectAttempts" label="最大重连次数">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.maxReconnectAttempts"
|
||||||
|
:min="0"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.enableCompression" label="启用压缩">
|
||||||
|
<ElSwitch v-model="config.enableCompression" />
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.sendRetryCount" label="发送重试次数">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.sendRetryCount"
|
||||||
|
:min="0"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
<ElFormItem prop="config.sendRetryIntervalMs" label="重试间隔(ms)">
|
||||||
|
<ElInputNumber
|
||||||
|
v-model="config.sendRetryIntervalMs"
|
||||||
|
:min="100"
|
||||||
|
:step="500"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</ElFormItem>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||||
|
import type { DataSinkApi } from '#/api/iot/rule/data/sink';
|
||||||
|
|
||||||
|
import { DICT_TYPE } from '@vben/constants';
|
||||||
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
|
import { getRangePickerDefaultProps } from '#/utils';
|
||||||
|
|
||||||
|
/** 列表的搜索表单 */
|
||||||
|
export function useGridFormSchema(): VbenFormSchema[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
fieldName: 'name',
|
||||||
|
label: '目的名称',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {
|
||||||
|
placeholder: '请输入目的名称',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'status',
|
||||||
|
label: '目的状态',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
|
||||||
|
placeholder: '请选择状态',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'type',
|
||||||
|
label: '目的类型',
|
||||||
|
component: 'Select',
|
||||||
|
componentProps: {
|
||||||
|
options: getDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM, 'number'),
|
||||||
|
placeholder: '请选择目的类型',
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'createTime',
|
||||||
|
label: '创建时间',
|
||||||
|
component: 'RangePicker',
|
||||||
|
componentProps: {
|
||||||
|
...getRangePickerDefaultProps(),
|
||||||
|
clearable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 列表的字段 */
|
||||||
|
export function useGridColumns(): VxeTableGridOptions<DataSinkApi.DataSink>['columns'] {
|
||||||
|
return [
|
||||||
|
{ type: 'checkbox', width: 40 },
|
||||||
|
{
|
||||||
|
field: 'id',
|
||||||
|
title: '目的编号',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '目的名称',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
title: '目的描述',
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
title: '目的状态',
|
||||||
|
minWidth: 100,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'type',
|
||||||
|
title: '目的类型',
|
||||||
|
minWidth: 120,
|
||||||
|
cellRender: {
|
||||||
|
name: 'CellDict',
|
||||||
|
props: { type: DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'createTime',
|
||||||
|
title: '创建时间',
|
||||||
|
minWidth: 180,
|
||||||
|
formatter: 'formatDateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
width: 160,
|
||||||
|
fixed: 'right',
|
||||||
|
slots: { default: 'actions' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue