feat(iot): 优化 iot 的代码风格(迁移 constants.ts)地址
parent
e816288b82
commit
3f09fc1498
|
|
@ -11,6 +11,7 @@ export namespace RuleSceneApi {
|
|||
status?: number;
|
||||
triggers?: Trigger[];
|
||||
actions?: Action[];
|
||||
lastTriggeredTime?: Date;
|
||||
createTime?: Date;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export namespace IotStatisticsApi {
|
|||
|
||||
/** 设备消息数量统计(按日期) */
|
||||
export interface DeviceMessageSummaryByDateRespVO {
|
||||
time: Date; // 时间轴
|
||||
time: string; // 时间轴
|
||||
upstreamCount: number; // 上行消息数量
|
||||
downstreamCount: number; // 下行消息数量
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,15 @@ export namespace ThingModelApi {
|
|||
dataSpecsList?: any[];
|
||||
}
|
||||
|
||||
/** IoT 物模型 TSL(树形)响应 */
|
||||
export interface ThingModelTSL {
|
||||
productId?: number;
|
||||
productKey?: string;
|
||||
properties?: Property[];
|
||||
events?: Event[];
|
||||
services?: Service[];
|
||||
}
|
||||
|
||||
/** IoT 数据定义(数值型) */
|
||||
export interface DataSpecsNumberData {
|
||||
min?: number | string;
|
||||
|
|
@ -236,7 +245,10 @@ export function deleteThingModel(id: number) {
|
|||
|
||||
/** 获取物模型 TSL */
|
||||
export function getThingModelTSL(productId: number) {
|
||||
return requestClient.get<any>('/iot/thing-model/get-tsl', {
|
||||
params: { productId },
|
||||
});
|
||||
return requestClient.get<ThingModelApi.ThingModelTSL>(
|
||||
'/iot/thing-model/get-tsl',
|
||||
{
|
||||
params: { productId },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,20 +115,20 @@ export function useGridColumns(): VxeTableGridOptions<AlertRecordApi.AlertRecord
|
|||
props: { type: DICT_TYPE.IOT_ALERT_LEVEL },
|
||||
},
|
||||
},
|
||||
// TODO @AI:非必要,不缩写;product、device
|
||||
{
|
||||
field: 'productId',
|
||||
title: '产品名称',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) =>
|
||||
productList.find((p) => p.id === cellValue)?.name || '-',
|
||||
productList.find((product) => product.id === cellValue)?.name || '-',
|
||||
},
|
||||
{
|
||||
field: 'deviceId',
|
||||
title: '设备名称',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) =>
|
||||
deviceList.find((d) => d.id === cellValue)?.deviceName || '-',
|
||||
deviceList.find((device) => device.id === cellValue)?.deviceName ||
|
||||
'-',
|
||||
},
|
||||
{
|
||||
field: 'deviceMessage',
|
||||
|
|
|
|||
|
|
@ -24,16 +24,13 @@ const props = defineProps<{
|
|||
deviceId: number;
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
method: undefined,
|
||||
upstream: undefined,
|
||||
});
|
||||
}); // 查询参数
|
||||
|
||||
/** 自动刷新开关 */
|
||||
const autoRefresh = ref(false);
|
||||
/** 自动刷新定时器 */
|
||||
let autoRefreshTimer: any = null;
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||
|
||||
/** 消息方法选项 */
|
||||
const methodOptions = computed(() => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import type { IotDeviceModbusConfigApi } from '#/api/iot/device/modbus/config';
|
|||
import { computed, ref } from 'vue';
|
||||
|
||||
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 { message } from 'ant-design-vue';
|
||||
|
|
@ -14,10 +19,6 @@ import { useVbenForm, z } from '#/adapter/form';
|
|||
import { saveModbusConfig } from '#/api/iot/device/modbus/config';
|
||||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
ModbusFrameFormatEnum,
|
||||
ModbusModeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { DescriptionItemSchema } from '#/components/description';
|
|||
import { computed, h, onMounted, ref } from 'vue';
|
||||
|
||||
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';
|
||||
|
||||
|
|
@ -25,7 +25,6 @@ import {
|
|||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { ModbusFunctionCodeOptions } from '#/views/iot/utils/constants';
|
||||
|
||||
import DeviceModbusConfigForm from './modbus-config-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 { 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 { message } from 'ant-design-vue';
|
||||
|
|
@ -19,12 +26,6 @@ import {
|
|||
updateModbusPoint,
|
||||
} from '#/api/iot/device/modbus/point';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
getByteOrderOptions,
|
||||
IoTThingModelTypeEnum,
|
||||
ModbusFunctionCodeOptions,
|
||||
ModbusRawDataTypeOptions,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import {
|
||||
DeviceStateEnum,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
|
|
@ -25,10 +29,6 @@ import {
|
|||
} from 'ant-design-vue';
|
||||
|
||||
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 DeviceDetailsMessage from './message.vue';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import type { IotDeviceApi } from '#/api/iot/device/device';
|
|||
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -26,13 +27,11 @@ import dayjs from 'dayjs';
|
|||
|
||||
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
defineProps<{ deviceId: number }>();
|
||||
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const loading = ref(false);
|
||||
const exporting = ref(false);
|
||||
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
||||
const total = ref(0); // 总数据量
|
||||
|
|
@ -330,54 +329,6 @@ function handleRefresh() {
|
|||
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() {
|
||||
dialogVisible.value = false;
|
||||
|
|
@ -429,18 +380,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
|||
刷新
|
||||
</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
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
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 { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="device-card-view">
|
||||
<div>
|
||||
<!-- 设备卡片列表 -->
|
||||
<div v-loading="loading" class="min-h-96">
|
||||
<Row v-if="list.length > 0" :gutter="[16, 16]">
|
||||
|
|
@ -118,30 +118,43 @@ onMounted(() => {
|
|||
:lg="6"
|
||||
>
|
||||
<Card
|
||||
:body-style="{ padding: '16px' }"
|
||||
class="device-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:body-style="{
|
||||
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="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" />
|
||||
</div>
|
||||
<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>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
||||
:value="item.state"
|
||||
class="status-tag"
|
||||
class="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="mb-3 flex items-start">
|
||||
<div class="info-list flex-1">
|
||||
<div class="info-item">
|
||||
<span class="info-label">所属产品</span>
|
||||
<div class="flex-1">
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
所属产品
|
||||
</span>
|
||||
<a
|
||||
class="info-value cursor-pointer text-primary"
|
||||
class="cursor-pointer truncate font-medium text-primary"
|
||||
@click="
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -152,25 +165,33 @@ onMounted(() => {
|
|||
{{ getProductName(item.productId) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">设备类型</span>
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
设备类型
|
||||
</span>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="item.deviceType"
|
||||
class="info-tag m-0"
|
||||
class="m-0 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Deviceid</span>
|
||||
<div class="flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
Deviceid
|
||||
</span>
|
||||
<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 }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</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
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
|
|
@ -185,11 +206,13 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<div
|
||||
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||
>
|
||||
<Button
|
||||
v-if="hasAccessByCodes(['iot:device:update'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||
|
|
@ -198,7 +221,7 @@ onMounted(() => {
|
|||
<Button
|
||||
v-if="hasAccessByCodes(['iot:device:query'])"
|
||||
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!)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||
|
|
@ -207,7 +230,7 @@ onMounted(() => {
|
|||
<Button
|
||||
v-if="hasAccessByCodes(['iot:device:message-query'])"
|
||||
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!)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:database" class="mr-1" />
|
||||
|
|
@ -221,7 +244,7 @@ onMounted(() => {
|
|||
<Button
|
||||
size="small"
|
||||
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" />
|
||||
</Button>
|
||||
|
|
@ -249,187 +272,3 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
</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(
|
||||
value: number,
|
||||
max: number,
|
||||
|
|
@ -129,9 +127,7 @@ export function getDeviceStateGaugeChartOptions(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备数量饼图配置
|
||||
*/
|
||||
/** 设备数量饼图配置 */
|
||||
export function getDeviceCountPieChartOptions(
|
||||
data: Array<{ name: string; value: number }>,
|
||||
): any {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ async function loadData() {
|
|||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,7 +21,9 @@ const { renderEcharts } = useEcharts(deviceCountChartRef);
|
|||
|
||||
/** 是否有数据 */
|
||||
const hasData = computed(() => {
|
||||
if (!props.statsData) return false;
|
||||
if (!props.statsData) {
|
||||
return false;
|
||||
}
|
||||
const categories = Object.entries(
|
||||
props.statsData.productCategoryDeviceCounts || {},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
<script lang="ts" setup>
|
||||
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 { DICT_TYPE } from '@vben/constants';
|
||||
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
import { Card, Empty, Spin } from 'ant-design-vue';
|
||||
|
||||
import { getDeviceLocationList } from '#/api/iot/device/device';
|
||||
import { loadBaiduMapSdk } from '#/components/map';
|
||||
import { DeviceStateEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
defineOptions({ name: 'DeviceMapCard' });
|
||||
|
||||
|
|
@ -152,6 +151,8 @@ async function init() {
|
|||
return;
|
||||
}
|
||||
await loadBaiduMapSdk();
|
||||
// 等待 v-show 容器渲染完成;SDK 缓存命中时上一行会同步 resolve,DOM 来不及切换
|
||||
await nextTick();
|
||||
initMap();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useDescription } from '#/components/description';
|
|||
|
||||
import { useDetailSchema } from '../../data';
|
||||
|
||||
/** IoT OTA 固件基本信息 */
|
||||
defineProps<{
|
||||
firmware?: IoTOtaFirmwareApi.Firmware;
|
||||
loading?: boolean;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
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 { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
/** 任务详情的描述字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
|
|||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTOtaTaskStatusEnum } from '@vben/constants';
|
||||
|
||||
import { Input, message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { cancelOtaTask, getOtaTaskPage } from '#/api/iot/ota/task';
|
||||
import { $t } from '#/locales';
|
||||
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import { useGridColumns } from '../data';
|
||||
import OtaTaskDetail from './detail.vue';
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { DICT_TYPE, IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
import { Card, Col, Row } from 'ant-design-vue';
|
||||
|
||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
loading?: boolean;
|
||||
statistics: Record<string, number>;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import type { IoTOtaTaskRecordApi } from '#/api/iot/ota/task/record';
|
|||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||
|
||||
import { Card, message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
|
@ -12,7 +14,6 @@ import {
|
|||
getOtaTaskRecordPage,
|
||||
} from '#/api/iot/ota/task/record';
|
||||
import { $t } from '#/locales';
|
||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import { useGridColumns } from '../data';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import { onMounted, provide, ref } from 'vue';
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||
|
||||
import { message, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { getDeviceCount } from '#/api/iot/device/device';
|
||||
import { getProduct } from '#/api/iot/product/product';
|
||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||
|
||||
import ProductDetailsHeader from './modules/header.vue';
|
||||
import ProductDetailsInfo from './modules/info.vue';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ const showProductSecret = ref(false); // 是否显示产品密钥
|
|||
|
||||
/** 格式化日期 */
|
||||
function formatDate(date?: Date | string) {
|
||||
if (!date) return '-';
|
||||
if (!date) {
|
||||
return '-';
|
||||
}
|
||||
return new Date(date).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="product-card-view">
|
||||
<div>
|
||||
<!-- 产品卡片列表 -->
|
||||
<div v-loading="loading" class="min-h-96">
|
||||
<Row v-if="list.length > 0" :gutter="[16, 16]">
|
||||
|
|
@ -104,49 +104,70 @@ onMounted(() => {
|
|||
:lg="6"
|
||||
>
|
||||
<Card
|
||||
:body-style="{ padding: '16px' }"
|
||||
class="product-card h-full rounded-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg"
|
||||
:body-style="{
|
||||
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="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
|
||||
:icon="item.icon || 'lucide:box'"
|
||||
class="text-xl"
|
||||
/>
|
||||
</div>
|
||||
<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 class="mb-3 flex items-start">
|
||||
<div class="info-list flex-1">
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品分类</span>
|
||||
<span class="info-value text-primary">
|
||||
<div class="flex-1">
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
产品分类
|
||||
</span>
|
||||
<span class="truncate font-medium text-primary">
|
||||
{{ getCategoryName(item.categoryId) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品类型</span>
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
产品类型
|
||||
</span>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="item.deviceType"
|
||||
class="info-tag m-0"
|
||||
class="m-0 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品标识</span>
|
||||
<div class="flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
产品标识
|
||||
</span>
|
||||
<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 }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</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
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
|
|
@ -161,11 +182,13 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<div
|
||||
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||
>
|
||||
<Button
|
||||
v-if="hasAccessByCodes(['iot:product:update'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||
|
|
@ -174,7 +197,7 @@ onMounted(() => {
|
|||
<Button
|
||||
v-if="hasAccessByCodes(['iot:product:query'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||
|
|
@ -183,7 +206,7 @@ onMounted(() => {
|
|||
<Button
|
||||
v-if="hasAccessByCodes(['iot:thing-model:query'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
||||
|
|
@ -198,7 +221,7 @@ onMounted(() => {
|
|||
size="small"
|
||||
danger
|
||||
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" />
|
||||
</Button>
|
||||
|
|
@ -211,7 +234,7 @@ onMounted(() => {
|
|||
<Button
|
||||
size="small"
|
||||
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" />
|
||||
</Button>
|
||||
|
|
@ -240,182 +263,3 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
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 { getSimpleProductList } from '#/api/iot/product/product';
|
||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||
import { IoTThingModelTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import { useSourceConfigColumns } from '../data';
|
||||
|
||||
|
|
@ -184,13 +183,12 @@ async function setData(data: any[]) {
|
|||
...item,
|
||||
identifierLoading: false,
|
||||
}));
|
||||
// 为已有数据预加载物模型
|
||||
// TODO @AI:这里有 linter 报错:Promise returned from forEach argument is ignored
|
||||
data?.forEach(async (item) => {
|
||||
if (item.productId && shouldShowIdentifierSelect(item)) {
|
||||
await loadThingModel(item.productId);
|
||||
}
|
||||
});
|
||||
// 为已有数据预加载物模型;并行加载,不阻塞 reloadGrid
|
||||
await Promise.all(
|
||||
(data || [])
|
||||
.filter((item) => item.productId && shouldShowIdentifierSelect(item))
|
||||
.map((item) => loadThingModel(item.productId)),
|
||||
);
|
||||
await reloadGrid();
|
||||
}
|
||||
|
||||
|
|
@ -274,7 +272,7 @@ defineExpose({ validate, getData, setData });
|
|||
:options="getThingModelOptions(formData[rowIndex])"
|
||||
class="w-full"
|
||||
/>
|
||||
<span v-else class="text-xs text-secondary">-</span>
|
||||
<span v-else class="text-xs text-muted-foreground">-</span>
|
||||
</template>
|
||||
<template #actions="{ rowIndex }">
|
||||
<Button danger type="link" @click="handleDelete(rowIndex)">删除</Button>
|
||||
|
|
|
|||
|
|
@ -92,39 +92,42 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
|||
{
|
||||
field: 'name',
|
||||
title: '规则名称',
|
||||
minWidth: 180,
|
||||
minWidth: 200,
|
||||
align: 'left',
|
||||
showOverflow: false,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{
|
||||
field: 'triggers',
|
||||
title: '触发条件',
|
||||
minWidth: 260,
|
||||
minWidth: 280,
|
||||
align: 'left',
|
||||
showOverflow: false,
|
||||
slots: { default: 'triggers' },
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
title: '执行动作',
|
||||
minWidth: 220,
|
||||
minWidth: 250,
|
||||
align: 'left',
|
||||
showOverflow: false,
|
||||
slots: { default: 'actionsCol' },
|
||||
},
|
||||
{
|
||||
field: 'status',
|
||||
title: '规则状态',
|
||||
width: 90,
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.COMMON_STATUS },
|
||||
},
|
||||
field: 'lastTriggeredTime',
|
||||
title: '最近触发',
|
||||
width: 180,
|
||||
slots: { default: 'lastTriggeredTime' },
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
width: 160,
|
||||
width: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
width: 210,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ onMounted(() => {
|
|||
<Select
|
||||
v-model:value="localValue"
|
||||
placeholder="请选择告警配置"
|
||||
filterable
|
||||
clearable
|
||||
show-search
|
||||
allow-clear
|
||||
@change="handleChange"
|
||||
class="w-full"
|
||||
:loading="loading"
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
|||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Col, Form, Row, Select } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getConditionTypeOptions,
|
||||
IoTDeviceStatusEnum,
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
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 DeviceSelector from '../selectors/device-selector.vue';
|
||||
|
|
@ -226,7 +226,7 @@ function handleOperatorChange() {
|
|||
:value="condition.operator"
|
||||
@change="
|
||||
(value: any) => updateConditionField('operator', value)
|
||||
|
||||
|
||||
"
|
||||
placeholder="请选择操作符"
|
||||
class="w-full"
|
||||
|
|
@ -249,7 +249,7 @@ function handleOperatorChange() {
|
|||
:value="condition.param"
|
||||
@change="
|
||||
(value: any) => updateConditionField('param', value)
|
||||
|
||||
|
||||
"
|
||||
placeholder="请选择设备状态"
|
||||
class="w-full"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
|||
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { IotRuleSceneTriggerTimeOperatorEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
|
|
@ -17,8 +18,6 @@ import {
|
|||
TimePicker,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { IotRuleSceneTriggerTimeOperatorEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
/** 当前时间条件配置组件 */
|
||||
defineOptions({ name: 'CurrentTimeConditionConfig' });
|
||||
|
||||
|
|
@ -225,7 +224,7 @@ watch(
|
|||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="w-full"
|
||||
/>
|
||||
<div v-else class="text-sm text-secondary">无需设置时间值</div>
|
||||
<div v-else class="text-sm text-muted-foreground">无需设置时间值</div>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IotRuleSceneActionTypeEnum,
|
||||
IoTThingModelAccessModeEnum,
|
||||
} from '@vben/constants';
|
||||
import { isObject } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Col, Form, Row, Select, Tag } from 'ant-design-vue';
|
||||
|
||||
import { getThingModelListByProductId } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IotRuleSceneActionTypeEnum,
|
||||
IoTThingModelAccessModeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
import { getThingModelTSL } from '#/api/iot/thingmodel';
|
||||
|
||||
import JsonParamsInput from '../inputs/json-params-input.vue';
|
||||
import DeviceSelector from '../selectors/device-selector.vue';
|
||||
|
|
@ -133,18 +133,13 @@ function handleServiceChange(serviceIdentifier?: any) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物模型TSL数据
|
||||
* @param productId 产品ID
|
||||
* @returns 物模型TSL数据
|
||||
*/
|
||||
async function getThingModelTSL(productId: number): Promise<any> {
|
||||
/** 获取物模型 TSL 数据 */
|
||||
async function fetchThingModelTSL(productId: number) {
|
||||
if (!productId) return null;
|
||||
|
||||
try {
|
||||
return await getThingModelListByProductId(productId);
|
||||
return await getThingModelTSL(productId);
|
||||
} catch (error) {
|
||||
console.error('获取物模型TSL数据失败:', error);
|
||||
console.error('获取物模型 TSL 数据失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +156,7 @@ async function loadThingModelProperties(productId: number) {
|
|||
|
||||
try {
|
||||
loadingThingModel.value = true;
|
||||
const tslData = await getThingModelTSL(productId);
|
||||
const tslData = await fetchThingModelTSL(productId);
|
||||
|
||||
// TODO DONE @AI:这里有 linter 报错
|
||||
if (!tslData?.properties) {
|
||||
|
|
@ -196,7 +191,7 @@ async function loadServiceList(productId: number) {
|
|||
|
||||
try {
|
||||
loadingServices.value = true;
|
||||
const tslData = await getThingModelTSL(productId);
|
||||
const tslData = await fetchThingModelTSL(productId);
|
||||
|
||||
// TODO DONE @AI:这里有 linter 报错
|
||||
if (!tslData?.services) {
|
||||
|
|
@ -368,8 +363,8 @@ watch(
|
|||
<Select
|
||||
v-model:value="action.identifier"
|
||||
placeholder="请选择服务"
|
||||
filterable
|
||||
clearable
|
||||
show-search
|
||||
allow-clear
|
||||
class="w-full"
|
||||
:loading="loadingServices"
|
||||
@change="handleServiceChange"
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ function removeConditionGroup() {
|
|||
<MainConditionInnerConfig
|
||||
:model-value="trigger"
|
||||
@update:model-value="updateCondition"
|
||||
:trigger-type="(trigger.type as number)"
|
||||
:trigger-type="trigger.type as number"
|
||||
@trigger-type-change="handleTriggerTypeChange"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -228,7 +228,7 @@ function removeConditionGroup() {
|
|||
@update:model-value="
|
||||
(value) => updateSubGroup(subGroupIndex, value)
|
||||
"
|
||||
:trigger-type="(trigger.type as number)"
|
||||
:trigger-type="trigger.type as number"
|
||||
:max-conditions="maxConditionsPerGroup"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ import type { Trigger } from '#/api/iot/rule/scene';
|
|||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Col, Form, Row, Select } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getTriggerTypeLabel,
|
||||
IoTDeviceStatusEnum,
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
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 ValueInput from '../inputs/value-input.vue';
|
||||
|
|
@ -368,10 +368,10 @@ function handlePropertyChange(propertyInfo: any) {
|
|||
|
||||
<!-- 其他触发类型的提示 -->
|
||||
<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) }}
|
||||
</p>
|
||||
<p class="text-xs text-secondary">此触发类型暂不需要配置额外条件</p>
|
||||
<p class="text-xs text-muted-foreground">此触发类型暂不需要配置额外条件</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
|||
|
||||
import { computed, nextTick } from 'vue';
|
||||
|
||||
import {
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
IotRuleSceneTriggerConditionTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
IotRuleSceneTriggerConditionTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
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 class="flex flex-col items-center gap-3">
|
||||
<IconifyIcon icon="lucide:plus" class="text-8 text-secondary" />
|
||||
<div class="text-secondary">
|
||||
<IconifyIcon icon="lucide:plus" class="text-8 text-muted-foreground" />
|
||||
<div class="text-muted-foreground">
|
||||
<p class="mb-1 text-base font-bold">暂无条件</p>
|
||||
<p class="text-xs">点击下方按钮添加第一个条件</p>
|
||||
</div>
|
||||
|
|
@ -119,7 +118,7 @@ function updateCondition(index: number, condition: TriggerCondition) {
|
|||
>
|
||||
{{ conditionIndex + 1 }}
|
||||
</div>
|
||||
<span class="text-base font-bold text-primary">
|
||||
<span class="text-base font-bold text-foreground">
|
||||
条件 {{ conditionIndex + 1 }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -159,7 +158,7 @@ function updateCondition(index: number, condition: TriggerCondition) {
|
|||
<IconifyIcon icon="lucide:plus" />
|
||||
继续添加条件
|
||||
</Button>
|
||||
<span class="mt-2 block text-xs text-secondary">
|
||||
<span class="mt-2 block text-xs text-muted-foreground">
|
||||
最多可添加 {{ maxConditions }} 个条件
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,12 @@ import type { TriggerCondition } from '#/api/iot/rule/scene';
|
|||
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
import { IotRuleSceneTriggerTypeEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Tag } from 'ant-design-vue';
|
||||
|
||||
import { IotRuleSceneTriggerTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import SubConditionGroupConfig from './sub-condition-group-config.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
<!-- JSON参数输入组件 - 通用版本 -->
|
||||
<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 { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Input, Popover, Tag } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
JSON_PARAMS_EXAMPLE_VALUES,
|
||||
JSON_PARAMS_INPUT_CONSTANTS,
|
||||
JSON_PARAMS_INPUT_ICONS,
|
||||
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参数输入组件 - 通用版本 */
|
||||
defineOptions({ name: 'JsonParamsInput' });
|
||||
|
|
@ -429,28 +428,17 @@ watch(
|
|||
<div class="absolute right-2 top-2">
|
||||
<Popover
|
||||
placement="leftTop"
|
||||
:width="450"
|
||||
:overlay-style="{ width: '450px' }"
|
||||
trigger="click"
|
||||
:show-arrow="true"
|
||||
:offset="8"
|
||||
popper-class="json-params-detail-popover"
|
||||
:arrow="true"
|
||||
overlay-class-name="json-params-detail-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<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">
|
||||
<template #content>
|
||||
<!-- 弹出层内容 -->
|
||||
<div class="json-params-detail-content">
|
||||
<div class="mb-4 flex items-center gap-2">
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -463,7 +451,7 @@ watch(
|
|||
:icon="paramsIcon"
|
||||
class="text-base text-primary"
|
||||
/>
|
||||
<span class="text-base font-bold text-primary">
|
||||
<span class="text-base font-bold text-foreground">
|
||||
{{ paramsLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -474,7 +462,7 @@ watch(
|
|||
class="flex items-center justify-between rounded-lg bg-card p-2"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="text-base font-bold text-primary">
|
||||
<div class="text-base font-bold text-foreground">
|
||||
{{ param.name }}
|
||||
<Tag
|
||||
v-if="param.required"
|
||||
|
|
@ -484,7 +472,7 @@ watch(
|
|||
{{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
|
||||
</Tag>
|
||||
</div>
|
||||
<div class="text-xs text-secondary">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
{{ param.identifier }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -492,7 +480,7 @@ watch(
|
|||
<Tag :color="getParamTypeTag(param.dataType)">
|
||||
{{ getParamTypeName(param.dataType) }}
|
||||
</Tag>
|
||||
<span class="text-xs text-secondary">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ getExampleValue(param) }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -500,11 +488,11 @@ watch(
|
|||
</div>
|
||||
|
||||
<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 }}
|
||||
</div>
|
||||
<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>
|
||||
</pre>
|
||||
|
|
@ -514,13 +502,22 @@ watch(
|
|||
<!-- 无参数提示 -->
|
||||
<div v-else>
|
||||
<div class="py-4 text-center">
|
||||
<p class="text-sm text-secondary">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ emptyMessage }}
|
||||
</p>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -547,7 +544,7 @@ watch(
|
|||
|
||||
<!-- 快速填充按钮 -->
|
||||
<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 }}
|
||||
</span>
|
||||
<Button size="small" type="primary" plain @click="fillExampleJson">
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { DatePicker, Input, Select, Tag, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
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' });
|
||||
|
|
@ -197,7 +197,7 @@ watch(
|
|||
class="min-w-0 flex-1"
|
||||
style="width: auto !important"
|
||||
/>
|
||||
<span class="whitespace-nowrap text-xs text-secondary"> 至 </span>
|
||||
<span class="whitespace-nowrap text-xs text-muted-foreground"> 至 </span>
|
||||
<Input
|
||||
v-model:value="rangeEnd"
|
||||
:type="getInputType()"
|
||||
|
|
@ -232,7 +232,7 @@ watch(
|
|||
v-if="listPreview.length > 0"
|
||||
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
|
||||
v-for="(item, index) in listPreview"
|
||||
:key="index"
|
||||
|
|
@ -282,7 +282,7 @@ watch(
|
|||
:content="`单位:${propertyConfig.unit}`"
|
||||
placement="top"
|
||||
>
|
||||
<span class="px-1 text-xs text-secondary">
|
||||
<span class="px-1 text-xs text-muted-foreground">
|
||||
{{ propertyConfig.unit }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@
|
|||
<script setup lang="ts">
|
||||
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 {
|
||||
getActionTypeLabel,
|
||||
getActionTypeOptions,
|
||||
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 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="gap-[8px] flex items-center">
|
||||
<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>
|
||||
</div>
|
||||
<div class="gap-[8px] flex items-center">
|
||||
|
|
@ -229,9 +228,9 @@ function onActionTypeChange(action: Action, type: number) {
|
|||
<Select
|
||||
:model-value="action.type"
|
||||
@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="请选择执行类型"
|
||||
class="w-full"
|
||||
>
|
||||
|
|
@ -275,10 +274,10 @@ function onActionTypeChange(action: Action, type: number) {
|
|||
>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div class="text-xs leading-relaxed text-secondary">
|
||||
<div class="text-xs leading-relaxed text-muted-foreground">
|
||||
当触发条件满足时,系统将自动发送告警通知,可在菜单 [告警中心 ->
|
||||
告警配置] 管理。
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
|
|||
<div class="flex items-center justify-between">
|
||||
<div class="gap-[8px] flex items-center">
|
||||
<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 class="gap-[8px] flex items-center">
|
||||
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
|
||||
|
|
@ -48,7 +48,7 @@ const formData = useVModel(props, 'modelValue', emit); // 表单数据
|
|||
placeholder="请输入场景名称"
|
||||
:maxlength="50"
|
||||
show-word-limit
|
||||
clearable
|
||||
allow-clear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import type { Trigger, TriggerCondition } from '#/api/iot/rule/scene';
|
|||
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
import {
|
||||
getTriggerTypeLabel,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
isDeviceTrigger,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Card, Empty, Form, Tag } from 'ant-design-vue';
|
||||
|
||||
import { CronTab } from '#/components/cron-tab';
|
||||
import {
|
||||
getTriggerTypeLabel,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
isDeviceTrigger,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import DeviceTriggerConfig from '../configs/device-trigger-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) {
|
||||
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]!;
|
||||
triggerItem.productId = undefined;
|
||||
triggerItem.deviceId = undefined;
|
||||
|
|
@ -113,7 +113,7 @@ onMounted(() => {
|
|||
<div class="flex items-center justify-between">
|
||||
<div class="gap-[8px] flex items-center">
|
||||
<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>
|
||||
</div>
|
||||
<Button type="primary" size="small" @click="addTrigger">
|
||||
|
|
@ -196,7 +196,7 @@ onMounted(() => {
|
|||
icon="lucide:timer"
|
||||
class="text-[18px] text-danger"
|
||||
/>
|
||||
<span class="text-[14px] font-medium text-primary">
|
||||
<span class="text-[14px] font-medium text-foreground">
|
||||
定时触发配置
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -232,8 +232,8 @@ onMounted(() => {
|
|||
<Empty description="暂无触发器">
|
||||
<template #description>
|
||||
<div class="space-y-[8px]">
|
||||
<p class="text-secondary">暂无触发器配置</p>
|
||||
<p class="text-[12px] text-primary">
|
||||
<p class="text-muted-foreground">暂无触发器配置</p>
|
||||
<p class="text-[12px] text-muted-foreground">
|
||||
请使用上方的"添加触发器"按钮来设置触发规则
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
<script setup lang="ts">
|
||||
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 { getDeviceListByProductId } from '#/api/iot/device/device';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { DEVICE_SELECTOR_OPTIONS } from '#/views/iot/utils/constants';
|
||||
|
||||
/** 设备选择器组件 */
|
||||
defineOptions({ name: 'DeviceSelector' });
|
||||
|
|
@ -26,13 +25,11 @@ const emit = defineEmits<{
|
|||
const deviceLoading = ref(false); // 设备加载状态
|
||||
const deviceList = ref<any[]>([]); // 设备列表
|
||||
|
||||
/**
|
||||
* 处理选择变化事件
|
||||
* @param value 选中的设备ID
|
||||
*/
|
||||
function handleChange(value?: number) {
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
/** 处理选择变化事件 */
|
||||
// TODO @AI:是不是应该 value 加个设备编号?方法名是不是也要优化下?方法名是不是要优化?
|
||||
function handleChange(value: any) {
|
||||
emit('update:modelValue', value as number | undefined);
|
||||
emit('change', value as number | undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -46,13 +43,12 @@ async function getDeviceList() {
|
|||
|
||||
try {
|
||||
deviceLoading.value = true;
|
||||
const res = await getDeviceListByProductId(props.productId);
|
||||
deviceList.value = res || [];
|
||||
const data = await getDeviceListByProductId(props.productId);
|
||||
deviceList.value = [DEVICE_SELECTOR_OPTIONS.ALL_DEVICES, ...(data || [])];
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error);
|
||||
deviceList.value = [];
|
||||
deviceList.value = [DEVICE_SELECTOR_OPTIONS.ALL_DEVICES];
|
||||
} finally {
|
||||
deviceList.value.unshift(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES);
|
||||
deviceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -81,8 +77,8 @@ watch(
|
|||
:value="modelValue"
|
||||
@change="handleChange"
|
||||
placeholder="请选择设备"
|
||||
filterable
|
||||
clearable
|
||||
show-search
|
||||
allow-clear
|
||||
class="w-full"
|
||||
:loading="deviceLoading"
|
||||
:disabled="!productId"
|
||||
|
|
@ -95,10 +91,10 @@ watch(
|
|||
>
|
||||
<div class="py-[4px] flex w-full items-center justify-between">
|
||||
<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 }}
|
||||
</div>
|
||||
<div class="text-[12px] text-primary">
|
||||
<div class="text-[12px] text-muted-foreground">
|
||||
{{ device.deviceKey }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Select } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IotRuleSceneTriggerConditionParameterOperatorEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
} from '@vben/constants';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Select } from 'ant-design-vue';
|
||||
|
||||
/** 操作符选择器组件 */
|
||||
defineOptions({ name: 'OperatorSelector' });
|
||||
|
|
@ -252,7 +252,7 @@ watch(
|
|||
>
|
||||
<div class="py-[4px] flex w-full items-center justify-between">
|
||||
<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 }}
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -261,7 +261,7 @@ watch(
|
|||
{{ operator.symbol }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[12px] text-secondary">
|
||||
<div class="text-[12px] text-muted-foreground">
|
||||
{{ operator.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ onMounted(() => {
|
|||
:value="modelValue"
|
||||
@change="(value: any) => handleChange(value)"
|
||||
placeholder="请选择产品"
|
||||
filterable
|
||||
clearable
|
||||
show-search
|
||||
allow-clear
|
||||
class="w-full"
|
||||
:loading="productLoading"
|
||||
>
|
||||
|
|
@ -71,10 +71,10 @@ onMounted(() => {
|
|||
>
|
||||
<div class="py-[4px] flex w-full items-center justify-between">
|
||||
<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 }}
|
||||
</div>
|
||||
<div class="text-[12px] text-secondary">
|
||||
<div class="text-[12px] text-muted-foreground">
|
||||
{{ product.productKey }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,22 +4,21 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
|
||||
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 {
|
||||
getAccessModeLabel,
|
||||
getDataTypeName,
|
||||
getDataTypeTagColor,
|
||||
getEventTypeLabel,
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
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' });
|
||||
|
|
@ -58,197 +57,125 @@ interface PropertySelectorItem {
|
|||
|
||||
const localValue = useVModel(props, 'modelValue', emit);
|
||||
|
||||
const loading = ref(false); // 加载状态
|
||||
const propertyList = ref<ThingModelApi.Property[]>([]); // 属性列表
|
||||
const thingModelTSL = ref<null | ThingModelApi.ThingModel>(null); // 物模型TSL数据
|
||||
const loading = ref(false);
|
||||
const propertyList = ref<PropertySelectorItem[]>([]);
|
||||
|
||||
// 计算属性:属性分组
|
||||
/** 触发类型 → 物模型类型 + 分组标签 */
|
||||
const TRIGGER_TYPE_TO_GROUP: Record<
|
||||
number,
|
||||
{ label: string; modelType: number }
|
||||
> = {
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST]: {
|
||||
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||
modelType: IoTThingModelTypeEnum.PROPERTY,
|
||||
},
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST]: {
|
||||
label: THING_MODEL_GROUP_LABELS.EVENT,
|
||||
modelType: IoTThingModelTypeEnum.EVENT,
|
||||
},
|
||||
[IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE]: {
|
||||
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
||||
modelType: IoTThingModelTypeEnum.SERVICE,
|
||||
},
|
||||
};
|
||||
|
||||
/** 属性分组:按触发类型筛选属性 / 事件 / 服务 */
|
||||
const propertyGroups = computed(() => {
|
||||
const groups: { label: string; options: any[] }[] = [];
|
||||
|
||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
|
||||
groups.push({
|
||||
label: THING_MODEL_GROUP_LABELS.PROPERTY,
|
||||
options: propertyList.value.filter(
|
||||
(property: any) => property.type === IoTThingModelTypeEnum.PROPERTY,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
|
||||
groups.push({
|
||||
label: THING_MODEL_GROUP_LABELS.EVENT,
|
||||
options: propertyList.value.filter(
|
||||
(property: any) => property.type === IoTThingModelTypeEnum.EVENT,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
|
||||
groups.push({
|
||||
label: THING_MODEL_GROUP_LABELS.SERVICE,
|
||||
options: propertyList.value.filter(
|
||||
(property: any) => property.type === IoTThingModelTypeEnum.SERVICE,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return groups.filter((group) => group.options.length > 0);
|
||||
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(() => {
|
||||
return propertyList.value.find((p) => p.identifier === localValue.value);
|
||||
});
|
||||
/** 当前选中的属性 */
|
||||
const selectedProperty = computed(() =>
|
||||
propertyList.value.find((p) => p.identifier === localValue.value),
|
||||
);
|
||||
|
||||
/**
|
||||
* 处理选择变化事件
|
||||
* @param value 选中的属性标识符
|
||||
*/
|
||||
/** 处理选择变化事件 */
|
||||
function handleChange(value: any) {
|
||||
const property = propertyList.value.find((p) => p.identifier === value);
|
||||
if (property) {
|
||||
emit('change', {
|
||||
type: property.dataType,
|
||||
config: property,
|
||||
});
|
||||
emit('change', { type: property.dataType, config: property });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取物模型TSL数据
|
||||
*/
|
||||
async function getThingModelTSL() {
|
||||
/** 获取物模型 TSL 数据 */
|
||||
async function loadThingModelTSL() {
|
||||
if (!props.productId) {
|
||||
thingModelTSL.value = null;
|
||||
propertyList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const tslData = await getThingModelListByProductId(props.productId);
|
||||
|
||||
if (tslData) {
|
||||
thingModelTSL.value = tslData;
|
||||
parseThingModelData();
|
||||
} else {
|
||||
console.error('获取物模型TSL失败: 返回数据为空');
|
||||
propertyList.value = [];
|
||||
}
|
||||
const tsl = await getThingModelTSL(props.productId);
|
||||
propertyList.value = parseThingModelData(tsl);
|
||||
} catch (error) {
|
||||
console.error('获取物模型TSL失败:', error);
|
||||
console.error('获取物模型 TSL 失败:', error);
|
||||
propertyList.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析物模型 TSL 数据 */
|
||||
function parseThingModelData() {
|
||||
const tsl = thingModelTSL.value;
|
||||
const properties: PropertySelectorItem[] = [];
|
||||
|
||||
if (!tsl) {
|
||||
propertyList.value = properties;
|
||||
return;
|
||||
}
|
||||
// 解析属性
|
||||
// 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,
|
||||
dataType: prop.dataType,
|
||||
type: IoTThingModelTypeEnum.PROPERTY,
|
||||
accessMode: prop.accessMode,
|
||||
required: prop.required,
|
||||
unit: getPropertyUnit(prop),
|
||||
range: getPropertyRange(prop),
|
||||
property: prop,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 解析事件
|
||||
// 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,
|
||||
dataType: 'struct',
|
||||
type: IoTThingModelTypeEnum.EVENT,
|
||||
eventType: event.type,
|
||||
required: event.required,
|
||||
outputParams: event.outputParams,
|
||||
event,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 解析服务
|
||||
if (tsl.services && Array.isArray(tsl.services)) {
|
||||
tsl.services.forEach((service) => {
|
||||
properties.push({
|
||||
identifier: service.identifier,
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
dataType: 'struct',
|
||||
type: IoTThingModelTypeEnum.SERVICE,
|
||||
callType: service.callType,
|
||||
required: service.required,
|
||||
inputParams: service.inputParams,
|
||||
outputParams: service.outputParams,
|
||||
service,
|
||||
});
|
||||
});
|
||||
}
|
||||
propertyList.value = properties;
|
||||
/** 把 TSL 树展平为 PropertySelectorItem[] */
|
||||
function parseThingModelData(
|
||||
tsl?: null | ThingModelApi.ThingModelTSL,
|
||||
): PropertySelectorItem[] {
|
||||
if (!tsl) return [];
|
||||
const properties = (tsl.properties ?? []).map<PropertySelectorItem>(
|
||||
(prop) => ({
|
||||
identifier: prop.identifier!,
|
||||
name: prop.name!,
|
||||
description: prop.description,
|
||||
dataType: prop.dataType!,
|
||||
type: IoTThingModelTypeEnum.PROPERTY,
|
||||
accessMode: prop.accessMode,
|
||||
required: prop.required,
|
||||
unit: prop.dataSpecs?.unit,
|
||||
range: getPropertyRange(prop),
|
||||
property: prop,
|
||||
}),
|
||||
);
|
||||
const events = (tsl.events ?? []).map<PropertySelectorItem>((event) => ({
|
||||
identifier: event.identifier!,
|
||||
name: event.name!,
|
||||
description: event.description,
|
||||
dataType: 'struct',
|
||||
type: IoTThingModelTypeEnum.EVENT,
|
||||
eventType: event.type,
|
||||
required: event.required,
|
||||
outputParams: event.outputParams,
|
||||
event,
|
||||
}));
|
||||
const services = (tsl.services ?? []).map<PropertySelectorItem>((service) => ({
|
||||
identifier: service.identifier!,
|
||||
name: service.name!,
|
||||
description: service.description,
|
||||
dataType: 'struct',
|
||||
type: IoTThingModelTypeEnum.SERVICE,
|
||||
callType: service.callType,
|
||||
required: service.required,
|
||||
inputParams: service.inputParams,
|
||||
outputParams: service.outputParams,
|
||||
service,
|
||||
}));
|
||||
return [...properties, ...events, ...services];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性单位
|
||||
* @param property 属性对象
|
||||
* @returns 属性单位
|
||||
*/
|
||||
function getPropertyUnit(property: any) {
|
||||
if (!property) return undefined;
|
||||
|
||||
// 数值型数据的单位
|
||||
if (property.dataSpecs && property.dataSpecs.unit) {
|
||||
return property.dataSpecs.unit;
|
||||
/** 获取属性取值范围:数值型给 min~max;枚举 / 布尔给选项列表 */
|
||||
function getPropertyRange(property: ThingModelApi.Property): string | undefined {
|
||||
const specs = property.dataSpecs;
|
||||
if (specs && specs.min !== undefined && specs.max !== undefined) {
|
||||
return `${specs.min}~${specs.max}`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性范围描述
|
||||
* @param property 属性对象
|
||||
* @returns 属性范围描述
|
||||
*/
|
||||
function getPropertyRange(property: any) {
|
||||
if (!property) return undefined;
|
||||
|
||||
// 数值型数据的范围
|
||||
if (property.dataSpecs) {
|
||||
const specs = property.dataSpecs;
|
||||
if (specs.min !== undefined && specs.max !== undefined) {
|
||||
return `${specs.min}~${specs.max}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 枚举型和布尔型数据的选项
|
||||
if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
|
||||
if (property.dataSpecsList?.length) {
|
||||
return property.dataSpecsList
|
||||
.map((item: any) => `${item.name}(${item.value})`)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +183,7 @@ function getPropertyRange(property: any) {
|
|||
watch(
|
||||
() => props.productId,
|
||||
() => {
|
||||
getThingModelTSL();
|
||||
loadThingModelTSL();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
|
@ -275,13 +202,13 @@ watch(
|
|||
<Select
|
||||
v-model:value="localValue"
|
||||
placeholder="请选择监控项"
|
||||
filterable
|
||||
clearable
|
||||
show-search
|
||||
allow-clear
|
||||
@change="handleChange"
|
||||
class="!w-[150px]"
|
||||
:loading="loading"
|
||||
>
|
||||
<Select.OptionGroup
|
||||
<Select.OptGroup
|
||||
v-for="group in propertyGroups"
|
||||
:key="group.label"
|
||||
:label="group.label"
|
||||
|
|
@ -293,60 +220,45 @@ watch(
|
|||
:value="property.identifier"
|
||||
>
|
||||
<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 }}
|
||||
</span>
|
||||
<Tag
|
||||
:color="getDataTypeTagColor(property.dataType)"
|
||||
class="ml-[8px] flex-shrink-0"
|
||||
>
|
||||
<Tag class="ml-[8px] flex-shrink-0">
|
||||
{{ property.identifier }}
|
||||
</Tag>
|
||||
</div>
|
||||
</Select.Option>
|
||||
</Select.OptionGroup>
|
||||
</Select.OptGroup>
|
||||
</Select>
|
||||
|
||||
<!-- 属性详情弹出层 -->
|
||||
<Popover
|
||||
v-if="selectedProperty"
|
||||
placement="rightTop"
|
||||
:width="350"
|
||||
:overlay-style="{ width: '350px' }"
|
||||
trigger="click"
|
||||
:show-arrow="true"
|
||||
:offset="8"
|
||||
popper-class="property-detail-popover"
|
||||
:arrow="true"
|
||||
overlay-class-name="property-detail-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<Button
|
||||
type="link"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="flex-shrink-0"
|
||||
title="查看属性详情"
|
||||
>
|
||||
<IconifyIcon icon="ep:info-filled" />
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<!-- 弹出层内容 -->
|
||||
<div class="property-detail-content">
|
||||
<template #content>
|
||||
<!-- 弹出层内容 -->
|
||||
<div class="property-detail-content">
|
||||
<div class="gap-[8px] mb-[12px] flex items-center">
|
||||
<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 }}
|
||||
</span>
|
||||
<Tag :color="getDataTypeTagColor(selectedProperty.dataType)">
|
||||
<Tag>
|
||||
{{ getDataTypeName(selectedProperty.dataType) }}
|
||||
</Tag>
|
||||
</div>
|
||||
|
||||
<div class="space-y-[8px] ml-[24px]">
|
||||
<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 class="text-[12px] flex-1 text-primary">
|
||||
<span class="text-[12px] flex-1 text-foreground">
|
||||
{{ selectedProperty.identifier }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -355,28 +267,28 @@ watch(
|
|||
v-if="selectedProperty.description"
|
||||
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 class="text-[12px] flex-1 text-primary">
|
||||
<span class="text-[12px] flex-1 text-foreground">
|
||||
{{ selectedProperty.description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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 class="text-[12px] flex-1 text-primary">
|
||||
<span class="text-[12px] flex-1 text-foreground">
|
||||
{{ selectedProperty.unit }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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 class="text-[12px] flex-1 text-primary">
|
||||
<span class="text-[12px] flex-1 text-foreground">
|
||||
{{ selectedProperty.range }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -389,10 +301,10 @@ watch(
|
|||
"
|
||||
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 class="text-[12px] flex-1 text-primary">
|
||||
<span class="text-[12px] flex-1 text-foreground">
|
||||
{{ getAccessModeLabel(selectedProperty.accessMode) }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -404,10 +316,10 @@ watch(
|
|||
"
|
||||
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 class="text-[12px] flex-1 text-primary">
|
||||
<span class="text-[12px] flex-1 text-foreground">
|
||||
{{ getEventTypeLabel(selectedProperty.eventType) }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -419,15 +331,25 @@ watch(
|
|||
"
|
||||
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 class="text-[12px] flex-1 text-primary">
|
||||
<span class="text-[12px] flex-1 text-foreground">
|
||||
{{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Button
|
||||
type="link"
|
||||
shape="circle"
|
||||
size="small"
|
||||
class="flex-shrink-0"
|
||||
title="查看属性详情"
|
||||
>
|
||||
<IconifyIcon icon="ep:info-filled" />
|
||||
</Button>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@ import type { RuleSceneApi } from '#/api/iot/rule/scene';
|
|||
import { ref } from 'vue';
|
||||
|
||||
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 { formatDateTime } from '@vben/utils';
|
||||
|
||||
|
|
@ -17,14 +23,9 @@ import {
|
|||
getSceneRulePage,
|
||||
updateSceneRuleStatus,
|
||||
} from '#/api/iot/rule/scene';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { $t } from '#/locales';
|
||||
import { CronUtils } from '#/utils/cron';
|
||||
import {
|
||||
getActionTypeLabel,
|
||||
getTriggerTypeLabel,
|
||||
IotRuleSceneActionTypeEnum,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import Form from './modules/form.vue';
|
||||
|
|
@ -105,109 +106,50 @@ function hasTimerTrigger(row: RuleSceneApi.SceneRule): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
/** 触发器列表项(用于列内多 tag 渲染) */
|
||||
interface TriggerCellItem {
|
||||
color: string;
|
||||
label: string;
|
||||
meta?: string;
|
||||
/** 触发条件摘要文本(拼接所有触发器) */
|
||||
function getTriggerSummary(row: RuleSceneApi.SceneRule): string {
|
||||
if (!row.triggers?.length) return '无触发器';
|
||||
return row.triggers
|
||||
.map((trigger) => {
|
||||
const type = trigger.type ?? 0;
|
||||
let description = getTriggerTypeLabel(type);
|
||||
if (
|
||||
(type === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
||||
type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||
type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) &&
|
||||
trigger.identifier
|
||||
) {
|
||||
description += ` (${trigger.identifier})`;
|
||||
} else if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||
description = `${getTriggerTypeLabel(type)} (${CronUtils.format(trigger.cronExpression || '')})`;
|
||||
}
|
||||
if (trigger.deviceId) {
|
||||
description += ` [设备 ID: ${trigger.deviceId}]`;
|
||||
} else if (trigger.productId) {
|
||||
description += ` [产品 ID: ${trigger.productId}]`;
|
||||
}
|
||||
return description;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
/** 动作列表项 */
|
||||
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;
|
||||
let label = getTriggerTypeLabel(type);
|
||||
if (
|
||||
(type === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
|
||||
type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
|
||||
type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) &&
|
||||
trigger.identifier
|
||||
) {
|
||||
label += ` · ${trigger.identifier}`;
|
||||
} else if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
|
||||
label += ` · ${CronUtils.format(trigger.cronExpression || '')}`;
|
||||
}
|
||||
const meta = trigger.deviceId
|
||||
? `设备 #${trigger.deviceId}`
|
||||
: (trigger.productId
|
||||
? `产品 #${trigger.productId}`
|
||||
: '');
|
||||
return { color: colorOfTrigger(type), label, meta };
|
||||
});
|
||||
}
|
||||
|
||||
/** 动作列:每个动作一项 */
|
||||
function getActionCellItems(row: RuleSceneApi.SceneRule): ActionCellItem[] {
|
||||
if (!row.actions?.length) {
|
||||
return [];
|
||||
}
|
||||
return row.actions.map((action) => {
|
||||
const type = action.type ?? 0;
|
||||
const label = getActionTypeLabel(type);
|
||||
const meta = action.deviceId
|
||||
? `设备 #${action.deviceId}`
|
||||
: action.productId
|
||||
? `产品 #${action.productId}`
|
||||
: action.alertConfigId
|
||||
? `告警 #${action.alertConfigId}`
|
||||
: '';
|
||||
return { color: colorOfAction(type), label, meta };
|
||||
});
|
||||
/** 执行动作摘要文本(拼接所有动作) */
|
||||
function getActionSummary(row: RuleSceneApi.SceneRule): string {
|
||||
if (!row.actions?.length) 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}]`;
|
||||
}
|
||||
if (action.alertConfigId) {
|
||||
description += ` [告警配置 ID: ${action.alertConfigId}]`;
|
||||
}
|
||||
return description;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
/** 取定时触发器的 CRON 频率描述 */
|
||||
|
|
@ -304,7 +246,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<div class="text-xl font-semibold">
|
||||
{{ statistics.total }}
|
||||
</div>
|
||||
<div class="text-xs text-secondary">总规则数</div>
|
||||
<div class="text-xs text-muted-foreground">总规则数</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -321,7 +263,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<div class="text-xl font-semibold">
|
||||
{{ statistics.enabled }}
|
||||
</div>
|
||||
<div class="text-xs text-secondary">启用规则</div>
|
||||
<div class="text-xs text-muted-foreground">启用规则</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -338,7 +280,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<div class="text-xl font-semibold">
|
||||
{{ statistics.disabled }}
|
||||
</div>
|
||||
<div class="text-xs text-secondary">禁用规则</div>
|
||||
<div class="text-xs text-muted-foreground">禁用规则</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -355,7 +297,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
<div class="text-xl font-semibold">
|
||||
{{ statistics.timerRules }}
|
||||
</div>
|
||||
<div class="text-xs text-secondary">定时规则</div>
|
||||
<div class="text-xs text-muted-foreground">定时规则</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -375,65 +317,51 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||
]"
|
||||
/>
|
||||
</template>
|
||||
<!-- 规则名称列:名称 + 状态 + 描述 -->
|
||||
<!-- 规则名称列:名称 + 状态 tag inline + 描述 -->
|
||||
<template #name="{ row }">
|
||||
<div class="gap-2 flex items-center">
|
||||
<span class="font-medium">{{ row.name }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-foreground">{{ row.name }}</span>
|
||||
<DictTag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
|
||||
</div>
|
||||
<Tooltip
|
||||
v-if="row.description"
|
||||
:title="row.description"
|
||||
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 }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<!-- 触发条件列:按触发器各显示一项 -->
|
||||
<!-- 触发条件列:单 tag 汇总 + 定时触发额外信息 -->
|
||||
<template #triggers="{ row }">
|
||||
<div v-if="getTriggerCellItems(row).length > 0" class="flex flex-col gap-1">
|
||||
<div
|
||||
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>
|
||||
<div class="space-y-1">
|
||||
<Tag color="processing" class="m-0">{{ getTriggerSummary(row) }}</Tag>
|
||||
<Tooltip
|
||||
v-if="hasTimerTrigger(row)"
|
||||
:title="getCronExpression(row)"
|
||||
placement="top"
|
||||
>
|
||||
<span class="text-xs text-secondary">
|
||||
<div class="text-xs text-muted-foreground">
|
||||
<IconifyIcon icon="lucide:clock" class="mr-1 inline" />
|
||||
{{ getCronFrequency(row) }}
|
||||
<template v-if="getNextExecutionTime(row)">
|
||||
· 下次 {{ formatDateTime(getNextExecutionTime(row) as Date) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span v-else class="text-xs text-secondary">无触发器</span>
|
||||
</template>
|
||||
<!-- 执行动作列:按动作各显示一项 -->
|
||||
<!-- 执行动作列:单 tag 汇总 -->
|
||||
<template #actionsCol="{ row }">
|
||||
<div v-if="getActionCellItems(row).length > 0" class="flex flex-col gap-1">
|
||||
<div
|
||||
v-for="(item, i) in getActionCellItems(row)"
|
||||
:key="`action-${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>
|
||||
</div>
|
||||
<span v-else class="text-xs text-secondary">无动作</span>
|
||||
<Tag color="success" class="m-0">{{ getActionSummary(row) }}</Tag>
|
||||
</template>
|
||||
<!-- 最近触发列 -->
|
||||
<template #lastTriggeredTime="{ row }">
|
||||
<span v-if="row.lastTriggeredTime">
|
||||
{{ formatDateTime(row.lastTriggeredTime) }}
|
||||
</span>
|
||||
<span v-else class="text-muted-foreground">未触发</span>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import type { IotSceneRule, RuleSceneApi } from '#/api/iot/rule/scene';
|
|||
import { computed, nextTick, reactive, ref } from 'vue';
|
||||
|
||||
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';
|
||||
|
||||
|
|
@ -14,11 +19,6 @@ import {
|
|||
updateSceneRule,
|
||||
} from '#/api/iot/rule/scene';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
IotRuleSceneActionTypeEnum,
|
||||
IotRuleSceneTriggerTypeEnum,
|
||||
isDeviceTrigger,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ActionSection from '../form/sections/action-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 { ThingModelApi } from '#/api/iot/thingmodel';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { DICT_TYPE, getDataTypeOptionsLabel } from '@vben/constants';
|
||||
import { getDictOptions } from '@vben/hooks';
|
||||
|
||||
import { getDataTypeOptionsLabel } from '#/views/iot/utils/constants';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchema(): VbenFormSchema[] {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
import { computed, inject } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { deleteThingModel, getThingModelPage } from '#/api/iot/thingmodel';
|
||||
import { $t } from '#/locales';
|
||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
import { DataDefinition } from './modules/components';
|
||||
|
|
|
|||
|
|
@ -3,32 +3,31 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
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>([
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
]);
|
||||
const PLACEHOLDER_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
IoTDataSpecsDataTypeEnum.DATE,
|
||||
IoTDataSpecsDataTypeEnum.STRUCT,
|
||||
]);
|
||||
const LIST_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.BOOL,
|
||||
IoTDataSpecsDataTypeEnum.ENUM,
|
||||
]);
|
||||
|
||||
const props = defineProps<{ data: ThingModelApi.ThingModel }>();
|
||||
|
||||
const formattedDataSpecsList = computed(() => {
|
||||
if (!props.data.property?.dataSpecsList?.length) {
|
||||
return '';
|
||||
|
|
|
|||
|
|
@ -2,29 +2,30 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Input, Radio } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelStructDataSpecs from './struct.vue';
|
||||
|
||||
const props = defineProps<{ modelValue: any }>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
/** 数组元素禁止选择的类型 */
|
||||
const EXCLUDED_CHILD_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.ENUM,
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
IoTDataSpecsDataTypeEnum.DATE,
|
||||
IoTDataSpecsDataTypeEnum.ENUM,
|
||||
]);
|
||||
const childDataTypeOptions = getDataTypeOptions().filter(
|
||||
(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>;
|
||||
|
||||
/** 元素类型切到 struct 时,初始化 dataSpecsList 占位 */
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import type { Ref } from 'vue';
|
|||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelProperty from '../property.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@ import type { Ref } from 'vue';
|
|||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import {
|
||||
IoTThingModelEventTypeEnum,
|
||||
IoTThingModelParamDirectionEnum,
|
||||
} from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Radio } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IoTThingModelEventTypeEnum,
|
||||
IoTThingModelParamDirectionEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
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 { 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 { $t } from '@vben/locales';
|
||||
import { cloneDeep, isEmpty } from '@vben/utils';
|
||||
|
|
@ -20,11 +25,6 @@ import {
|
|||
ThingModelFormRules,
|
||||
updateThingModel,
|
||||
} from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IOT_PROVIDE_KEY,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelEvent from './event.vue';
|
||||
import ThingModelProperty from './property.vue';
|
||||
|
|
@ -51,12 +51,14 @@ const [Modal, modalApi] = useVbenModal({
|
|||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = cloneDeep(formData.value);
|
||||
data.productId = product!.value.id;
|
||||
data.productKey = product!.value.productKey;
|
||||
fillExtraAttributes(data);
|
||||
try {
|
||||
const data = cloneDeep(formData.value);
|
||||
data.productId = product!.value.id;
|
||||
data.productKey = product!.value.productKey;
|
||||
fillExtraAttributes(data);
|
||||
await (data.id ? updateThingModel(data) : createThingModel(data));
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
|
|
@ -68,9 +70,10 @@ const [Modal, modalApi] = useVbenModal({
|
|||
if (!isOpen) {
|
||||
return;
|
||||
}
|
||||
// 每次打开都先重置到空白,避免上一次的状态残留
|
||||
// 每次打开都重置;避免上一次的状态残留
|
||||
formData.value = buildEmptyFormData();
|
||||
formRef.value?.clearValidate?.();
|
||||
// 加载数据
|
||||
const data = modalApi.getData<{ id?: number }>();
|
||||
if (!data?.id) {
|
||||
return;
|
||||
|
|
@ -78,6 +81,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||
modalApi.lock();
|
||||
try {
|
||||
const result = await getThingModel(data.id);
|
||||
// 设置到 values
|
||||
formData.value = normalizeFormData(result);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
|
|
@ -136,14 +140,31 @@ function normalizeFormData(result: ThingModelApi.ThingModel): ThingModelApi.Thin
|
|||
|
||||
/** 按功能类型将子表单数据回写到顶层,并清理无关分支 */
|
||||
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);
|
||||
data.dataType = data.property.dataType;
|
||||
data.property.identifier = data.identifier;
|
||||
data.property.name = data.name;
|
||||
delete data.service;
|
||||
delete data.event;
|
||||
} else if (data.type === IoTThingModelTypeEnum.SERVICE) {
|
||||
|
||||
break;
|
||||
}
|
||||
case IoTThingModelTypeEnum.SERVICE: {
|
||||
removeDataSpecs(data.service);
|
||||
data.dataType = data.service.dataType;
|
||||
data.service.identifier = data.identifier;
|
||||
|
|
@ -156,16 +177,10 @@ function fillExtraAttributes(data: any) {
|
|||
}
|
||||
delete data.property;
|
||||
delete data.event;
|
||||
} else if (data.type === 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;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import type { Ref } from 'vue';
|
|||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Button, Divider, Form, Input } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import ThingModelProperty from './property.vue';
|
||||
|
||||
|
|
@ -31,6 +31,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||
if (!thingModelParams.value) {
|
||||
thingModelParams.value = [];
|
||||
}
|
||||
// 组装表单
|
||||
const data = formData.value;
|
||||
const item = {
|
||||
identifier: data.identifier,
|
||||
|
|
@ -48,7 +49,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||
? undefined
|
||||
: data.property.dataSpecsList,
|
||||
};
|
||||
// 按 identifier 去重,存在则更新,否则追加
|
||||
// 按 identifier 去重;存在则更新,否则追加
|
||||
const existingIndex = thingModelParams.value.findIndex(
|
||||
(spec) => spec.identifier === data.identifier,
|
||||
);
|
||||
|
|
@ -57,6 +58,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||
} else {
|
||||
thingModelParams.value[existingIndex] = item;
|
||||
}
|
||||
// 关闭
|
||||
await modalApi.close();
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
|
|
@ -65,10 +67,12 @@ const [Modal, modalApi] = useVbenModal({
|
|||
}
|
||||
formData.value = buildEmptyFormData();
|
||||
paramFormRef.value?.clearValidate?.();
|
||||
// 加载数据
|
||||
const data = modalApi.getData<any>();
|
||||
if (isEmpty(data)) {
|
||||
return;
|
||||
}
|
||||
// 设置到 values
|
||||
formData.value = {
|
||||
identifier: data.identifier ?? '',
|
||||
name: data.name ?? '',
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
|
||||
import { computed, watch } from 'vue';
|
||||
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IoTThingModelAccessModeEnum,
|
||||
} from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Input, Radio, Select } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules, validateBoolName } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
getDataTypeOptions,
|
||||
IoTDataSpecsDataTypeEnum,
|
||||
IoTThingModelAccessModeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
import {
|
||||
ThingModelArrayDataSpecs,
|
||||
|
|
@ -25,6 +25,12 @@ import {
|
|||
ThingModelStructDataSpecs,
|
||||
} from './data-specs';
|
||||
|
||||
const props = defineProps<{
|
||||
isParams?: boolean;
|
||||
isStructDataSpecs?: boolean;
|
||||
modelValue: any;
|
||||
}>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
/** 嵌套在结构体里时,禁止再选数组 / 结构体(最多支持两层嵌套) */
|
||||
const NESTED_EXCLUDED_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.ARRAY,
|
||||
|
|
@ -35,17 +41,11 @@ const STRUCT_CHILD_OPTIONS = getDataTypeOptions().filter(
|
|||
);
|
||||
/** 数值型数据类型集合 */
|
||||
const NUMERIC_TYPES = new Set<string>([
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
IoTDataSpecsDataTypeEnum.DOUBLE,
|
||||
IoTDataSpecsDataTypeEnum.FLOAT,
|
||||
IoTDataSpecsDataTypeEnum.INT,
|
||||
]);
|
||||
|
||||
const props = defineProps<{
|
||||
isParams?: boolean;
|
||||
isStructDataSpecs?: boolean;
|
||||
modelValue: any;
|
||||
}>();
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
const property = useVModel(
|
||||
props,
|
||||
'modelValue',
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@ import type { Ref } from 'vue';
|
|||
|
||||
import { watch } from 'vue';
|
||||
|
||||
import {
|
||||
IoTThingModelParamDirectionEnum,
|
||||
IoTThingModelServiceCallTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Form, Radio } from 'ant-design-vue';
|
||||
|
||||
import { ThingModelFormRules } from '#/api/iot/thingmodel';
|
||||
import {
|
||||
IoTThingModelParamDirectionEnum,
|
||||
IoTThingModelServiceCallTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
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 { useVbenModal } from '@vben/common-ui';
|
||||
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||
|
||||
import { Radio, Textarea } from 'ant-design-vue';
|
||||
|
||||
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);
|
||||
|
||||
|
|
@ -25,7 +25,9 @@ const [Modal, modalApi] = useVbenModal({
|
|||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
// 加载数据
|
||||
thingModelTSL.value = await getThingModelTSL(product?.value?.id || 0);
|
||||
// 设置到 values
|
||||
tslString.value = JSON.stringify(thingModelTSL.value, null, 2);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
|
|
@ -38,12 +40,12 @@ const formattedTSL = computed(() =>
|
|||
JSON.stringify(thingModelTSL.value, null, 2),
|
||||
);
|
||||
|
||||
/** 编辑器内容变化时,同步到数据对象 */
|
||||
/** 编辑器内容变化时,同步到数据对象;编辑过程中 JSON 可能是中间态,解析失败保留原值 */
|
||||
watch(tslString, (newValue) => {
|
||||
try {
|
||||
thingModelTSL.value = JSON.parse(newValue);
|
||||
} catch {
|
||||
// JSON 解析失败时保持原值
|
||||
// 中间态忽略
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
@ -57,14 +59,12 @@ watch(tslString, (newValue) => {
|
|||
<Radio.Button value="editor">编辑器视图</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<!-- 代码视图:只读展示 -->
|
||||
<!-- 代码视图:只读展示(pre / code 必须紧贴,避免显示出空白) -->
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<pre
|
||||
class="m-0 whitespace-pre-wrap break-words font-mono text-[13px] leading-normal"
|
||||
><code>{{ formattedTSL }}</code></pre>
|
||||
<pre class="m-0 whitespace-pre-wrap break-words font-mono text-[13px] leading-normal"><code>{{ formattedTSL }}</code></pre>
|
||||
</div>
|
||||
<!-- 编辑器视图:可编辑 -->
|
||||
<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_BAIDU_MAP_KEY=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN
|
||||
|
||||
# GoView域名
|
||||
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: '产品名称',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) =>
|
||||
productList.find((p) => p.id === cellValue)?.name || '-',
|
||||
productList.find((product) => product.id === cellValue)?.name || '-',
|
||||
},
|
||||
{
|
||||
field: 'deviceId',
|
||||
title: '设备名称',
|
||||
minWidth: 120,
|
||||
formatter: ({ cellValue }) =>
|
||||
deviceList.find((d) => d.id === cellValue)?.deviceName || '-',
|
||||
deviceList.find((device) => device.id === cellValue)?.deviceName ||
|
||||
'-',
|
||||
},
|
||||
{
|
||||
field: 'deviceMessage',
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export function useAdvancedFormSchema(): VbenFormSchema[] {
|
|||
api: getSimpleDeviceGroupList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
mode: 'multiple',
|
||||
multiple: true,
|
||||
placeholder: '请选择设备分组',
|
||||
},
|
||||
},
|
||||
|
|
@ -168,7 +168,7 @@ export function useGroupFormSchema(): VbenFormSchema[] {
|
|||
api: getSimpleDeviceGroupList,
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
mode: 'multiple',
|
||||
multiple: true,
|
||||
placeholder: '请选择设备分组',
|
||||
},
|
||||
rules: 'required',
|
||||
|
|
|
|||
|
|
@ -30,17 +30,13 @@ const props = defineProps<{
|
|||
deviceId: number;
|
||||
}>();
|
||||
|
||||
/** 查询参数 */
|
||||
const queryParams = reactive({
|
||||
method: undefined,
|
||||
upstream: undefined,
|
||||
});
|
||||
}); // 查询参数
|
||||
|
||||
// TODO @AI:变量的注释,写在 // 尾注释。别的模块也看看。
|
||||
/** 自动刷新开关 */
|
||||
const autoRefresh = ref(false);
|
||||
/** 自动刷新定时器 */
|
||||
let autoRefreshTimer: any = null;
|
||||
const autoRefresh = ref(false); // 自动刷新开关
|
||||
let autoRefreshTimer: any = null; // 自动刷新定时器
|
||||
|
||||
/** 消息方法选项 */
|
||||
const methodOptions = computed(() => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import type { IotDeviceModbusConfigApi } from '#/api/iot/device/modbus/config';
|
|||
import { computed, ref } from 'vue';
|
||||
|
||||
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 { ElMessage } from 'element-plus';
|
||||
|
|
@ -14,10 +19,6 @@ import { useVbenForm, z } from '#/adapter/form';
|
|||
import { saveModbusConfig } from '#/api/iot/device/modbus/config';
|
||||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
ModbusFrameFormatEnum,
|
||||
ModbusModeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { DescriptionItemSchema } from '#/components/description';
|
|||
import { computed, h, onMounted, ref } from 'vue';
|
||||
|
||||
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';
|
||||
|
||||
|
|
@ -25,7 +25,6 @@ import {
|
|||
import { ProtocolTypeEnum } from '#/api/iot/product/product';
|
||||
import { useDescription } from '#/components/description';
|
||||
import { DictTag } from '#/components/dict-tag';
|
||||
import { ModbusFunctionCodeOptions } from '#/views/iot/utils/constants';
|
||||
|
||||
import DeviceModbusConfigForm from './modbus-config-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 { 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 { ElMessage } from 'element-plus';
|
||||
|
|
@ -19,12 +26,6 @@ import {
|
|||
updateModbusPoint,
|
||||
} from '#/api/iot/device/modbus/point';
|
||||
import { $t } from '#/locales';
|
||||
import {
|
||||
getByteOrderOptions,
|
||||
IoTThingModelTypeEnum,
|
||||
ModbusFunctionCodeOptions,
|
||||
ModbusRawDataTypeOptions,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ContentWrap } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import {
|
||||
DeviceStateEnum,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
|
|
@ -24,10 +28,6 @@ import {
|
|||
} from 'element-plus';
|
||||
|
||||
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 DeviceDetailsMessage from './message.vue';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
|
|
@ -20,10 +24,6 @@ import {
|
|||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import type { IotDeviceApi } from '#/api/iot/device/device';
|
|||
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||
|
||||
import { IoTDataSpecsDataTypeEnum } from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||
import { formatDate, formatDateTime } from '@vben/utils';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
|
|
@ -26,13 +27,11 @@ import {
|
|||
|
||||
import { getHistoryDevicePropertyList } from '#/api/iot/device/device';
|
||||
import ShortcutDateRangePicker from '#/components/shortcut-date-range-picker/shortcut-date-range-picker.vue';
|
||||
import { IoTDataSpecsDataTypeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
defineProps<{ deviceId: number }>();
|
||||
|
||||
const dialogVisible = ref(false); // 弹窗的是否展示
|
||||
const loading = ref(false);
|
||||
const exporting = ref(false);
|
||||
const viewMode = ref<'chart' | 'list'>('chart'); // 视图模式状态
|
||||
const list = ref<IotDeviceApi.DevicePropertyDetail[]>([]); // 列表的数据
|
||||
const total = ref(0); // 总数据量
|
||||
|
|
@ -297,54 +296,6 @@ function handleRefresh() {
|
|||
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() {
|
||||
dialogVisible.value = false;
|
||||
|
|
@ -394,16 +345,6 @@ defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
|||
刷新
|
||||
</ElButton>
|
||||
|
||||
<!-- 导出按钮 -->
|
||||
<ElButton
|
||||
:disabled="list.length === 0"
|
||||
:loading="exporting"
|
||||
@click="handleExport"
|
||||
>
|
||||
<IconifyIcon icon="ant-design:export-outlined" class="mr-1" />
|
||||
导出
|
||||
</ElButton>
|
||||
|
||||
<!-- 视图切换 -->
|
||||
<ElButtonGroup class="ml-auto">
|
||||
<ElButton
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import type { ThingModelApi } from '#/api/iot/thingmodel';
|
|||
import { computed, onMounted, reactive, watch } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IotDeviceMessageMethodEnum } from '@vben/constants';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IotDeviceMessageMethodEnum,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '@vben/constants';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
|
|
@ -20,10 +24,6 @@ import {
|
|||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getDeviceMessagePairPage } from '#/api/iot/device/device';
|
||||
import {
|
||||
getThingModelServiceCallTypeLabel,
|
||||
IoTThingModelTypeEnum,
|
||||
} from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
deviceId: number;
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="device-card-view">
|
||||
<div>
|
||||
<!-- 设备卡片列表 -->
|
||||
<div v-loading="loading" class="min-h-96">
|
||||
<ElRow v-if="list.length > 0" :gutter="16">
|
||||
|
|
@ -119,29 +119,38 @@ onMounted(() => {
|
|||
class="mb-4"
|
||||
>
|
||||
<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="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" />
|
||||
</div>
|
||||
<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>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_DEVICE_STATE"
|
||||
:value="item.state"
|
||||
class="status-tag"
|
||||
class="text-xs"
|
||||
/>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="mb-3 flex items-start">
|
||||
<div class="info-list flex-1">
|
||||
<div class="info-item">
|
||||
<span class="info-label">所属产品</span>
|
||||
<div class="flex-1">
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
所属产品
|
||||
</span>
|
||||
<a
|
||||
class="info-value cursor-pointer text-primary"
|
||||
class="cursor-pointer truncate font-medium text-primary"
|
||||
@click="
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
|
|
@ -152,25 +161,33 @@ onMounted(() => {
|
|||
{{ getProductName(item.productId) }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">设备类型</span>
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
设备类型
|
||||
</span>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="item.deviceType"
|
||||
class="info-tag m-0"
|
||||
class="m-0 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Deviceid</span>
|
||||
<div class="flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
Deviceid
|
||||
</span>
|
||||
<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 }}
|
||||
</span>
|
||||
</ElTooltip>
|
||||
</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
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
|
|
@ -185,11 +202,13 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<div
|
||||
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||
>
|
||||
<ElButton
|
||||
v-if="hasAccessByCodes(['iot:device:update'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||
|
|
@ -198,7 +217,7 @@ onMounted(() => {
|
|||
<ElButton
|
||||
v-if="hasAccessByCodes(['iot:device:query'])"
|
||||
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!)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||
|
|
@ -207,7 +226,7 @@ onMounted(() => {
|
|||
<ElButton
|
||||
v-if="hasAccessByCodes(['iot:device:message-query'])"
|
||||
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!)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:database" class="mr-1" />
|
||||
|
|
@ -222,7 +241,7 @@ onMounted(() => {
|
|||
<ElButton
|
||||
size="small"
|
||||
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" />
|
||||
</ElButton>
|
||||
|
|
@ -250,188 +269,3 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
</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(
|
||||
value: number,
|
||||
max: number,
|
||||
|
|
@ -129,9 +127,7 @@ export function getDeviceStateGaugeChartOptions(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设备数量饼图配置
|
||||
*/
|
||||
/** 设备数量饼图配置 */
|
||||
export function getDeviceCountPieChartOptions(
|
||||
data: Array<{ name: string; value: number }>,
|
||||
): any {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ async function loadData() {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO @AI:antd 这里,也要加下 /** */
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
|
|
|
|||
|
|
@ -21,8 +21,9 @@ const { renderEcharts } = useEcharts(deviceCountChartRef);
|
|||
|
||||
/** 是否有数据 */
|
||||
const hasData = computed(() => {
|
||||
// TODO @AI:即使只有一行,还是希望 return 可以换行;
|
||||
if (!props.statsData) return false;
|
||||
if (!props.statsData) {
|
||||
return false;
|
||||
}
|
||||
const categories = Object.entries(
|
||||
props.statsData.productCategoryDeviceCounts || {},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,16 @@
|
|||
<script lang="ts" setup>
|
||||
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 { DICT_TYPE } from '@vben/constants';
|
||||
import { DeviceStateEnum, DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElEmpty } from 'element-plus';
|
||||
|
||||
import { getDeviceLocationList } from '#/api/iot/device/device';
|
||||
import { loadBaiduMapSdk } from '#/components/map';
|
||||
import { DeviceStateEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
defineOptions({ name: 'DeviceMapCard' });
|
||||
|
||||
|
|
@ -152,6 +151,8 @@ async function init() {
|
|||
return;
|
||||
}
|
||||
await loadBaiduMapSdk();
|
||||
// 等待 v-show 容器渲染完成;SDK 缓存命中时上一行会同步 resolve,DOM 来不及切换
|
||||
await nextTick();
|
||||
initMap();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useDescription } from '#/components/description';
|
|||
|
||||
import { useDetailSchema } from '../../data';
|
||||
|
||||
/** IoT OTA 固件基本信息 */ // TODO @AI:是不是要去掉折行注释哈?
|
||||
defineProps<{
|
||||
firmware?: IoTOtaFirmwareApi.Firmware;
|
||||
loading?: boolean;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ import type { VbenFormSchema } from '#/adapter/form';
|
|||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
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 { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { IoTOtaTaskDeviceScopeEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
/** 任务详情的描述字段 */
|
||||
export function useDetailSchema(): DescriptionItemSchema[] {
|
||||
|
|
@ -77,7 +76,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||
label: '选择设备',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
mode: 'multiple',
|
||||
multiple: true,
|
||||
placeholder: '请选择设备',
|
||||
showSearch: true,
|
||||
filterOption: true,
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import type { IoTOtaTaskApi } from '#/api/iot/ota/task';
|
|||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IoTOtaTaskStatusEnum } from '@vben/constants';
|
||||
|
||||
import { ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { cancelOtaTask, getOtaTaskPage } from '#/api/iot/ota/task';
|
||||
import { $t } from '#/locales';
|
||||
import { IoTOtaTaskStatusEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import { useGridColumns } from '../data';
|
||||
import OtaTaskDetail from './detail.vue';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { DICT_TYPE, IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
import { ElCard, ElCol, ElRow } from 'element-plus';
|
||||
|
||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
const props = defineProps<{
|
||||
loading?: boolean;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import type { IoTOtaTaskRecordApi } from '#/api/iot/ota/task/record';
|
|||
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IoTOtaTaskRecordStatusEnum } from '@vben/constants';
|
||||
|
||||
import { ElCard, ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
|
|
@ -12,7 +14,6 @@ import {
|
|||
getOtaTaskRecordPage,
|
||||
} from '#/api/iot/ota/task/record';
|
||||
import { $t } from '#/locales';
|
||||
import { IoTOtaTaskRecordStatusEnum } from '#/views/iot/utils/constants';
|
||||
|
||||
import { useGridColumns } from '../data';
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import { onMounted, provide, ref } from 'vue';
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { IOT_PROVIDE_KEY } from '@vben/constants';
|
||||
|
||||
import { ElMessage, ElTabPane, ElTabs } from 'element-plus';
|
||||
|
||||
import { getDeviceCount } from '#/api/iot/device/device';
|
||||
import { getProduct } from '#/api/iot/product/product';
|
||||
import IoTProductThingModel from '#/views/iot/thingmodel/index.vue';
|
||||
import { IOT_PROVIDE_KEY } from '#/views/iot/utils/constants';
|
||||
|
||||
import ProductDetailsHeader from './modules/header.vue';
|
||||
import ProductDetailsInfo from './modules/info.vue';
|
||||
|
|
|
|||
|
|
@ -25,8 +25,9 @@ const showProductSecret = ref(false); // 是否显示产品密钥
|
|||
|
||||
/** 格式化日期 */
|
||||
function formatDate(date?: Date | string) {
|
||||
// TODO @AI:即使单行,也需要 return 换行;
|
||||
if (!date) return '-';
|
||||
if (!date) {
|
||||
return '-';
|
||||
}
|
||||
return new Date(date).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="product-card-view">
|
||||
<div>
|
||||
<!-- 产品卡片列表 -->
|
||||
<div v-loading="loading" class="min-h-96">
|
||||
<ElRow v-if="list.length > 0" :gutter="16">
|
||||
|
|
@ -103,52 +103,68 @@ onMounted(() => {
|
|||
class="mb-4"
|
||||
>
|
||||
<ElCard
|
||||
body-class="!p-4"
|
||||
class="product-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="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
|
||||
:icon="item.icon || 'lucide:box'"
|
||||
class="text-xl"
|
||||
/>
|
||||
</div>
|
||||
<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 class="mb-3 flex items-start">
|
||||
<div class="info-list flex-1">
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品分类</span>
|
||||
<span class="info-value text-primary">
|
||||
<div class="flex-1">
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
产品分类
|
||||
</span>
|
||||
<span class="truncate font-medium text-primary">
|
||||
{{ getCategoryName(item.categoryId) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品类型</span>
|
||||
<div class="mb-2 flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
产品类型
|
||||
</span>
|
||||
<DictTag
|
||||
:type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE"
|
||||
:value="item.deviceType"
|
||||
class="info-tag m-0"
|
||||
class="m-0 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">产品标识</span>
|
||||
<div class="flex items-center text-[13px]">
|
||||
<span class="mr-2 shrink-0 opacity-65 dark:text-white/65">
|
||||
产品标识
|
||||
</span>
|
||||
<ElTooltip
|
||||
:content="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 }}
|
||||
</span>
|
||||
</ElTooltip>
|
||||
</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
|
||||
v-if="item.picUrl"
|
||||
:src="item.picUrl"
|
||||
|
|
@ -163,11 +179,13 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
<!-- 按钮组 -->
|
||||
<div class="action-buttons">
|
||||
<div
|
||||
class="mt-auto flex gap-2 border-t border-border pt-3"
|
||||
>
|
||||
<ElButton
|
||||
v-if="hasAccessByCodes(['iot:product:update'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:edit" class="mr-1" />
|
||||
|
|
@ -176,7 +194,7 @@ onMounted(() => {
|
|||
<ElButton
|
||||
v-if="hasAccessByCodes(['iot:product:query'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:eye" class="mr-1" />
|
||||
|
|
@ -185,7 +203,7 @@ onMounted(() => {
|
|||
<ElButton
|
||||
v-if="hasAccessByCodes(['iot:thing-model:query'])"
|
||||
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)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:git-branch" class="mr-1" />
|
||||
|
|
@ -201,7 +219,7 @@ onMounted(() => {
|
|||
size="small"
|
||||
type="danger"
|
||||
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" />
|
||||
</ElButton>
|
||||
|
|
@ -215,7 +233,7 @@ onMounted(() => {
|
|||
<ElButton
|
||||
size="small"
|
||||
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" />
|
||||
</ElButton>
|
||||
|
|
@ -244,182 +262,3 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
</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