Commit db625d06b20e9266b242dc6b0a2ad2b8bb656891

Authored by xp.Huang
1 parent f241489f

feat:新增边缘管理

Showing 59 changed files with 4755 additions and 1 deletions

Too many changes to show.

To preserve performance only 59 of 68 files are displayed.

@@ -9,12 +9,16 @@ enum OrderType { @@ -9,12 +9,16 @@ enum OrderType {
9 ASC = 'ASC', 9 ASC = 'ASC',
10 DESC = 'DESC', 10 DESC = 'DESC',
11 } 11 }
  12 +enum SortProperty {
  13 + CREATEtIME = 'createdTime',
  14 +}
12 15
13 export interface BaseQueryParams { 16 export interface BaseQueryParams {
14 pageSize: number; 17 pageSize: number;
15 page: number; 18 page: number;
16 orderFiled?: string; 19 orderFiled?: string;
17 orderType?: OrderType; 20 orderType?: OrderType;
  21 + sortProperty?: SortProperty;
18 } 22 }
19 23
20 export class BaseQueryRequest implements BaseQueryParams { 24 export class BaseQueryRequest implements BaseQueryParams {
  1 +import { EdgeInstanceItemType, QueryEdgeInstancePageParams } from './model/edgeInstance';
  2 +import { PaginationResult } from '/#/axios';
  3 +import { defHttp } from '/@/utils/http/axios';
  4 +
  5 +enum EdgeManageApi {
  6 + PAGE_LIST_GET = '/tenant/edgeInfos',
  7 + EDGE = '/edge',
  8 + EDGE_INFO = '/edge/info',
  9 + EDGE_EVENTS = '/events/EDGE',
  10 + EDGE_DEVICE_INFO = '/device/info',
  11 + EDGE_SYNC = '/edge/sync',
  12 + EDGE_DEVICE_DISTRIBUTION = '/tenant/deviceInfos',
  13 +}
  14 +
  15 +//分页
  16 +export const edgeInstancePage = (params: QueryEdgeInstancePageParams) => {
  17 + return defHttp.get<PaginationResult<EdgeInstanceItemType>>(
  18 + {
  19 + url: EdgeManageApi.PAGE_LIST_GET,
  20 + params,
  21 + },
  22 + {
  23 + joinPrefix: false,
  24 + }
  25 + );
  26 +};
  27 +
  28 +// 创建或编辑
  29 +export const createOrEditEdgeInstance = (data: EdgeInstanceItemType) => {
  30 + return defHttp.post<EdgeInstanceItemType>(
  31 + {
  32 + url: EdgeManageApi.EDGE,
  33 + data,
  34 + },
  35 + {
  36 + joinPrefix: false,
  37 + }
  38 + );
  39 +};
  40 +
  41 +// 删除
  42 +export const deleteEdgeInstance = (id: string) => {
  43 + return defHttp.delete(
  44 + {
  45 + url: `${EdgeManageApi.EDGE}/${id}`,
  46 + },
  47 + {
  48 + joinPrefix: false,
  49 + }
  50 + );
  51 +};
  52 +
  53 +// 详情
  54 +export const infoEdgeInstance = (id: string) => {
  55 + return defHttp.get(
  56 + {
  57 + url: `${EdgeManageApi.EDGE_INFO}/${id}`,
  58 + },
  59 + {
  60 + joinPrefix: false,
  61 + }
  62 + );
  63 +};
  64 +
  65 +// 边缘事件
  66 +export const edgeEventPage = (params: QueryEdgeInstancePageParams, id: string | undefined) => {
  67 + return defHttp.get(
  68 + {
  69 + url: `${EdgeManageApi.EDGE_EVENTS}/${id}`,
  70 + params,
  71 + },
  72 + {
  73 + joinPrefix: false,
  74 + }
  75 + );
  76 +};
  77 +
  78 +// 边缘设备
  79 +export const edgeDevicePage = (params: QueryEdgeInstancePageParams, id: string | undefined) => {
  80 + return defHttp.get(
  81 + {
  82 + url: `${EdgeManageApi.EDGE}/${id}/devices`,
  83 + params,
  84 + },
  85 + {
  86 + joinPrefix: false,
  87 + }
  88 + );
  89 +};
  90 +
  91 +// 边缘设备详情
  92 +export const infoEdgeDevice = (id: string) => {
  93 + return defHttp.get(
  94 + {
  95 + url: `${EdgeManageApi.EDGE_DEVICE_INFO}/${id}`,
  96 + },
  97 + {
  98 + joinPrefix: false,
  99 + }
  100 + );
  101 +};
  102 +
  103 +// 边缘同步
  104 +export const syncEdge = (id: string) => {
  105 + return defHttp.post(
  106 + {
  107 + url: `${EdgeManageApi.EDGE_SYNC}/${id}`,
  108 + },
  109 + {
  110 + joinPrefix: false,
  111 + }
  112 + );
  113 +};
  114 +
  115 +//设备分配查询
  116 +export const edgeDeviceDistributionPage = (params: QueryEdgeInstancePageParams) => {
  117 + return defHttp.get<PaginationResult<EdgeInstanceItemType>>(
  118 + {
  119 + url: EdgeManageApi.EDGE_DEVICE_DISTRIBUTION,
  120 + params,
  121 + },
  122 + {
  123 + joinPrefix: false,
  124 + }
  125 + );
  126 +};
  127 +
  128 +//设备分配给边缘
  129 +export const edgeDeviceDistribution = (edgeId: string | undefined, deviceId: string) => {
  130 + return defHttp.post<PaginationResult<EdgeInstanceItemType>>(
  131 + {
  132 + url: `${EdgeManageApi.EDGE}/${edgeId}/device/${deviceId}`,
  133 + data: {
  134 + headers: {
  135 + normalizedNames: {},
  136 + lazyUpdate: null,
  137 + },
  138 + params: {
  139 + updates: null,
  140 + cloneFrom: null,
  141 + encoder: {},
  142 + map: null,
  143 + interceptorConfig: {
  144 + ignoreLoading: false,
  145 + ignoreErrors: false,
  146 + resendRequest: false,
  147 + },
  148 + },
  149 + },
  150 + },
  151 + {
  152 + joinPrefix: false,
  153 + }
  154 + );
  155 +};
  156 +
  157 +//取消设备分配给边缘
  158 +export const edgeDeviceDeleteDistribution = (edgeId: string | undefined, deviceId: string) => {
  159 + return defHttp.delete<PaginationResult<EdgeInstanceItemType>>(
  160 + {
  161 + url: `${EdgeManageApi.EDGE}/${edgeId}/device/${deviceId}`,
  162 + },
  163 + {
  164 + joinPrefix: false,
  165 + }
  166 + );
  167 +};
  1 +import { BaseQueryParams } from '../../base';
  2 +import {
  3 + Configuration,
  4 + ProvisionConfiguration,
  5 + TransportConfiguration,
  6 +} from '../../device/model/deviceModel';
  7 +import { ModelOfMatterParams } from '../../device/model/modelOfMatterModel';
  8 +
  9 +export type QueryEdgeInstancePageParams = BaseQueryParams & {
  10 + name?: string;
  11 + tenantId?: string;
  12 + textSearch?: string;
  13 +};
  14 +
  15 +export interface EdgeInstanceItemType {
  16 + id?: {
  17 + entityType: string;
  18 + id: string;
  19 + };
  20 + createdTime?: number;
  21 + tenantId?: {
  22 + entityType: string;
  23 + id: string;
  24 + };
  25 + customerId?: {
  26 + entityType: string;
  27 + id: string;
  28 + };
  29 + rootRuleChainId?: {
  30 + entityType: string;
  31 + id: string;
  32 + };
  33 + name: string;
  34 + label: string;
  35 + additionalInfo: {
  36 + description: string;
  37 + };
  38 + status: number;
  39 + type: string;
  40 + routingKey: string;
  41 + secret: string;
  42 + active?: boolean;
  43 +}
  44 +
  45 +export interface ProfileData {
  46 + configuration: Configuration;
  47 + transportConfiguration: TransportConfiguration;
  48 + provisionConfiguration: ProvisionConfiguration;
  49 + alarms?: any;
  50 + thingsModel?: ModelOfMatterParams[];
  51 +}
  52 +
  53 +export interface EdgeDeviceItemType {
  54 + creator: string;
  55 + createTime: string;
  56 + codeType?: string;
  57 + code?: string;
  58 + name: string;
  59 + transportType: string;
  60 + provisionType: string;
  61 + deviceType: string;
  62 + deviceCount: number;
  63 + tbDeviceId: string;
  64 + tbProfileId: string;
  65 + defaultQueueName: string;
  66 + image: string;
  67 + type: string;
  68 + default: boolean;
  69 + defaultRuleChainId: string;
  70 + profileId: string;
  71 + alias?: string;
  72 + brand?: string;
  73 + organizationId: string;
  74 + organizationDTO: {
  75 + name: string;
  76 + };
  77 + alarmStatus: number;
  78 + deviceProfile: {
  79 + default: boolean;
  80 + name: string;
  81 + transportType: string;
  82 + profileData: ProfileData;
  83 + };
  84 + customerAdditionalInfo?: {
  85 + isPublic?: boolean;
  86 + };
  87 + ifShowClass?: Boolean;
  88 + sip?: {
  89 + cameraCode: string;
  90 + localIp: string;
  91 + manufacturer: string;
  92 + streamMode: string;
  93 + };
  94 + customerName?: string;
  95 + gatewayId?: string;
  96 + id: {
  97 + entityType: string;
  98 + id: string;
  99 + };
  100 + createdTime: number;
  101 + additionalInfo: {
  102 + gateway: boolean;
  103 + overwriteActivityTime: boolean;
  104 + description: string;
  105 + };
  106 + tenantId: {
  107 + entityType: string;
  108 + id: string;
  109 + };
  110 + customerId: {
  111 + entityType: string;
  112 + id: string;
  113 + };
  114 + label: string;
  115 + deviceProfileId: {
  116 + entityType: string;
  117 + id: string;
  118 + };
  119 + deviceData: {
  120 + configuration: {
  121 + type: string;
  122 + };
  123 + transportConfiguration: {
  124 + type: string;
  125 + };
  126 + };
  127 + firmwareId: null;
  128 + softwareId: null;
  129 + externalId: null;
  130 + customerTitle: null;
  131 + customerIsPublic: boolean;
  132 + deviceProfileName: string;
  133 + active: boolean;
  134 + deviceState: string;
  135 + deviceToken?: string;
  136 + gatewayName?: string;
  137 + gatewayAlias?: string;
  138 + sn: string;
  139 +}
@@ -4,6 +4,7 @@ const menuMap = new Map(); @@ -4,6 +4,7 @@ const menuMap = new Map();
4 4
5 menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board'); 5 menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board');
6 menuMap.set('/rule/chain/:id', '/rule/chain'); 6 menuMap.set('/rule/chain/:id', '/rule/chain');
  7 +menuMap.set('/edge/edge_detail/:id', '/edge');
7 8
8 export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => { 9 export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => {
9 let flag = false; 10 let flag = false;
@@ -173,7 +173,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) { @@ -173,7 +173,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
173 // authentication schemes,e.g: Bearer 173 // authentication schemes,e.g: Bearer
174 // authenticationScheme: 'Bearer', 174 // authenticationScheme: 'Bearer',
175 authenticationScheme: 'Bearer', 175 authenticationScheme: 'Bearer',
176 - timeout: 10 * 1000, 176 + timeout: 26 * 1000,
177 // 基础接口地址 177 // 基础接口地址
178 // baseURL: globSetting.apiUrl, 178 // baseURL: globSetting.apiUrl,
179 // 接口可能会有通用的地址部分,可以统一抽取出来 179 // 接口可能会有通用的地址部分,可以统一抽取出来
@@ -89,3 +89,18 @@ export const withInstall = <T>(component: T, alias?: string) => { @@ -89,3 +89,18 @@ export const withInstall = <T>(component: T, alias?: string) => {
89 }; 89 };
90 return component as T & Plugin; 90 return component as T & Plugin;
91 }; 91 };
  92 +
  93 +// 字节单位转换
  94 +export const formatSizeUnits = (bytes) => {
  95 + if (bytes < 100) {
  96 + return bytes;
  97 + } else if (bytes < 1024) {
  98 + return bytes + 'bytes';
  99 + } else if (bytes < 1048576) {
  100 + return (bytes / 1024).toFixed(2) + 'KB';
  101 + } else if (bytes < 1073741824) {
  102 + return (bytes / 1048576).toFixed(2) + 'MB';
  103 + } else {
  104 + return (bytes / 1073741824).toFixed(2) + 'GB';
  105 + }
  106 +};
  1 +export { default as CardMode } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { Button, Tooltip, Card, Popconfirm } from 'ant-design-vue';
  3 + import { AuthIcon, EnumTableCardMode } from '/@/components/Widget';
  4 + import { useMessage } from '/@/hooks/web/useMessage';
  5 + import { BasicCardList, useCardList } from '/@/components/CardList';
  6 + import { useRoute } from 'vue-router';
  7 + import { ref } from 'vue';
  8 + import { HandleOperationEnum, HandleOperationNameEnum } from '../../config';
  9 + import { searchFormSchema } from '../EdgeInstance/config';
  10 + import { deleteEdgeInstance, edgeInstancePage } from '/@/api/edgeManage/edgeInstance';
  11 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  12 + import { EdgeInstanceFormDrawer } from '../EdgeInstance';
  13 + import { useDrawer } from '/@/components/Drawer';
  14 + import moment from 'moment';
  15 + import { isArray } from '/@/utils/is';
  16 + import AuthDropDown from '/@/components/Widget/AuthDropDown.vue';
  17 + import edgeStatusIsOnlinePng from '/@/assets/images/edgeStatusIsOnline.png';
  18 + import edgeStatusIsOfflinePng from '/@/assets/images/edgeStatusIsOffline.png';
  19 + import { Image } from 'ant-design-vue';
  20 + import { useGo } from '/@/hooks/web/usePage';
  21 +
  22 + defineProps<{
  23 + mode: EnumTableCardMode;
  24 + }>();
  25 +
  26 + defineEmits(['register']);
  27 +
  28 + enum DropMenuEvent {
  29 + DELETE = 'delete',
  30 + }
  31 +
  32 + const { createMessage } = useMessage();
  33 +
  34 + const go = useGo();
  35 +
  36 + const { query } = useRoute();
  37 +
  38 + const disabledDeleteFlag = ref(true);
  39 +
  40 + const [registerCardList, { reload, getSelectedRecords, clearSelectedKeys }] = useCardList({
  41 + api: async ({ page, pageSize, textSearch }) => {
  42 + const res = await edgeInstancePage({
  43 + page: page === 1 ? 0 : page,
  44 + pageSize,
  45 + textSearch,
  46 + });
  47 + return {
  48 + total: res?.totalElements,
  49 + items: res?.data,
  50 + };
  51 + },
  52 + useSearchForm: true,
  53 + gutter: 4,
  54 + rowKey: 'routingKey', //id是对象
  55 + formConfig: {
  56 + schemas: searchFormSchema,
  57 + labelWidth: 100,
  58 + model: {
  59 + name: (query as Recordable)?.name ? decodeURIComponent((query as Recordable)?.name) : null,
  60 + },
  61 + },
  62 + selections: {
  63 + beforeSelectValidate: () => {
  64 + return true;
  65 + },
  66 + onSelect: (_record, _flag, allSelecteds) => {
  67 + disabledDeleteFlag.value = !allSelecteds.length;
  68 + },
  69 + onSelectAll: () => {
  70 + // 全选事件
  71 + disabledDeleteFlag.value = false;
  72 + },
  73 + onUnSelectAll: () => {
  74 + // 反选事件
  75 + disabledDeleteFlag.value = true;
  76 + },
  77 + onSelectToggle: (status: boolean) => {
  78 + // 全选是false,反选是true
  79 + if (!status) disabledDeleteFlag.value = false;
  80 + else disabledDeleteFlag.value = true;
  81 + },
  82 + },
  83 + });
  84 +
  85 + const [registerEdgeInstanceFormDrawer, { openDrawer: openEdgeInstanceFormDrawer }] = useDrawer();
  86 +
  87 + const handleEventIsSuccess = () => reload();
  88 +
  89 + const handleGoDetail = (record: EdgeInstanceItemType | null) => {
  90 + go('/edge/edge_detail/' + record?.id?.id);
  91 + };
  92 +
  93 + const handleOperationEvent = (
  94 + event: HandleOperationEnum,
  95 + record: EdgeInstanceItemType | null
  96 + ) => {
  97 + const isUpdate = event === HandleOperationEnum.CREATE ? false : true;
  98 + const isUpdateText =
  99 + event === HandleOperationEnum.CREATE
  100 + ? HandleOperationNameEnum.CREATE
  101 + : event === HandleOperationEnum.UPDATE
  102 + ? HandleOperationNameEnum.UPDATE
  103 + : HandleOperationNameEnum.VIEW;
  104 + if (event === HandleOperationEnum.VIEW) {
  105 + } else {
  106 + openEdgeInstanceFormDrawer(true, { isUpdate, record, isUpdateText, event });
  107 + }
  108 + };
  109 +
  110 + const handleDelete = async (event: HandleOperationEnum, id?: string | null) => {
  111 + try {
  112 + if (event === HandleOperationEnum.BATCH_DELETE) {
  113 + const batchDeleteIds = getSelectedRecords().map(
  114 + (rowRecord: EdgeInstanceItemType) => rowRecord?.id?.id
  115 + );
  116 + if (isArray(batchDeleteIds) && batchDeleteIds.length === 0) return;
  117 + for (let item of batchDeleteIds) await deleteEdgeInstance(item!);
  118 + } else {
  119 + await deleteEdgeInstance(id!);
  120 + }
  121 + createMessage.success('删除成功');
  122 + clearSelectedKeys();
  123 + disabledDeleteFlag.value = true;
  124 + await reload();
  125 + } catch (error) {
  126 + throw error;
  127 + }
  128 + };
  129 +</script>
  130 +
  131 +<template>
  132 + <section>
  133 + <BasicCardList @register="registerCardList">
  134 + <template #toolbar>
  135 + <Button type="primary" @click="handleOperationEvent(HandleOperationEnum.CREATE, null)"
  136 + >新增实例</Button
  137 + >
  138 + <Popconfirm
  139 + title="您确定要批量删除数据"
  140 + ok-text="确定"
  141 + cancel-text="取消"
  142 + @confirm="handleDelete(HandleOperationEnum.BATCH_DELETE, null)"
  143 + :disabled="disabledDeleteFlag"
  144 + >
  145 + <Button type="primary" danger :disabled="disabledDeleteFlag"> 批量删除 </Button>
  146 + </Popconfirm>
  147 + </template>
  148 + <template #renderItem="{ item }: BasicCardListRenderItem<EdgeInstanceItemType>">
  149 + <Card hoverable>
  150 + <template #cover>
  151 + <div class="w-full h-full !flex flex-col justify-between m-3">
  152 + <div class="!flex justify-between align-center text-center">
  153 + <span class="truncate font-bold fill-dark-900 text-sm"> {{ item.name }} </span>
  154 + <div class="mr-6">
  155 + <a-tag class="!flex items-center" :color="item.active ? '#E8FFEA' : '#FFECE8'">
  156 + <template #icon>
  157 + <template v-if="item.active">
  158 + <Image :width="12" :height="12" :src="edgeStatusIsOnlinePng" />
  159 + </template>
  160 + <template v-else>
  161 + <Image :width="12" :height="12" :src="edgeStatusIsOfflinePng" />
  162 + </template>
  163 + </template>
  164 + <span class="ml-1" :style="{ color: item.active ? '#00B42A' : '#F53F3F' }">{{
  165 + item.active ? '在线' : '离线'
  166 + }}</span>
  167 + </a-tag>
  168 + </div>
  169 + </div>
  170 + <div class="!flex justify-between align-center text-center">
  171 + <span class="truncate text-xs" style="color: #86909c">
  172 + {{ moment(item.createdTime).format('YYYY-MM-DD HH:mm:ss') }}
  173 + </span>
  174 + </div>
  175 + </div>
  176 + </template>
  177 + <template class="ant-card-actions" #actions>
  178 + <Tooltip title="详情">
  179 + <AuthIcon
  180 + class="!text-lg"
  181 + icon="ant-design:eye-outlined"
  182 + @click.stop="handleGoDetail(item)"
  183 + />
  184 + </Tooltip>
  185 + <Tooltip title="编辑">
  186 + <AuthIcon
  187 + class="!text-lg"
  188 + icon="ant-design:form-outlined"
  189 + @click.stop="handleOperationEvent(HandleOperationEnum.UPDATE, item)"
  190 + />
  191 + </Tooltip>
  192 + <AuthDropDown
  193 + @click.stop
  194 + :trigger="['hover']"
  195 + :drop-menu-list="[
  196 + {
  197 + text: '删除',
  198 + event: DropMenuEvent.DELETE,
  199 + icon: 'ant-design:delete-outlined',
  200 + popconfirm: {
  201 + title: '是否确认删除操作?',
  202 + onConfirm: handleDelete.bind(null, HandleOperationEnum.DELETE, item?.id?.id),
  203 + },
  204 + },
  205 + ]"
  206 + />
  207 + </template>
  208 + <Card.Meta>
  209 + <template #description>
  210 + <div class="truncate h-17 !flex justify-between flex-col">
  211 + <div class="truncate !flex">
  212 + <span class="text-xs" style="color: #86909c">标签</span>
  213 + <span class="truncate ml-7.5 text-xs" style="color: #00b42a">{{
  214 + item.label
  215 + }}</span>
  216 + </div>
  217 + <div class="truncate !flex">
  218 + <span class="text-xs" style="color: #86909c">边缘类型</span>
  219 + <span style="color: #4e5969" class="truncate ml-7.5 text-xs">{{
  220 + item.type
  221 + }}</span>
  222 + </div>
  223 + <div class="truncate !flex">
  224 + <span class="text-xs" style="color: #86909c">描述</span>
  225 + <span style="color: #4e5969" class="truncate ml-7.5 text-xs">
  226 + {{ item?.additionalInfo?.description }}
  227 + </span>
  228 + </div>
  229 + </div>
  230 + </template>
  231 + </Card.Meta>
  232 + </Card>
  233 + </template>
  234 + </BasicCardList>
  235 + <EdgeInstanceFormDrawer
  236 + @register="registerEdgeInstanceFormDrawer"
  237 + @success="handleEventIsSuccess"
  238 + />
  239 + </section>
  240 +</template>
  241 +
  242 +<style lang="less" scoped>
  243 + .profile-list:deep(.ant-image-img) {
  244 + @apply !w-full !h-full;
  245 + }
  246 +
  247 + .profile-list:deep(.ant-card-body) {
  248 + @apply !p-4;
  249 + }
  250 +
  251 + :deep(.ant-card-body) {
  252 + padding: 12px;
  253 + }
  254 +</style>
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { EdgeDeviceBasicInfo } from '../EdgeDeviceBasicInfo';
  4 + import { EdgeDeviceTabInfo } from '../EdgeDeviceTabInfo';
  5 + import { EdgeDeviceItemType, EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 + import { infoEdgeDevice } from '/@/api/edgeManage/edgeInstance';
  7 + import { useRoute } from 'vue-router';
  8 + import { PageWrapper } from '/@/components/Page';
  9 + import { useGo } from '/@/hooks/web/usePage';
  10 +
  11 + const emits = defineEmits(['register', 'success']);
  12 +
  13 + defineProps({
  14 + recordEdgeInstanceData: {
  15 + type: Object as PropType<EdgeInstanceItemType>,
  16 + default: () => {},
  17 + },
  18 + });
  19 +
  20 + const route = useRoute();
  21 +
  22 + const go = useGo();
  23 +
  24 + const deviceId = ref(route.params?.id);
  25 +
  26 + const edgeId = ref(route.params?.edgeId as string);
  27 +
  28 + const recordData = ref<EdgeDeviceItemType>();
  29 +
  30 + const handleEventIsSuccess = () => {
  31 + emits('success');
  32 + };
  33 +
  34 + infoEdgeDevice(deviceId.value as string).then((res) => {
  35 + recordData.value = res;
  36 + });
  37 +
  38 + function goBack() {
  39 + go('/edge/edge_device/' + edgeId.value);
  40 + }
  41 +</script>
  42 +
  43 +<template>
  44 + <PageWrapper :title="`边缘设备详情`" contentBackground @back="goBack">
  45 + <!-- 基础信息 -->
  46 + <div class="m-4">
  47 + <EdgeDeviceBasicInfo
  48 + :recordData="recordData"
  49 + :edgeId="edgeId"
  50 + @success="handleEventIsSuccess"
  51 + />
  52 + </div>
  53 + <!-- Tab信息 -->
  54 + <div>
  55 + <EdgeDeviceTabInfo v-if="recordData" :recordData="recordData" />
  56 + </div>
  57 + </PageWrapper>
  58 +</template>
  59 +
  60 +<style lang="less" scoped></style>
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import { Tag, Tooltip } from 'ant-design-vue';
  3 +import { h } from 'vue';
  4 +import { transformTime } from '/@/hooks/web/useDateToLocaleString';
  5 +import { DeviceState, DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  6 +import { handeleCopy } from '/@/views/device/profiles/step/topic';
  7 +
  8 +// 表格配置
  9 +export const columns: BasicColumn[] = [
  10 + {
  11 + title: '设备状态',
  12 + dataIndex: 'deviceState',
  13 + width: 100,
  14 + customRender: ({ record }) => {
  15 + const color =
  16 + record.deviceState == DeviceState.INACTIVE
  17 + ? 'warning'
  18 + : record.deviceState == DeviceState.ONLINE
  19 + ? 'success'
  20 + : 'error';
  21 + const text =
  22 + record.deviceState == DeviceState.INACTIVE
  23 + ? '待激活'
  24 + : record.deviceState == DeviceState.ONLINE
  25 + ? '在线'
  26 + : '离线';
  27 + return h(Tag, { color: color }, () => text);
  28 + },
  29 + },
  30 + {
  31 + dataIndex: 'name',
  32 + title: '别名/设备名称',
  33 + width: 210,
  34 + slots: { customRender: 'name', title: 'deviceTitle' },
  35 + customRender: ({ record }) => {
  36 + return h('div', { style: 'display:flex;flex-direction:column' }, [
  37 + record.alias &&
  38 + h(
  39 + 'div',
  40 + {
  41 + class: 'cursor-pointer truncate',
  42 + },
  43 + h(
  44 + Tooltip,
  45 + {
  46 + placement: 'topLeft',
  47 + title: `${record.alias}`,
  48 + },
  49 + () => `${record.alias}`
  50 + )
  51 + ),
  52 + h(
  53 + 'div',
  54 + {
  55 + class: 'cursor-pointer text-blue-500 truncate',
  56 + onClick: () => {
  57 + handeleCopy(`${record.name}`);
  58 + },
  59 + },
  60 + h(
  61 + Tooltip,
  62 + {
  63 + placement: 'topLeft',
  64 + title: `${record.name}`,
  65 + },
  66 + () => `${record.name}`
  67 + )
  68 + ),
  69 + ]);
  70 + },
  71 + },
  72 + {
  73 + title: '所属产品',
  74 + width: 160,
  75 + dataIndex: 'deviceProfileName',
  76 + },
  77 + {
  78 + title: '所属组织',
  79 + dataIndex: 'organizationDTO.name',
  80 + width: 160,
  81 + },
  82 + {
  83 + title: '设备类型',
  84 + width: 100,
  85 + dataIndex: 'deviceType',
  86 + customRender: ({ record }) => {
  87 + const color = 'success';
  88 + const text =
  89 + record.deviceType === DeviceTypeEnum.GATEWAY
  90 + ? '网关设备'
  91 + : record.deviceType === DeviceTypeEnum.DIRECT_CONNECTION
  92 + ? '直连设备'
  93 + : '网关子设备';
  94 + return h(Tag, { color: color }, () => text);
  95 + },
  96 + },
  97 + {
  98 + title: '创建时间',
  99 + width: 120,
  100 + dataIndex: 'createdTime',
  101 + format: (_text: string, record: Recordable) => {
  102 + return transformTime(record.createdTime);
  103 + },
  104 + },
  105 +];
  106 +
  107 +// 表格查询表单
  108 +export const searchFormSchema: FormSchema[] = [
  109 + {
  110 + field: 'textSearch',
  111 + label: '设备名称',
  112 + component: 'Input',
  113 + colProps: { span: 6 },
  114 + componentProps: {
  115 + maxLength: 255,
  116 + placeholder: '请输入设备名称',
  117 + },
  118 + },
  119 +];
  1 +export { default as EdgeDevice } from './index.vue';
  2 +export { default as EdgeDeviceDetail } from './EdgeDeviceDetailDrawer.vue';
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { EdgeDeviceItemType, EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  4 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  5 + import { columns, searchFormSchema } from './config';
  6 + import { edgeDeviceDeleteDistribution, edgeDevicePage } from '/@/api/edgeManage/edgeInstance';
  7 + import { useModal } from '/@/components/Modal';
  8 + import { EdgeDeviceDistribution } from '../EdgeDeviceDistribution';
  9 + import { useMessage } from '/@/hooks/web/useMessage';
  10 + import { useRoute } from 'vue-router';
  11 + import { useGo } from '/@/hooks/web/usePage';
  12 + import { PageWrapper } from '/@/components/Page';
  13 +
  14 + defineEmits(['register']);
  15 +
  16 + defineProps({
  17 + recordData: {
  18 + type: Object as PropType<EdgeInstanceItemType>,
  19 + default: () => {},
  20 + },
  21 + });
  22 +
  23 + const route = useRoute();
  24 +
  25 + const edgeId = ref(route.params?.id as string);
  26 +
  27 + const go = useGo();
  28 +
  29 + const { createMessage } = useMessage();
  30 +
  31 + const [registerEdgeDeviceDistributionModal, { openModal }] = useModal();
  32 +
  33 + const handleEventIsDistribution = () => {
  34 + openModal(true, {
  35 + record: 1,
  36 + });
  37 + };
  38 +
  39 + const handleEventIsCancelDistribution = async (record: EdgeDeviceItemType) => {
  40 + await edgeDeviceDeleteDistribution(edgeId.value as string, record.id.id);
  41 + createMessage.success('取消分配成功');
  42 + reload();
  43 + };
  44 +
  45 + const [registerTable, { reload }] = useTable({
  46 + title: '边缘设备',
  47 + columns,
  48 + api: async ({ page, pageSize, textSearch }) => {
  49 + const res = await edgeDevicePage(
  50 + {
  51 + page: page - 1 < 0 ? 0 : page - 1,
  52 + pageSize,
  53 + textSearch,
  54 + },
  55 + edgeId.value as string
  56 + );
  57 + return {
  58 + total: res?.totalElements,
  59 + items: res?.data,
  60 + };
  61 + },
  62 + formConfig: {
  63 + labelWidth: 100,
  64 + schemas: searchFormSchema,
  65 + },
  66 + useSearchForm: true,
  67 + showIndexColumn: false,
  68 + clickToRowSelect: false,
  69 + showTableSetting: true,
  70 + bordered: true,
  71 + rowKey: 'name',
  72 + actionColumn: {
  73 + width: 140,
  74 + title: '操作',
  75 + slots: { customRender: 'action' },
  76 + fixed: 'right',
  77 + },
  78 + });
  79 +
  80 + const handleEventIsSuccess = () => reload();
  81 +
  82 + function goBack() {
  83 + go('/edge/edge_detail/' + edgeId.value);
  84 + }
  85 +
  86 + function handleGoDeviceDetail(record: Recordable) {
  87 + go(`/edge/edge_device/edge_device_detail/${record?.id?.id}/${edgeId.value}`);
  88 + }
  89 +</script>
  90 +
  91 +<template>
  92 + <PageWrapper :title="`边缘设备`" contentBackground @back="goBack">
  93 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  94 + <template #toolbar>
  95 + <a-button type="primary" @click="handleEventIsDistribution"> 分配设备 </a-button>
  96 + </template>
  97 + <template #action="{ record }">
  98 + <TableAction
  99 + :actions="[
  100 + {
  101 + label: '详情',
  102 + icon: 'ant-design:eye-outlined',
  103 + onClick: handleGoDeviceDetail.bind(null, record),
  104 + },
  105 + {
  106 + label: '取消分配',
  107 + icon: 'mdi:account-arrow-left',
  108 + onClick: handleEventIsCancelDistribution.bind(null, record),
  109 + },
  110 + ]"
  111 + />
  112 + </template>
  113 + </BasicTable>
  114 + <EdgeDeviceDistribution
  115 + @register="registerEdgeDeviceDistributionModal"
  116 + :edgeId="edgeId"
  117 + @success="handleEventIsSuccess"
  118 + />
  119 + </PageWrapper>
  120 +</template>
  121 +
  122 +<style lang="less" scoped></style>
  1 +import { findDictItemByCode } from '/@/api/system/dict';
  2 +import { BasicColumn, FormSchema } from '/@/components/Table';
  3 +import { AlarmStatus } from '/@/enums/alarmEnum';
  4 +import { alarmLevel } from '/@/views/device/list/config/detail.config';
  5 +import { Tag } from 'ant-design-vue';
  6 +import { h } from 'vue';
  7 +
  8 +// 表格配置
  9 +export const columns: BasicColumn[] = [
  10 + {
  11 + title: '告警状态',
  12 + dataIndex: 'status',
  13 + customRender({ record }: { record }) {
  14 + const flag = !!record.cleared;
  15 + return h(Tag, { color: flag ? 'green' : 'red' }, () => (flag ? '清除' : '激活'));
  16 + },
  17 + width: 90,
  18 + },
  19 + {
  20 + title: '告警设备',
  21 + dataIndex: 'deviceName',
  22 + customRender: ({ record }) => {
  23 + const { deviceAlias, deviceName } = record || {};
  24 + return deviceAlias || deviceName;
  25 + },
  26 + },
  27 + {
  28 + title: '告警场景',
  29 + dataIndex: 'type',
  30 + },
  31 + {
  32 + title: '告警级别',
  33 + dataIndex: 'severity',
  34 + format: (text) => alarmLevel(text),
  35 + },
  36 + {
  37 + title: '告警时间',
  38 + dataIndex: 'createdTime',
  39 + },
  40 + {
  41 + title: '告警详情',
  42 + dataIndex: 'details',
  43 + slots: { customRender: 'details' },
  44 + },
  45 +];
  46 +
  47 +// 表格查询表单
  48 +export const searchFormSchema: FormSchema[] = [
  49 + {
  50 + field: 'status',
  51 + label: '告警/确认状态',
  52 + component: 'Cascader',
  53 + helpMessage: [
  54 + '激活未确认: 可以处理,清除',
  55 + '激活已确认: 只可清除,已经处理',
  56 + '清除未确认: 只可处理,已经清除',
  57 + '清除已确认: 不需要做处理和清除',
  58 + ],
  59 + colProps: { span: 6 },
  60 + componentProps: {
  61 + popupClassName: 'alarm-stauts-cascader',
  62 + options: [
  63 + {
  64 + value: '0',
  65 + label: '清除',
  66 + children: [
  67 + {
  68 + value: AlarmStatus.CLEARED_UN_ACK,
  69 + label: '清除未确认',
  70 + },
  71 + {
  72 + value: AlarmStatus.CLEARED_ACK,
  73 + label: '清除已确认',
  74 + },
  75 + ],
  76 + },
  77 + {
  78 + value: '1',
  79 + label: '激活',
  80 + children: [
  81 + {
  82 + value: AlarmStatus.ACTIVE_UN_ACK,
  83 + label: '激活未确认',
  84 + },
  85 + {
  86 + value: AlarmStatus.ACTIVE_ACK,
  87 + label: '激活已确认',
  88 + },
  89 + ],
  90 + },
  91 + ],
  92 + placeholder: '请选择告警/确认状态',
  93 + },
  94 + },
  95 + {
  96 + field: 'severity',
  97 + label: '告警级别',
  98 + component: 'ApiSelect',
  99 + colProps: { span: 6 },
  100 + componentProps: {
  101 + placeholder: '请选择告警级别',
  102 + api: findDictItemByCode,
  103 + params: {
  104 + dictCode: 'severity_type',
  105 + },
  106 + labelField: 'itemText',
  107 + valueField: 'itemValue',
  108 + },
  109 + },
  110 +];
  1 +export { default as EdgeDeviceAlarm } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
  3 + import { columns, searchFormSchema } from './config';
  4 + import { BasicModal, useModal } from '/@/components/Modal';
  5 + import { Input, Button } from 'ant-design-vue';
  6 + import { ref, computed, nextTick } from 'vue';
  7 + import { EyeOutlined } from '@ant-design/icons-vue';
  8 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  9 + import { getDeviceAlarm, doBatchAckAlarm, doBatchClearAlarm } from '/@/api/device/deviceManager';
  10 + import { AlarmLogItem } from '/@/api/device/model/deviceConfigModel';
  11 + import { AlarmStatus } from '/@/enums/alarmEnum';
  12 + import { useMessage } from '/@/hooks/web/useMessage';
  13 + import { getDeviceDetail } from '/@/api/device/deviceManager';
  14 + import { getAttribute } from '/@/api/ruleengine/ruleengineApi';
  15 + import {
  16 + operationBoolean,
  17 + operationNumber_OR_TIME,
  18 + operationString,
  19 + } from '/@/views/rule/linkedge/config/formatData';
  20 + import { clearOrAckAlarm } from '/@/api/device/deviceManager';
  21 +
  22 + const props = defineProps({
  23 + recordData: {
  24 + type: Object as PropType<EdgeDeviceItemType>,
  25 + default: () => {},
  26 + },
  27 + });
  28 +
  29 + defineEmits(['register']);
  30 +
  31 + const outputData = ref<string>();
  32 +
  33 + const { createMessage } = useMessage();
  34 +
  35 + const [registerTable, { reload, getSelectRows, clearSelectedRowKeys, getRowSelection }] =
  36 + useTable({
  37 + columns,
  38 + api: getDeviceAlarm,
  39 + beforeFetch: (params) => {
  40 + const { status } = params;
  41 + const obj = {
  42 + ...params,
  43 + deviceId: props?.recordData?.id?.id,
  44 + ...{
  45 + status: status ? status.at(-1) : null,
  46 + },
  47 + };
  48 + return obj;
  49 + },
  50 + formConfig: {
  51 + labelWidth: 130,
  52 + schemas: searchFormSchema,
  53 + },
  54 + useSearchForm: true,
  55 + showIndexColumn: false,
  56 + clickToRowSelect: false,
  57 + showTableSetting: true,
  58 + bordered: true,
  59 + rowKey: 'id',
  60 + rowSelection: {
  61 + type: 'checkbox',
  62 + getCheckboxProps: (record: AlarmLogItem) => {
  63 + return {
  64 + disabled: record.status === AlarmStatus.CLEARED_ACK,
  65 + };
  66 + },
  67 + },
  68 + actionColumn: {
  69 + title: '操作',
  70 + slots: { customRender: 'action' },
  71 + fixed: 'right',
  72 + },
  73 + });
  74 +
  75 + const [registerModal, { openModal, closeModal }] = useModal();
  76 +
  77 + const findName = (item: Recordable, curr: Recordable) => {
  78 + return item.attribute.find((item) => item.identifier === curr?.key)?.name;
  79 + };
  80 +
  81 + const findLogin = (curr: Recordable) => {
  82 + return [...operationNumber_OR_TIME, ...operationString, ...operationBoolean].find(
  83 + (item) => item.value === curr?.logic
  84 + )?.symbol;
  85 + };
  86 +
  87 + const findAttribute = (item: Recordable, curr: Recordable) => {
  88 + item.attribute.find((findItem) => findItem.identifier === curr?.key);
  89 + };
  90 +
  91 + const findValue = (item: Recordable, curr: Recordable) => {
  92 + return {
  93 + ['触发属性']: findName(item, curr),
  94 + ['触发条件']: `${findLogin(curr)}${curr?.logicValue}`,
  95 + ['触发值']: `${curr?.realValue}${
  96 + (findAttribute(item, curr) as any)?.detail?.dataType?.specs?.unit?.key ?? ''
  97 + }`,
  98 + };
  99 + };
  100 +
  101 + const handleAlarmText = (text: string) => (text === 'triggerData' ? '触发器' : '执行条件');
  102 +
  103 + const handleViewDetail = async (record: Recordable) => {
  104 + await nextTick();
  105 + const { details } = record;
  106 + if (!details) return;
  107 + const deviceIdKeys = Object.keys(details);
  108 + const dataFormat = await handleAlarmDetailFormat(deviceIdKeys);
  109 + const mapDataFormat = deviceIdKeys.map((deviceKey: string) => {
  110 + const findDataFormat = dataFormat.find(
  111 + (dataItem: Recordable) => dataItem.tbDeviceId === deviceKey
  112 + );
  113 + const dataKeys = Object.keys(details[deviceKey]);
  114 + const data: any = dataKeys.map((dataItem: string) => {
  115 + if (dataItem !== 'triggerData' && dataItem !== 'conditionData') {
  116 + return findValue(findDataFormat, details[deviceKey]);
  117 + } else {
  118 + return {
  119 + [handleAlarmText(dataItem)]: findValue(findDataFormat, details[deviceKey][dataItem]),
  120 + };
  121 + }
  122 + });
  123 + const objectDataFormat = data.reduce((acc: Recordable, curr: Recordable) => {
  124 + return {
  125 + ...acc,
  126 + ...curr,
  127 + };
  128 + });
  129 + return {
  130 + [findDataFormat.name]: objectDataFormat,
  131 + };
  132 + });
  133 + const objectDataFormats = mapDataFormat.reduce((acc: Recordable, curr: Recordable) => {
  134 + return {
  135 + ...acc,
  136 + ...curr,
  137 + };
  138 + });
  139 + outputData.value = JSON.stringify(objectDataFormats, null, 2);
  140 + openModal(true);
  141 + };
  142 +
  143 + const handleAlarmDetailFormat = async (keys: string[]) => {
  144 + const temp: Recordable = [];
  145 + for (let item of keys) {
  146 + if (item === 'key' || item === 'data') return []; //旧数据则终止
  147 + const deviceDetailRes = await getDeviceDetail(item);
  148 + const { deviceProfileId } = deviceDetailRes;
  149 + if (!deviceProfileId) return [];
  150 + const attributeRes = await getAttribute(deviceProfileId);
  151 + const dataFormat: Recordable = handleDataFormat(deviceDetailRes, attributeRes);
  152 + temp.push(dataFormat);
  153 + }
  154 + return temp;
  155 + };
  156 +
  157 + const handleDataFormat = (deviceDetail: Recordable, attributes: Recordable) => {
  158 + const { name, tbDeviceId, alias } = deviceDetail;
  159 + const attribute = attributes.map((item) => ({
  160 + identifier: item.identifier,
  161 + name: item.name,
  162 + detail: item.detail,
  163 + }));
  164 + return {
  165 + name: alias || name,
  166 + tbDeviceId,
  167 + attribute,
  168 + };
  169 + };
  170 +
  171 + const handleEventIsDone = async (record: Recordable) => {
  172 + if (!record.id) return;
  173 + await clearOrAckAlarm(record.id, false);
  174 + createMessage.success('操作成功');
  175 + reload();
  176 + };
  177 +
  178 + const handleEventIsClear = async (record: Recordable) => {
  179 + if (!record.id) return;
  180 + await clearOrAckAlarm(record.id, true);
  181 + createMessage.success('操作成功');
  182 + reload();
  183 + };
  184 +
  185 + const getCanBatchClear = computed(() => {
  186 + const rowSelection = getRowSelection();
  187 + const getRows: AlarmLogItem[] = getSelectRows();
  188 + return !rowSelection.selectedRowKeys?.length || !getRows.every((item) => !item.cleared);
  189 + });
  190 +
  191 + const getCanBatchAck = computed(() => {
  192 + const rowSelection = getRowSelection();
  193 + const getRows: AlarmLogItem[] = getSelectRows();
  194 + return !rowSelection.selectedRowKeys?.length || !getRows.every((item) => !item.acknowledged);
  195 + });
  196 +
  197 + const handleBatchClear = async () => {
  198 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  199 + if (!ids.length) return;
  200 + await doBatchClearAlarm(ids);
  201 + createMessage.success('操作成功');
  202 + clearSelectedRowKeys();
  203 + reload();
  204 + };
  205 +
  206 + const handleBatchAck = async () => {
  207 + const ids = getSelectRows<AlarmLogItem>().map((item) => item.id);
  208 + if (!ids.length) return;
  209 + await doBatchAckAlarm(ids);
  210 + createMessage.success('操作成功');
  211 + clearSelectedRowKeys();
  212 + reload();
  213 + };
  214 +</script>
  215 +
  216 +<template>
  217 + <div>
  218 + <BasicTable @register="registerTable">
  219 + <template #details="{ record }">
  220 + <span class="cursor-pointer text-blue-500" @click="handleViewDetail(record)">
  221 + <EyeOutlined class="svg:text-blue-500" />
  222 + <span class="ml-2">查看告警详情</span>
  223 + </span>
  224 + </template>
  225 + <template #action="{ record }">
  226 + <TableAction
  227 + :actions="[
  228 + {
  229 + label: '处理',
  230 + icon: 'ant-design:edit-outlined',
  231 + onClick: handleEventIsDone.bind(null, record),
  232 + ifShow: !record.acknowledged,
  233 + },
  234 + {
  235 + label: '清除',
  236 + icon: 'ant-design:close-circle-outlined',
  237 + onClick: handleEventIsClear.bind(null, record),
  238 + ifShow: !record.cleared,
  239 + },
  240 + ]"
  241 + />
  242 + </template>
  243 + <template #toolbar>
  244 + <a-button danger :disabled="getCanBatchClear" @click="handleBatchClear">
  245 + 批量清除
  246 + </a-button>
  247 + <Button @click="handleBatchAck" type="primary" :disabled="getCanBatchAck">
  248 + <span>批量处理</span>
  249 + </Button>
  250 + </template>
  251 + </BasicTable>
  252 + <BasicModal title="告警详情" @register="registerModal" @ok="closeModal">
  253 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  254 + </BasicModal>
  255 + </div>
  256 +</template>
  257 +
  258 +<style>
  259 + .alarm-stauts-cascader {
  260 + .ant-cascader-menu {
  261 + height: fit-content;
  262 + }
  263 + }
  264 +</style>
  1 +import { Tag, Tooltip } from 'ant-design-vue';
  2 +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  3 +import { DescItem } from '/@/components/Description/src/typing';
  4 +import { formatToDateTime } from '/@/utils/dateUtil';
  5 +import { CSSProperties, h } from 'vue';
  6 +
  7 +export const descSchema = (): DescItem[] => {
  8 + return [
  9 + {
  10 + field: 'name',
  11 + label: '设备名称',
  12 + render(val, data: Record<'alias' | 'name', string>) {
  13 + return h(Tooltip, { title: data.alias || val }, () =>
  14 + h('span', { style: { cursor: 'pointer' } as CSSProperties }, data.alias || val)
  15 + );
  16 + },
  17 + },
  18 + {
  19 + field: 'label',
  20 + label: '设备标签',
  21 + render: (text) => {
  22 + return text
  23 + ? h(
  24 + Tag,
  25 + {
  26 + color: '#00B42A',
  27 + },
  28 + text
  29 + )
  30 + : '';
  31 + },
  32 + },
  33 + {
  34 + field: 'gatewayName',
  35 + label: '所属网关',
  36 + render: (text) => {
  37 + return text
  38 + ? h(
  39 + Tag,
  40 + {
  41 + color: '#00B42A',
  42 + },
  43 + text
  44 + )
  45 + : '';
  46 + },
  47 + },
  48 + {
  49 + field: 'deviceProfileName',
  50 + label: '产品',
  51 + },
  52 + {
  53 + field: 'deviceType',
  54 + label: '设备类型',
  55 + render: (_, data) => {
  56 + const text =
  57 + data.deviceType === DeviceTypeEnum.GATEWAY
  58 + ? '网关设备'
  59 + : data.deviceType === DeviceTypeEnum.DIRECT_CONNECTION
  60 + ? '直连设备'
  61 + : '网关子设备';
  62 + return h(
  63 + 'span',
  64 + {
  65 + style: { cursor: 'pointer' },
  66 + },
  67 + text
  68 + );
  69 + },
  70 + },
  71 + {
  72 + field: 'createdTime',
  73 + label: '创建时间',
  74 + render: (_, data) => {
  75 + return formatToDateTime(data.createdTime, 'YYYY-MM-DD HH:mm:ss');
  76 + },
  77 + },
  78 + {
  79 + field: 'additionalInfo.description',
  80 + label: '描述',
  81 + },
  82 + ];
  83 +};
  1 +export { default as EdgeDeviceBasicInfo } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { Description, useDescription } from '/@/components/Description';
  3 + import { descSchema } from './config';
  4 + import type { PropType } from 'vue';
  5 + import { unref } from 'vue';
  6 + import edgeDevicePng from '/@/assets/images/edgeDevice.png';
  7 + import { Image, Popconfirm } from 'ant-design-vue';
  8 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  9 + import { getDeviceToken } from '/@/api/device/deviceManager';
  10 + import { useClipboard } from '@vueuse/core';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
  12 + import ManageDeviceTokenModal from '/@/views/device/list/cpns/modal/ManageDeviceTokenModal.vue';
  13 + import { useModal } from '/@/components/Modal';
  14 + import { edgeDeviceDeleteDistribution } from '/@/api/edgeManage/edgeInstance';
  15 + import { useGo } from '/@/hooks/web/usePage';
  16 +
  17 + const emits = defineEmits(['success']);
  18 +
  19 + const props = defineProps({
  20 + recordData: {
  21 + type: Object as PropType<EdgeDeviceItemType>,
  22 + default: () => {},
  23 + },
  24 + edgeId: {
  25 + type: String,
  26 + default: '',
  27 + },
  28 + });
  29 +
  30 + const CS = {
  31 + 'word-break': 'break-all',
  32 + overflow: 'hidden',
  33 + display: '-webkit-box',
  34 + '-webkit-line-clamp': 2,
  35 + '-webkit-box-orient': 'vertical',
  36 + };
  37 +
  38 + const [register] = useDescription({
  39 + layout: 'vertical',
  40 + schema: descSchema(),
  41 + column: 5,
  42 + });
  43 +
  44 + const { createMessage } = useMessage();
  45 +
  46 + const go = useGo();
  47 +
  48 + const { copied, copy } = useClipboard({ legacy: true });
  49 +
  50 + const handleEventIsCopyDeviceToken = async () => {
  51 + if (!props?.recordData?.id?.id) return;
  52 + const token = await getDeviceToken(props?.recordData?.id?.id);
  53 + if (token.credentialsType === 'ACCESS_TOKEN') {
  54 + await copy(token.credentialsId);
  55 + } else {
  56 + await copy(token.credentialsValue);
  57 + }
  58 + if (unref(copied)) {
  59 + createMessage.success('复制成功');
  60 + }
  61 + };
  62 +
  63 + const [registerModal, { openModal }] = useModal();
  64 +
  65 + const handleEventIsManageDeviceToken = async () => {
  66 + if (!props?.recordData?.id?.id) return;
  67 + const token = await getDeviceToken(props?.recordData?.id?.id);
  68 + openModal(true, token);
  69 + };
  70 +
  71 + const handleEventIsCancelDistribution = async () => {
  72 + await edgeDeviceDeleteDistribution(props.edgeId, props.recordData.id?.id);
  73 + createMessage.success('取消分配成功');
  74 + emits('success');
  75 + goBack();
  76 + };
  77 +
  78 + function goBack() {
  79 + go('/edge/edge_device/' + props.edgeId);
  80 + }
  81 +</script>
  82 +
  83 +<template>
  84 + <a-row :gutter="{ xs: 8, sm: 16, md: 24, lg: 32 }">
  85 + <a-col class="gutter-row" :span="3">
  86 + <div class="!flex flex-col justify-between items-center">
  87 + <div><Image :src="edgeDevicePng" :width="180" /></div>
  88 + <div class="!flex flex-col mt-3">
  89 + <span style="color: #1d2129" class="font-bold">{{
  90 + recordData?.alias ? recordData?.alias : recordData?.name
  91 + }}</span>
  92 + <span style="color: #3d3d3d">边缘设备详情</span>
  93 + </div>
  94 + </div>
  95 + </a-col>
  96 + <a-col class="gutter-row" :span="21">
  97 + <div class="!flex flex-col justify-between">
  98 + <Description v-if="recordData" @register="register" :data="recordData" :contentStyle="CS" />
  99 + <div class="!flex mt-3">
  100 + <a-button type="primary" @click="handleEventIsCopyDeviceToken">复制访问令牌</a-button>
  101 + <a-button class="ml-4" type="primary" @click="handleEventIsManageDeviceToken"
  102 + >管理凭证</a-button
  103 + >
  104 + <Popconfirm
  105 + title="您是否要取消分配边缘"
  106 + ok-text="是"
  107 + cancel-text="否"
  108 + @confirm="handleEventIsCancelDistribution"
  109 + >
  110 + <a-button class="ml-4" type="primary">取消分配边缘</a-button>
  111 + </Popconfirm>
  112 + </div>
  113 + </div>
  114 + </a-col>
  115 + </a-row>
  116 + <ManageDeviceTokenModal @register="registerModal" />
  117 +</template>
  118 +
  119 +<style lang="less" scoped>
  120 + :deep(.ant-image-img) {
  121 + height: 157px;
  122 + }
  123 +</style>
  1 +import { ModelOfMatterParams } from '/@/api/device/model/modelOfMatterModel';
  2 +import { getModelServices } from '/@/api/device/modelOfMatter';
  3 +import { FormProps, FormSchema, useComponentRegister } from '/@/components/Form';
  4 +import { validateTCPCustomCommand } from '/@/components/Form/src/components/ThingsModelForm';
  5 +import { JSONEditor, JSONEditorValidator } from '/@/components/CodeEditor';
  6 +import {
  7 + TransportTypeEnum,
  8 + CommandTypeNameEnum,
  9 + CommandTypeEnum,
  10 + ServiceCallTypeEnum,
  11 + CommandDeliveryWayEnum,
  12 + CommandDeliveryWayNameEnum,
  13 + TCPProtocolTypeEnum,
  14 +} from '/@/enums/deviceEnum';
  15 +import { DeviceTypeEnum } from '/@/api/device/model/deviceModel';
  16 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  17 +
  18 +export interface CommandDeliveryFormFieldType {
  19 + [CommandFieldsEnum.COMMAND_TYPE]: CommandTypeEnum;
  20 + [CommandFieldsEnum.TCP_COMMAND_VALUE]?: string;
  21 + [CommandFieldsEnum.COMAND_VALUE]?: string;
  22 + [CommandFieldsEnum.SERVICE]?: string;
  23 + [CommandFieldsEnum.MODEL_INPUT]?: ModelOfMatterParams;
  24 + [CommandFieldsEnum.CALL_TYPE]: CommandDeliveryWayEnum;
  25 +}
  26 +
  27 +export enum CommandFieldsEnum {
  28 + COMMAND_TYPE = 'commandType',
  29 + TCP_COMMAND_VALUE = 'tcpCommandValue',
  30 + COMAND_VALUE = 'commandValue',
  31 + SERVICE = 'service',
  32 + MODEL_INPUT = 'modelInput',
  33 + CALL_TYPE = 'callType',
  34 +
  35 + SERVICE_COMMAND = 'serviceCommand',
  36 +
  37 + SERVICE_OBJECT_MODEL = 'serviceObjectModel',
  38 +}
  39 +
  40 +useComponentRegister('JSONEditor', JSONEditor);
  41 +
  42 +export const CommandSchemas = (deviceRecord: EdgeDeviceItemType): FormSchema[] => {
  43 + const { deviceData, deviceProfileId, deviceType } = deviceRecord;
  44 +
  45 + const isTCPTransport = deviceData.transportConfiguration.type === TransportTypeEnum.TCP;
  46 +
  47 + const isTCPModbus =
  48 + isTCPTransport &&
  49 + deviceRecord.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  50 + TCPProtocolTypeEnum.MODBUS_RTU;
  51 +
  52 + return [
  53 + {
  54 + field: CommandFieldsEnum.COMMAND_TYPE,
  55 + component: 'RadioGroup',
  56 + label: '下发类型',
  57 + defaultValue: CommandTypeEnum.CUSTOM,
  58 + required: true,
  59 + componentProps: ({ formActionType }) => {
  60 + const { setFieldsValue } = formActionType;
  61 +
  62 + const getOptions = () => {
  63 + const options = [{ label: CommandTypeNameEnum.CUSTOM, value: CommandTypeEnum.CUSTOM }];
  64 +
  65 + if (isTCPModbus || (isTCPTransport && deviceType === DeviceTypeEnum.SENSOR))
  66 + return options;
  67 +
  68 + options.push({ label: CommandTypeNameEnum.SERVICE, value: CommandTypeEnum.SERVICE });
  69 +
  70 + return options;
  71 + };
  72 + return {
  73 + options: getOptions(),
  74 + onChange() {
  75 + setFieldsValue({
  76 + [CommandFieldsEnum.SERVICE]: null,
  77 + [CommandFieldsEnum.MODEL_INPUT]: null,
  78 + [CommandFieldsEnum.COMAND_VALUE]: null,
  79 + [CommandFieldsEnum.TCP_COMMAND_VALUE]: null,
  80 + });
  81 + },
  82 + };
  83 + },
  84 + },
  85 + {
  86 + field: CommandFieldsEnum.CALL_TYPE,
  87 + component: 'RadioGroup',
  88 + label: '单向/双向',
  89 + required: true,
  90 + defaultValue: CommandDeliveryWayEnum.ONE_WAY,
  91 + ifShow: ({ model }) => model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  92 + componentProps: {
  93 + options: Object.keys(CommandDeliveryWayEnum).map((key) => ({
  94 + label: CommandDeliveryWayNameEnum[key],
  95 + value: CommandDeliveryWayEnum[key],
  96 + })),
  97 + },
  98 + },
  99 + {
  100 + field: CommandFieldsEnum.TCP_COMMAND_VALUE,
  101 + label: '命令',
  102 + required: true,
  103 + ifShow: ({ model }) =>
  104 + deviceData.transportConfiguration.type === TransportTypeEnum.TCP &&
  105 + model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  106 + component: 'Input',
  107 + rules: [{ validator: validateTCPCustomCommand }],
  108 + componentProps: {
  109 + placeholder: '请输入命令',
  110 + },
  111 + },
  112 + {
  113 + field: CommandFieldsEnum.COMAND_VALUE,
  114 + label: '命令',
  115 + component: 'JSONEditor',
  116 + colProps: { span: 20 },
  117 + changeEvent: 'update:value',
  118 + valueField: 'value',
  119 + required: true,
  120 + rules: JSONEditorValidator(),
  121 + ifShow: ({ model }) =>
  122 + deviceData.transportConfiguration.type !== TransportTypeEnum.TCP &&
  123 + model[CommandFieldsEnum.COMMAND_TYPE] === CommandTypeEnum.CUSTOM,
  124 + componentProps: {
  125 + height: 250,
  126 + },
  127 + },
  128 + {
  129 + field: CommandFieldsEnum.SERVICE,
  130 + label: '服务',
  131 + component: 'ApiSelect',
  132 + required: true,
  133 + ifShow: ({ model }) => model[CommandFieldsEnum.COMMAND_TYPE] !== CommandTypeEnum.CUSTOM,
  134 + rules: [{ required: true, message: '请选择服务' }],
  135 + componentProps: ({ formActionType }) => {
  136 + const { setFieldsValue } = formActionType;
  137 + return {
  138 + api: getModelServices,
  139 + params: {
  140 + deviceProfileId: deviceProfileId.id,
  141 + },
  142 + valueField: 'identifier',
  143 + labelField: 'functionName',
  144 + getPopupContainer: () => document.body,
  145 + placeholder: '请选择服务',
  146 + onChange(
  147 + value: string,
  148 + options: ModelOfMatterParams & Record<'label' | 'value', string>
  149 + ) {
  150 + if (!value) return;
  151 + setFieldsValue({
  152 + [CommandFieldsEnum.CALL_TYPE]:
  153 + options.callType === ServiceCallTypeEnum.ASYNC
  154 + ? CommandDeliveryWayEnum.ONE_WAY
  155 + : CommandDeliveryWayEnum.TWO_WAY,
  156 + [CommandFieldsEnum.MODEL_INPUT]: null,
  157 + [CommandFieldsEnum.SERVICE_OBJECT_MODEL]: Object.assign(options, {
  158 + functionName: options.label,
  159 + identifier: options.value,
  160 + }),
  161 + });
  162 + },
  163 + };
  164 + },
  165 + },
  166 + {
  167 + field: CommandFieldsEnum.SERVICE_OBJECT_MODEL,
  168 + label: '服务物模型',
  169 + component: 'Input',
  170 + ifShow: false,
  171 + },
  172 + {
  173 + field: CommandFieldsEnum.MODEL_INPUT,
  174 + component: 'Input',
  175 + label: '输入参数',
  176 + changeEvent: 'update:value',
  177 + valueField: 'value',
  178 + ifShow: ({ model }) =>
  179 + model[CommandFieldsEnum.SERVICE] &&
  180 + model[CommandFieldsEnum.SERVICE_OBJECT_MODEL] &&
  181 + model[CommandFieldsEnum.COMMAND_TYPE] !== CommandTypeEnum.CUSTOM,
  182 + componentProps: {
  183 + formProps: {
  184 + wrapperCol: { span: 24 },
  185 + } as FormProps,
  186 + },
  187 + slot: 'serviceCommand',
  188 + },
  189 + ];
  190 +};
  1 +export { default as CommandDeliveryModal } from './index.vue';
  1 +<template>
  2 + <BasicModal
  3 + title="命令下发"
  4 + :width="650"
  5 + @register="registerModal"
  6 + @ok="handleOk"
  7 + @cancel="handleCancel"
  8 + >
  9 + <BasicForm @register="registerForm">
  10 + <template #serviceCommand="{ field, model }">
  11 + <ThingsModelForm
  12 + :disabled="
  13 + deviceDetail?.deviceData?.transportConfiguration?.type === TransportTypeEnum.TCP
  14 + "
  15 + ref="thingsModelFormRef"
  16 + v-model:value="model[field]"
  17 + :key="model[CommandFieldsEnum.SERVICE_OBJECT_MODEL]?.identifier"
  18 + :inputData="model[CommandFieldsEnum.SERVICE_OBJECT_MODEL]?.functionJson?.inputData"
  19 + :transportType="deviceDetail?.deviceData?.transportConfiguration?.type"
  20 + />
  21 + </template>
  22 + </BasicForm>
  23 + </BasicModal>
  24 +</template>
  25 +<script lang="ts" setup>
  26 + import { nextTick, ref, unref } from 'vue';
  27 + import { BasicForm, ThingsModelForm, useForm } from '/@/components/Form';
  28 + import { CommandDeliveryFormFieldType, CommandFieldsEnum, CommandSchemas } from './config';
  29 + import { useMessage } from '/@/hooks/web/useMessage';
  30 + import { CommandDeliveryWayEnum } from '/@/enums/deviceEnum';
  31 + import { CommandTypeEnum, RPCCommandMethodEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  32 + import { BasicModal, useModalInner } from '/@/components/Modal';
  33 + import { getDeviceActiveTime } from '/@/api/alarm/position';
  34 + import { RpcCommandType } from '/@/api/device/model/deviceConfigModel';
  35 + import { parseStringToJSON } from '/@/components/CodeEditor';
  36 + import { commandIssuanceApi } from '/@/api/device/deviceManager';
  37 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  38 +
  39 + defineEmits(['register']);
  40 +
  41 + const thingsModelFormRef = ref<InstanceType<typeof ThingsModelForm>>();
  42 + const props = defineProps<{
  43 + deviceDetail: EdgeDeviceItemType;
  44 + }>();
  45 +
  46 + const [registerModal, { setModalProps }] = useModalInner(
  47 + (params: ModalParamsType<EdgeDeviceItemType>) => {
  48 + const { record } = params;
  49 + setProps({
  50 + schemas: CommandSchemas(record),
  51 + });
  52 + }
  53 + );
  54 +
  55 + const { createMessage } = useMessage();
  56 +
  57 + const [registerForm, { setProps, getFieldsValue, validate, resetFields, clearValidate }] =
  58 + useForm({
  59 + labelWidth: 120,
  60 + baseColProps: { span: 20 },
  61 + labelAlign: 'right',
  62 + showSubmitButton: false,
  63 + showResetButton: false,
  64 + });
  65 +
  66 + const handleCancel = async () => {
  67 + await resetFields();
  68 + await nextTick();
  69 + await clearValidate();
  70 + };
  71 +
  72 + const handleValidate = async () => {
  73 + await validate();
  74 + await unref(thingsModelFormRef)?.validate?.();
  75 + };
  76 +
  77 + const handleValidateDeviceActive = async (): Promise<boolean> => {
  78 + const result = await getDeviceActiveTime(unref(props.deviceDetail)!.id?.id);
  79 + const [firstItem] = result;
  80 + return !!firstItem.value;
  81 + };
  82 +
  83 + const handleCommandParams = (
  84 + values: CommandDeliveryFormFieldType,
  85 + serviceCommand: Recordable
  86 + ) => {
  87 + const { commandType, service } = values;
  88 +
  89 + const isTcpDevice =
  90 + unref(props.deviceDetail)?.deviceData?.transportConfiguration?.type === TransportTypeEnum.TCP;
  91 + if (commandType === CommandTypeEnum.CUSTOM) {
  92 + if (isTcpDevice) {
  93 + const value = values.tcpCommandValue;
  94 + return value?.replaceAll(/\s/g, '');
  95 + }
  96 + return parseStringToJSON(values.commandValue!).json;
  97 + } else {
  98 + if (isTcpDevice) return Reflect.get(serviceCommand, CommandFieldsEnum.SERVICE_COMMAND);
  99 + return {
  100 + [service!]: serviceCommand,
  101 + };
  102 + }
  103 + };
  104 +
  105 + const handleOk = async () => {
  106 + await handleValidate();
  107 +
  108 + try {
  109 + setModalProps({ loading: true, confirmLoading: true });
  110 +
  111 + const values = getFieldsValue() as CommandDeliveryFormFieldType;
  112 + const { callType, commandType } = values;
  113 + const serviceCommand = unref(thingsModelFormRef)?.getFieldsValue() || {};
  114 +
  115 + if (callType === CommandDeliveryWayEnum.TWO_WAY && !(await handleValidateDeviceActive())) {
  116 + createMessage.warn('当前设备不在线');
  117 + return;
  118 + }
  119 +
  120 + const rpcCommands: RpcCommandType = {
  121 + additionalInfo: {
  122 + cmdType:
  123 + commandType === CommandTypeEnum.CUSTOM
  124 + ? CommandTypeEnum.CUSTOM
  125 + : CommandTypeEnum.SERVICE,
  126 + },
  127 + method: RPCCommandMethodEnum.THINGSKIT,
  128 + persistent: true,
  129 + params: handleCommandParams(values, serviceCommand),
  130 + };
  131 +
  132 + await commandIssuanceApi(callType, unref(props.deviceDetail)!.id?.id, rpcCommands);
  133 +
  134 + createMessage.success('命令下发成功');
  135 + } finally {
  136 + setModalProps({ loading: false, confirmLoading: false });
  137 + }
  138 + };
  139 +</script>
  140 +<style scoped lang="less"></style>
  1 +import { BasicColumn, FormSchema } from '/@/components/Table';
  2 +import moment from 'moment';
  3 +import { Tag } from 'ant-design-vue';
  4 +import { h } from 'vue';
  5 +
  6 +// 表格配置
  7 +export const columns: BasicColumn[] = [
  8 + {
  9 + title: '命令下发时间',
  10 + dataIndex: 'createTime',
  11 + format: (text) => {
  12 + return moment(text).format('YYYY-MM-DD HH:mm:ss');
  13 + },
  14 + },
  15 + {
  16 + title: '命令类型',
  17 + dataIndex: 'additionalInfo.cmdType',
  18 + format: (text) => {
  19 + return h(
  20 + Tag,
  21 + {
  22 + color: Number(text) === 1 ? 'green' : 'blue',
  23 + },
  24 + () => (Number(text) === 1 ? '服务' : '自定义')
  25 + ) as unknown as any;
  26 + },
  27 + },
  28 + {
  29 + title: '响应类型',
  30 + dataIndex: 'request.oneway',
  31 + format: (text) => {
  32 + return !text ? '双向' : '单向';
  33 + },
  34 + },
  35 + {
  36 + title: '命令状态',
  37 + dataIndex: 'status',
  38 + customRender: ({ text, record }) => {
  39 + return h(
  40 + Tag,
  41 + {
  42 + color:
  43 + text == 'EXPIRED'
  44 + ? 'red'
  45 + : text == 'DELIVERED'
  46 + ? 'blue'
  47 + : text == 'QUEUED'
  48 + ? '#00C9A7'
  49 + : text == 'TIMEOUT'
  50 + ? 'red'
  51 + : text == 'SENT'
  52 + ? '#00C9A7'
  53 + : text == 'FAILED'
  54 + ? 'red'
  55 + : text == 'SUCCESSFUL'
  56 + ? 'green'
  57 + : 'red',
  58 + },
  59 + () => record?.statusName
  60 + );
  61 + },
  62 + },
  63 + {
  64 + title: '响应内容',
  65 + dataIndex: 'response111',
  66 + slots: { customRender: 'responseContent' },
  67 + },
  68 + {
  69 + title: '命令内容',
  70 + dataIndex: 'request.body',
  71 + slots: { customRender: 'recordContent' },
  72 + },
  73 +];
  74 +
  75 +// 表格查询表单
  76 +export const searchFormSchema: FormSchema[] = [
  77 + {
  78 + field: 'status',
  79 + label: '命令状态',
  80 + component: 'Select',
  81 + colProps: { span: 6 },
  82 + componentProps: {
  83 + options: [
  84 + {
  85 + label: '队列中',
  86 + value: 'QUEUED',
  87 + },
  88 + {
  89 + label: '已发送',
  90 + value: 'SENT',
  91 + },
  92 + {
  93 + label: '发送成功',
  94 + value: 'DELIVERED',
  95 + },
  96 +
  97 + {
  98 + label: '响应成功',
  99 + value: 'SUCCESSFUL',
  100 + },
  101 + {
  102 + label: '超时',
  103 + value: 'TIMEOUT',
  104 + },
  105 + {
  106 + label: '已过期',
  107 + value: 'EXPIRED',
  108 + },
  109 + {
  110 + label: '响应失败',
  111 + value: 'FAILED',
  112 + },
  113 + {
  114 + label: '已删除',
  115 + value: 'DELETED',
  116 + },
  117 + ],
  118 + placeholder: '请选择命令状态',
  119 + },
  120 + },
  121 + {
  122 + field: 'sendTime',
  123 + label: '命令下发时间',
  124 + component: 'RangePicker',
  125 + componentProps: {
  126 + showTime: {
  127 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  128 + },
  129 + },
  130 + colProps: { span: 10 },
  131 + },
  132 +];
  1 +export { default as EdgeDeviceCommand } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns, searchFormSchema } from './config';
  4 + import { JsonPreview } from '/@/components/CodeEditor';
  5 + import { Button, Modal, Space } from 'ant-design-vue';
  6 + import { h } from 'vue';
  7 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 + import { deviceCommandRecordGetQuery } from '/@/api/device/deviceConfigApi';
  9 + import { useModal } from '/@/components/Modal';
  10 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  11 + import { CommandDeliveryModal } from './CommandDeliveryModal';
  12 +
  13 + const props = defineProps({
  14 + recordData: {
  15 + type: Object as PropType<EdgeDeviceItemType>,
  16 + default: () => {},
  17 + },
  18 + });
  19 +
  20 + defineEmits(['register']);
  21 +
  22 + const [registerCommandDeliverModal, { openModal }] = useModal();
  23 +
  24 + const [registerTable] = useTable({
  25 + columns,
  26 + api: deviceCommandRecordGetQuery,
  27 + beforeFetch: (params) => {
  28 + return {
  29 + ...params,
  30 + tbDeviceId: props?.recordData?.id?.id,
  31 + };
  32 + },
  33 + formConfig: {
  34 + labelWidth: 120,
  35 + schemas: searchFormSchema,
  36 + fieldMapToTime: [['sendTime', ['startTime', 'endTime'], 'x']],
  37 + },
  38 + useSearchForm: true,
  39 + showIndexColumn: false,
  40 + clickToRowSelect: false,
  41 + showTableSetting: true,
  42 + bordered: true,
  43 + rowKey: 'id',
  44 + });
  45 +
  46 + const commonModalInfo = (title, value) => {
  47 + Modal.info({
  48 + title,
  49 + width: 600,
  50 + content: h(JsonPreview, { data: value }),
  51 + });
  52 + };
  53 +
  54 + const handleRecordContent = (record) => {
  55 + if (!record?.request?.body) return;
  56 + if (Object.prototype.toString.call(record?.request?.body) !== '[object Object]') return;
  57 + const jsonParams = record?.request?.body?.params;
  58 + commonModalInfo('命令下发内容', jsonParams);
  59 + };
  60 +
  61 + const handleRecordResponseContent = (record) => {
  62 + const jsonParams = record?.response;
  63 + commonModalInfo('响应内容', jsonParams);
  64 + };
  65 +
  66 + const handleEventIsCommand = () => {
  67 + openModal(true, {
  68 + mode: DataActionModeEnum.READ,
  69 + record: props.recordData,
  70 + } as ModalParamsType<EdgeDeviceItemType>);
  71 + };
  72 +</script>
  73 +
  74 +<template>
  75 + <div>
  76 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  77 + <template #toolbar>
  78 + <Space>
  79 + <Button type="primary" @click="handleEventIsCommand">命令下发</Button>
  80 + </Space>
  81 + </template>
  82 + <template #recordContent="{ record }">
  83 + <Button type="link" class="ml-2" @click="handleRecordContent(record)"> 查看 </Button>
  84 + </template>
  85 + <template #responseContent="{ record }">
  86 + <div v-if="!record.request?.oneway">
  87 + <Button v-if="record?.response === null" type="text" class="ml-2"> 未响应 </Button>
  88 + <Button v-else type="link" class="ml-2" @click="handleRecordResponseContent(record)">
  89 + 查看
  90 + </Button>
  91 + </div>
  92 + <div v-else>--</div>
  93 + </template>
  94 + </BasicTable>
  95 + <CommandDeliveryModal @register="registerCommandDeliverModal" :deviceDetail="recordData" />
  96 + </div>
  97 +</template>
  1 +export { default as EdgeDeviceDistribution } from './index.vue';
  1 +<template>
  2 + <BasicModal
  3 + v-bind="$attrs"
  4 + @register="registerModal"
  5 + title="将设备分配给边缘"
  6 + :canFullscreen="false"
  7 + centered
  8 + :minHeight="300"
  9 + @ok="handleEventIsDistribution"
  10 + @cancel="handleEventIsCancel"
  11 + okText="分配"
  12 + >
  13 + <div class="!flex items-center gap-5">
  14 + <span>分配设备</span>
  15 + <Select
  16 + v-if="edgeId"
  17 + placeholder="请选择设备"
  18 + v-model:value="deviceIds"
  19 + class="!w-1/2"
  20 + :options="selectOptions"
  21 + v-bind="createPickerSearch()"
  22 + mode="multiple"
  23 + allowClear
  24 + />
  25 + </div>
  26 + </BasicModal>
  27 +</template>
  28 +
  29 +<script lang="ts" setup>
  30 + import { BasicModal, useModalInner } from '/@/components/Modal';
  31 + import { edgeDeviceDistribution } from '/@/api/edgeManage/edgeInstance';
  32 + import { useMessage } from '/@/hooks/web/useMessage';
  33 + import { onMounted, ref } from 'vue';
  34 + import { edgeDeviceDistributionPage } from '/@/api/edgeManage/edgeInstance';
  35 + import { Select } from 'ant-design-vue';
  36 + import { isArray } from '/@/utils/is';
  37 + import { createPickerSearch } from '/@/utils/pickerSearch';
  38 +
  39 + const emits = defineEmits(['success', 'register']);
  40 +
  41 + const { createMessage } = useMessage();
  42 +
  43 + const props = defineProps({
  44 + edgeId: {
  45 + type: String,
  46 + default: '',
  47 + },
  48 + });
  49 +
  50 + const [registerModal, { closeModal }] = useModalInner((data) => {
  51 + const { _ } = data;
  52 + deviceIds.value = [];
  53 + handleGetDeviceDistributionList();
  54 + });
  55 +
  56 + const selectOptions = ref<{ label: string; value: string }[]>([]);
  57 +
  58 + const deviceIds = ref<string[]>([]);
  59 +
  60 + const handleGetDeviceDistributionList = async () => {
  61 + const { data } = await edgeDeviceDistributionPage({ page: 0, pageSize: 50 });
  62 + selectOptions.value = data?.map((dataItem: Recordable) => ({
  63 + label: dataItem.alias || dataItem.name,
  64 + value: dataItem.id.id,
  65 + })) as { label: string; value: string }[];
  66 + };
  67 +
  68 + onMounted(() => {
  69 + handleGetDeviceDistributionList();
  70 + });
  71 +
  72 + const handleEventIsDistribution = async () => {
  73 + if (!props.edgeId) return;
  74 + if (!deviceIds.value) return createMessage.error('请选择设备');
  75 + if (isArray(deviceIds.value) && deviceIds.value.length === 0)
  76 + return createMessage.error('请选择设备');
  77 + for (let item of deviceIds.value) await edgeDeviceDistribution(props?.edgeId, item);
  78 + createMessage.success('分配成功');
  79 + handleEventIsCancel();
  80 + emits('success');
  81 + handleGetDeviceDistributionList();
  82 + };
  83 +
  84 + const handleEventIsCancel = () => {
  85 + deviceIds.value.length = 0;
  86 + closeModal();
  87 + };
  88 +</script>
  1 +import moment from 'moment';
  2 +import { findDictItemByCode } from '/@/api/system/dict';
  3 +import { BasicColumn, FormSchema } from '/@/components/Table';
  4 +import {
  5 + EventType,
  6 + EventTypeColor,
  7 + EventTypeName,
  8 +} from '/@/views/device/list/cpns/tabs/EventManage/config';
  9 +import { Tag } from 'ant-design-vue';
  10 +import { h } from 'vue';
  11 +import { formatToDateTime } from '/@/utils/dateUtil';
  12 +
  13 +// 表格配置
  14 +export const columns: BasicColumn[] = [
  15 + {
  16 + title: '时间',
  17 + dataIndex: 'eventTime',
  18 + format(text) {
  19 + return formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss');
  20 + },
  21 + },
  22 + {
  23 + title: '标识符',
  24 + dataIndex: 'eventIdentifier',
  25 + },
  26 + {
  27 + title: '事件名称',
  28 + dataIndex: 'eventName',
  29 + },
  30 + {
  31 + title: '事件类型',
  32 + dataIndex: 'eventType',
  33 + customRender({ text }) {
  34 + return h(
  35 + Tag,
  36 + {
  37 + color: EventTypeColor[text as EventType],
  38 + },
  39 + () => EventTypeName[text as EventType]
  40 + );
  41 + },
  42 + },
  43 + {
  44 + title: '输出参数',
  45 + dataIndex: 'outputParams',
  46 + slots: { customRender: 'outputParams' },
  47 + },
  48 +];
  49 +
  50 +export const formSchemas: FormSchema[] = [
  51 + {
  52 + field: 'eventIdentifier',
  53 + label: '标识符',
  54 + component: 'Input',
  55 + componentProps: {
  56 + placeholder: '请输入标识符',
  57 + },
  58 + colProps: { span: 6 },
  59 + },
  60 + {
  61 + field: 'eventType',
  62 + label: '事件类型',
  63 + component: 'ApiSelect',
  64 + componentProps: {
  65 + placeholder: '请选择事件类型',
  66 + api: findDictItemByCode,
  67 + params: {
  68 + dictCode: 'event_type',
  69 + },
  70 + labelField: 'itemText',
  71 + valueField: 'itemValue',
  72 + },
  73 + colProps: { span: 6 },
  74 + },
  75 + {
  76 + field: 'dateRange',
  77 + label: '时间范围',
  78 + component: 'RangePicker',
  79 + componentProps: {
  80 + showTime: {
  81 + defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
  82 + },
  83 + },
  84 + colProps: { span: 10 },
  85 + },
  86 +];
  1 +export { default as EdgeDeviceEvent } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns, formSchemas } from './config';
  4 + import { BasicModal, useModal } from '/@/components/Modal';
  5 + import { Input } from 'ant-design-vue';
  6 + import { ref } from 'vue';
  7 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 + import { EventManageRequest } from '/@/api/device/model/eventManageModel';
  9 + import { getEventManageListRecord } from '/@/api/device/eventManage';
  10 + import { EyeOutlined } from '@ant-design/icons-vue';
  11 +
  12 + const props = defineProps({
  13 + recordData: {
  14 + type: Object as PropType<EdgeDeviceItemType>,
  15 + default: () => {},
  16 + },
  17 + });
  18 +
  19 + const outputData = ref<string>();
  20 +
  21 + const [registerTable] = useTable({
  22 + columns,
  23 + api: getEventManageListRecord,
  24 + fetchSetting: {
  25 + totalField: 'totalElements',
  26 + listField: 'data',
  27 + },
  28 + beforeFetch: (params: EventManageRequest) => {
  29 + const page = params.page - 1 < 0 ? 0 : params.page - 1;
  30 + const _params = Object.keys(params)
  31 + .filter((key) => params[key])
  32 + .reduce((prev, next) => ({ ...prev, [next]: params[next] }), {});
  33 + return {
  34 + ..._params,
  35 + page,
  36 + startTime: params.startTime ? new Date(params.startTime).getTime() : params.startTime,
  37 + endTime: params.endTime ? new Date(params.endTime).getTime() : params.endTime,
  38 + tbDeviceId: props?.recordData?.id?.id,
  39 + };
  40 + },
  41 + formConfig: {
  42 + labelWidth: 80,
  43 + schemas: formSchemas,
  44 + fieldMapToTime: [['dateRange', ['startTime', 'endTime'], 'YYYY-MM-DD HH:mm:ss']],
  45 + },
  46 + useSearchForm: true,
  47 + showIndexColumn: false,
  48 + clickToRowSelect: false,
  49 + showTableSetting: true,
  50 + bordered: true,
  51 + rowKey: 'id',
  52 + });
  53 +
  54 + const [registerModal, { openModal, closeModal }] = useModal();
  55 +
  56 + const handleViewDetail = (record: Record<'eventValue', Recordable>) => {
  57 + outputData.value = JSON.stringify(record.eventValue, null, 2);
  58 + openModal(true);
  59 + };
  60 +</script>
  61 +
  62 +<template>
  63 + <div>
  64 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  65 + <template #outputParams="{ record }">
  66 + <span class="cursor-pointer text-blue-500" @click="handleViewDetail(record)">
  67 + <EyeOutlined class="svg:text-blue-500" />
  68 + <span class="ml-2">详情</span>
  69 + </span>
  70 + </template>
  71 + </BasicTable>
  72 + <BasicModal title="输出参数" @register="registerModal" @ok="closeModal">
  73 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  74 + </BasicModal>
  75 + </div>
  76 +</template>
  1 +<script lang="ts" setup>
  2 + import { computed, nextTick, onMounted, onUnmounted, Ref, ref, unref } from 'vue';
  3 + import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  4 + import { Empty, Spin } from 'ant-design-vue';
  5 + import { useECharts } from '/@/hooks/web/useECharts';
  6 + import { AggregateDataEnum, selectDeviceAttrSchema } from '/@/views/device/localtion/config.data';
  7 + import { useTimePeriodForm } from '/@/views/device/localtion/cpns/TimePeriodForm';
  8 + import {
  9 + defaultSchemas,
  10 + OrderByEnum,
  11 + } from '/@/views/device/localtion/cpns/TimePeriodForm/config';
  12 + import TimePeriodForm from '/@/views/device/localtion/cpns/TimePeriodForm/TimePeriodForm.vue';
  13 + import { useGridLayout } from '/@/hooks/component/useGridLayout';
  14 + import { ColEx } from '/@/components/Form/src/types';
  15 + import { useHistoryData } from './hook/useHistoryData';
  16 + import { formatToDateTime } from '/@/utils/dateUtil';
  17 + import { useTable, BasicTable, BasicColumn, SorterResult } from '/@/components/Table';
  18 + import {
  19 + ModeSwitchButton,
  20 + TABLE_CHART_MODE_LIST,
  21 + EnumTableChartMode,
  22 + } from '/@/components/Widget';
  23 + import { SchemaFiled } from '/@/views/visual/palette/components/HistoryTrendModal/config';
  24 + import { DataTypeEnum } from '/@/enums/objectModelEnum';
  25 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  26 +
  27 + // interface DeviceDetail {
  28 + // tbDeviceId: string;
  29 + // deviceProfileId: string;
  30 + // }
  31 +
  32 + const props = defineProps<{
  33 + deviceDetail: EdgeDeviceItemType;
  34 + attr?: string;
  35 + }>();
  36 +
  37 + const mode = ref<EnumTableChartMode>(EnumTableChartMode.CHART);
  38 +
  39 + const chartRef = ref();
  40 +
  41 + const loading = ref(false);
  42 +
  43 + const isNull = ref(false);
  44 +
  45 + const historyData = ref<{ ts: number; value: string; name: string }[]>([]);
  46 +
  47 + const { deviceAttrs, getDeviceKeys, getSearchParams, setChartOptions, getDeviceAttribute } =
  48 + useHistoryData();
  49 +
  50 + const getIdentifierNameMapping = computed(() => {
  51 + const mapping = {};
  52 + unref(deviceAttrs).forEach((item) => {
  53 + const { identifier, name } = item;
  54 + mapping[identifier] = name;
  55 + });
  56 + return mapping;
  57 + });
  58 +
  59 + function hasDeviceAttr() {
  60 + if (!unref(deviceAttrs).length) {
  61 + return false;
  62 + } else {
  63 + return true;
  64 + }
  65 + }
  66 +
  67 + const sortOrder = ref<any>('descend');
  68 + const columns = computed<BasicColumn[]>(() => {
  69 + return [
  70 + {
  71 + title: '属性',
  72 + dataIndex: 'name',
  73 + format: (text) => {
  74 + return unref(getIdentifierNameMapping)[text];
  75 + },
  76 + },
  77 + {
  78 + title: '值',
  79 + dataIndex: 'value',
  80 + },
  81 + {
  82 + title: '更新时间',
  83 + dataIndex: 'ts',
  84 + format: (val) => {
  85 + return formatToDateTime(val, 'YYYY-MM-DD HH:mm:ss');
  86 + },
  87 + sorter: true,
  88 + sortOrder: unref(sortOrder),
  89 + sortDirections: ['descend', 'ascend', 'descend'],
  90 + },
  91 + ];
  92 + });
  93 +
  94 + const [registerTable, { setColumns }] = useTable({
  95 + showIndexColumn: false,
  96 + showTableSetting: false,
  97 + dataSource: historyData,
  98 + maxHeight: 300,
  99 + columns: unref(columns),
  100 + size: 'small',
  101 + });
  102 +
  103 + const getTableList = async (orderBy?: OrderByEnum) => {
  104 + // 表单验证
  105 + await method.validate();
  106 + const value = method.getFieldsValue();
  107 + const searchParams = getSearchParams(value);
  108 +
  109 + if (!hasDeviceAttr()) return;
  110 + // 发送请求
  111 + loading.value = true;
  112 + const res = await getDeviceHistoryInfo(
  113 + {
  114 + ...searchParams,
  115 + entityId: props.deviceDetail?.id?.id,
  116 + },
  117 + orderBy
  118 + );
  119 + historyData.value = getTableHistoryData(res);
  120 + loading.value = false;
  121 + };
  122 +
  123 + const handleTableChange = async (_pag, _filters, sorter: SorterResult) => {
  124 + sortOrder.value = sorter.order;
  125 + await setColumns(unref(columns));
  126 + if (sorter.field == 'ts') {
  127 + if (sorter.order == 'descend') {
  128 + getTableList(OrderByEnum.DESC);
  129 + } else {
  130 + getTableList(OrderByEnum.ASC);
  131 + }
  132 + }
  133 + };
  134 +
  135 + const { setOptions, getInstance } = useECharts(chartRef as Ref<HTMLDivElement>);
  136 +
  137 + const [register, method] = useTimePeriodForm({
  138 + schemas: [...defaultSchemas, ...selectDeviceAttrSchema],
  139 + baseColProps: useGridLayout(2, 3, 4) as unknown as ColEx,
  140 + async submitFunc() {
  141 + // 表单验证
  142 + await method.validate();
  143 + const value = method.getFieldsValue();
  144 + const searchParams = getSearchParams(value);
  145 +
  146 + if (!hasDeviceAttr()) return;
  147 + // 发送请求
  148 + loading.value = true;
  149 + const res = await getDeviceHistoryInfo(
  150 + {
  151 + ...searchParams,
  152 + entityId: props.deviceDetail?.id?.id,
  153 + },
  154 + searchParams.orderBy as OrderByEnum
  155 + );
  156 + historyData.value = getTableHistoryData(res);
  157 + loading.value = false;
  158 + // 判断数据对象是否为空
  159 + if (!Object.keys(res).length) {
  160 + isNull.value = false;
  161 + return;
  162 + } else {
  163 + isNull.value = true;
  164 + }
  165 +
  166 + const selectedKeys = unref(deviceAttrs).find(
  167 + (item) => item.identifier === value[SchemaFiled.KEYS]
  168 + );
  169 +
  170 + setOptions(setChartOptions(res, selectedKeys));
  171 + },
  172 + });
  173 +
  174 + const getTableHistoryData = (record: Recordable<{ ts: number; value: string }[]>) => {
  175 + const keys = Object.keys(record);
  176 + const list = keys.reduce((prev, next) => {
  177 + const list = record[next].map((item) => {
  178 + return {
  179 + ...item,
  180 + name: next,
  181 + };
  182 + });
  183 + return [...prev, ...list];
  184 + }, []);
  185 + return list;
  186 + };
  187 +
  188 + const getDeviceDataKey = async () => {
  189 + try {
  190 + await getDeviceAttribute(props.deviceDetail);
  191 + if (props.attr) {
  192 + method.setFieldsValue({ keys: props.attr });
  193 + const attrInfo = unref(deviceAttrs).find((item) => item.identifier === props.attr);
  194 + if (
  195 + [DataTypeEnum.STRING, DataTypeEnum.STRUCT].includes(
  196 + attrInfo?.detail.dataType.type as unknown as DataTypeEnum
  197 + )
  198 + ) {
  199 + mode.value = EnumTableChartMode.TABLE;
  200 + } else {
  201 + mode.value = EnumTableChartMode.CHART;
  202 + }
  203 + }
  204 + } catch (error) {}
  205 + };
  206 +
  207 + const openHistoryPanel = async (orderBy = OrderByEnum.ASC) => {
  208 + await nextTick();
  209 + method.updateSchema({
  210 + field: 'keys',
  211 + componentProps: {
  212 + options: unref(deviceAttrs).map((item) => ({ label: item.name, value: item.identifier })),
  213 + },
  214 + });
  215 +
  216 + method.setFieldsValue({
  217 + [SchemaFiled.START_TS]: 1 * 24 * 60 * 60 * 1000,
  218 + [SchemaFiled.LIMIT]: 7,
  219 + [SchemaFiled.AGG]: AggregateDataEnum.NONE,
  220 + });
  221 +
  222 + if (!hasDeviceAttr()) return;
  223 +
  224 + const keys = props.attr ? props.attr : unref(getDeviceKeys).join();
  225 +
  226 + const res = await getDeviceHistoryInfo(
  227 + {
  228 + entityId: props.deviceDetail?.id?.id,
  229 + keys,
  230 + startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
  231 + endTs: Date.now(),
  232 + agg: AggregateDataEnum.NONE,
  233 + limit: 7,
  234 + },
  235 + orderBy
  236 + );
  237 + historyData.value = getTableHistoryData(res);
  238 +
  239 + // 判断对象是否为空
  240 + if (!Object.keys(res).length) {
  241 + isNull.value = false;
  242 + return;
  243 + } else {
  244 + isNull.value = true;
  245 + }
  246 + const selectedKeys = unref(deviceAttrs).find((item) => item.identifier === props.attr);
  247 +
  248 + setOptions(setChartOptions(res, selectedKeys));
  249 + };
  250 +
  251 + const switchMode = async (flag: EnumTableChartMode) => {
  252 + mode.value = flag;
  253 + };
  254 +
  255 + onMounted(async () => {
  256 + await getDeviceDataKey();
  257 + await openHistoryPanel();
  258 + });
  259 +
  260 + onUnmounted(() => {
  261 + getInstance()?.clear();
  262 + });
  263 +</script>
  264 +
  265 +<template>
  266 + <section class="flex flex-col p-4 h-full" style="background-color: #f0f2f5">
  267 + <section class="bg-white p-3 mb-4">
  268 + <TimePeriodForm @register="register" />
  269 + </section>
  270 + <section class="bg-white p-3">
  271 + <Spin :spinning="loading" :absolute="true">
  272 + <div
  273 + v-show="mode === EnumTableChartMode.CHART"
  274 + class="flex h-70px items-center justify-end p-2"
  275 + >
  276 + <ModeSwitchButton
  277 + v-model:value="mode"
  278 + :mode="TABLE_CHART_MODE_LIST"
  279 + @change="switchMode"
  280 + />
  281 + </div>
  282 + <div
  283 + v-show="isNull && mode === EnumTableChartMode.CHART"
  284 + ref="chartRef"
  285 + :style="{ height: '400px', width: '100%' }"
  286 + >
  287 + </div>
  288 + <Empty v-show="!isNull && mode === EnumTableChartMode.CHART" />
  289 +
  290 + <BasicTable
  291 + @change="handleTableChange"
  292 + v-show="mode === EnumTableChartMode.TABLE"
  293 + @register="registerTable"
  294 + >
  295 + <template #toolbar>
  296 + <div class="flex h-70px items-center justify-end p-2">
  297 + <ModeSwitchButton
  298 + v-model:value="mode"
  299 + :mode="TABLE_CHART_MODE_LIST"
  300 + @change="switchMode"
  301 + />
  302 + </div>
  303 + </template>
  304 + </BasicTable>
  305 + </Spin>
  306 + </section>
  307 + </section>
  308 +</template>
  309 +
  310 +<style scoped></style>
  1 +import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  2 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  3 +import { DataType, Specs } from '/@/api/device/model/modelOfMatterModel';
  4 +import { TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  5 +import { DataTypeEnum } from '/@/enums/objectModelEnum';
  6 +import { isArray } from '/@/utils/is';
  7 +
  8 +export interface StructValueItemType extends BaseAdditionalInfo {
  9 + name: string;
  10 + key: string;
  11 + value?: string | number | boolean;
  12 +}
  13 +
  14 +export interface BaseAdditionalInfo {
  15 + unit?: string;
  16 + boolClose?: string;
  17 + boolOpen?: string;
  18 + unitName?: string;
  19 +}
  20 +
  21 +export interface SocketInfoDataSourceItemType extends BaseAdditionalInfo {
  22 + accessMode: string;
  23 + key: string;
  24 + name: string;
  25 + type: DataTypeEnum;
  26 + detail: DeviceModelOfMatterAttrs;
  27 + time?: number;
  28 + value?: string | number | boolean | Record<string, StructValueItemType>;
  29 + expand?: boolean;
  30 + showHistoryDataButton?: boolean;
  31 + rawValue?: any;
  32 + enum?: Record<string, string>;
  33 +}
  34 +
  35 +export function buildTableDataSourceByObjectModel(
  36 + models: DeviceModelOfMatterAttrs[],
  37 + deviceDetail: EdgeDeviceItemType
  38 +): SocketInfoDataSourceItemType[] {
  39 + const isTCPTransportType =
  40 + deviceDetail.deviceData.transportConfiguration.type === TransportTypeEnum.TCP;
  41 +
  42 + const isModbusDevice =
  43 + isTCPTransportType &&
  44 + deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  45 + TCPProtocolTypeEnum.MODBUS_RTU;
  46 +
  47 + function getAdditionalInfoByDataType(dataType?: DataType) {
  48 + const { specs, specsList, type } = dataType || {};
  49 + if (isArray(specs)) return {};
  50 + const { unit, boolClose, boolOpen, unitName } = (specs as Partial<Specs>) || {};
  51 + const result = { unit, boolClose, boolOpen, unitName };
  52 + if ((type == DataTypeEnum.ENUM && specsList && specsList.length) || isModbusDevice) {
  53 + Reflect.set(
  54 + result,
  55 + 'enum',
  56 + (specsList || []).reduce((prev, next) => ({ ...prev, [next.value!]: next.name }), {})
  57 + );
  58 + }
  59 +
  60 + if (isModbusDevice) {
  61 + result.boolClose = '关';
  62 + result.boolOpen = '开';
  63 + }
  64 +
  65 + return result;
  66 + }
  67 +
  68 + return models.map((item) => {
  69 + const { accessMode, identifier, name, detail } = item;
  70 + const { dataType } = detail;
  71 + const { type, specs } = dataType || {};
  72 +
  73 + const res = {
  74 + accessMode,
  75 + name,
  76 + key: identifier,
  77 + type: type!,
  78 + detail: item,
  79 + };
  80 +
  81 + if (isArray(specs) && type === DataTypeEnum.STRUCT) {
  82 + const value: Record<string, StructValueItemType> = specs
  83 + .filter((item) => item.dataType?.type !== DataTypeEnum.STRUCT)
  84 + .reduce((prev, next) => {
  85 + const { identifier, functionName, dataType } = next;
  86 + return {
  87 + ...prev,
  88 + [identifier]: {
  89 + key: identifier!,
  90 + name: functionName!,
  91 + type: dataType?.type,
  92 + ...getAdditionalInfoByDataType(dataType),
  93 + },
  94 + };
  95 + }, {});
  96 +
  97 + Object.assign(res, { value });
  98 + } else {
  99 + Object.assign(res, getAdditionalInfoByDataType(dataType));
  100 + }
  101 + return res;
  102 + });
  103 +}
  1 +export { default as ObjectModelCommandDeliveryModal } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { ref, unref } from 'vue';
  3 + import { useGenerateFormSchemasByObjectModel } from './useGenerateFormSchemasByObjectModel';
  4 + import { ModalParamsType } from '/#/utils';
  5 + import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  6 + import { BasicForm, useForm } from '/@/components/Form';
  7 + import { BasicModal, useModalInner } from '/@/components/Modal';
  8 + import { CommandTypeEnum } from '/@/enums/deviceEnum';
  9 + import { DataTypeEnum, FunctionTypeEnum } from '/@/enums/objectModelEnum';
  10 + import { useCommandDelivery } from './useCommandDelivery';
  11 + import { useMessage } from '/@/hooks/web/useMessage';
  12 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  13 +
  14 + defineEmits(['register']);
  15 + const props = defineProps<{ deviceId: string; deviceName: string }>();
  16 +
  17 + const [registerForm, { setProps, getFieldsValue, resetFields, validate }] = useForm({
  18 + schemas: [],
  19 + showActionButtonGroup: false,
  20 + layout: 'vertical',
  21 + });
  22 +
  23 + const { getFormByObjectModel } = useGenerateFormSchemasByObjectModel();
  24 +
  25 + const currentParams = ref<{
  26 + deviceDetail: EdgeDeviceItemType;
  27 + objectModel: DeviceModelOfMatterAttrs;
  28 + }>();
  29 +
  30 + const [register] = useModalInner(
  31 + async (params: {
  32 + record: ModalParamsType<{
  33 + objectModel: DeviceModelOfMatterAttrs;
  34 + deviceDetail: EdgeDeviceItemType;
  35 + }>;
  36 + }) => {
  37 + const { record } = params;
  38 + currentParams.value = record as unknown as {
  39 + objectModel: DeviceModelOfMatterAttrs;
  40 + deviceDetail: EdgeDeviceItemType;
  41 + };
  42 + const { objectModel, deviceDetail } = record;
  43 + const schemas = getFormByObjectModel(objectModel, deviceDetail);
  44 + setProps({ schemas });
  45 + resetFields();
  46 + }
  47 + );
  48 +
  49 + const { createMessage } = useMessage();
  50 + const loading = ref(false);
  51 + const handleSend = async () => {
  52 + try {
  53 + loading.value = true;
  54 + if (!props.deviceId) return;
  55 +
  56 + await validate();
  57 + const { deviceDetail, objectModel } = unref(currentParams) || {};
  58 +
  59 + let value = getFieldsValue();
  60 + if (objectModel?.detail.dataType.type !== DataTypeEnum.STRUCT) {
  61 + value = value[objectModel!.identifier];
  62 + }
  63 +
  64 + const { doCommandDelivery } = useCommandDelivery();
  65 +
  66 + await doCommandDelivery({
  67 + deviceDetail,
  68 + objectModel: {
  69 + ...(objectModel || {}),
  70 + functionName: objectModel!.name,
  71 + identifier: objectModel!.identifier,
  72 + functionType: FunctionTypeEnum.PROPERTIES,
  73 + specs: objectModel?.detail,
  74 + },
  75 + cmdType: CommandTypeEnum.ATTRIBUTE,
  76 + value,
  77 + });
  78 +
  79 + createMessage.success('属性下发成功');
  80 + } catch (error) {
  81 + throw error;
  82 + } finally {
  83 + loading.value = false;
  84 + }
  85 + };
  86 +</script>
  87 +
  88 +<template>
  89 + <BasicModal
  90 + title="属性下发"
  91 + @register="register"
  92 + :min-height="60"
  93 + ok-text="属性下发"
  94 + wrap-class-name="model-of-matter-send-command-modal"
  95 + @ok="handleSend"
  96 + :ok-button-props="{ loading }"
  97 + >
  98 + <BasicForm @register="registerForm" class="dynamic-form" />
  99 + </BasicModal>
  100 +</template>
  101 +
  102 +<style lang="less">
  103 + .model-of-matter-send-command-modal {
  104 + .dynamic-form {
  105 + .ant-input-number {
  106 + @apply !w-full;
  107 + }
  108 + }
  109 + }
  110 +</style>
  1 +import { ref } from 'vue';
  2 +import { commandIssuanceApi, getDeviceDetail } from '/@/api/device/deviceManager';
  3 +import { RpcCommandType } from '/@/api/device/model/deviceConfigModel';
  4 +import { DeviceProfileModel } from '/@/api/device/model/deviceModel';
  5 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 +import { Tsl } from '/@/api/device/model/modelOfMatterModel';
  7 +import { getModelTsl } from '/@/api/device/modelOfMatter';
  8 +import {
  9 + CommandDeliveryWayEnum,
  10 + CommandTypeEnum,
  11 + RPCCommandMethodEnum,
  12 + ServiceCallTypeEnum,
  13 + TCPProtocolTypeEnum,
  14 + TransportTypeEnum,
  15 +} from '/@/enums/deviceEnum';
  16 +import { FunctionTypeEnum } from '/@/enums/objectModelEnum';
  17 +import { isFunction, isNullOrUnDef } from '/@/utils/is';
  18 +import { getDeviceActiveTime } from '/@/api/alarm/position';
  19 +import { useMessage } from '/@/hooks/web/useMessage';
  20 +import { useCoverModbusCommand } from './useCoverModbusCommand';
  21 +
  22 +interface SetupType {
  23 + entityId: string;
  24 + transportType: TransportTypeEnum;
  25 + isTCPModbus: boolean;
  26 + deviceCode: string | undefined;
  27 + deviceDetail: EdgeDeviceItemType;
  28 + objectModel: Tsl | undefined;
  29 + identifier: string;
  30 +}
  31 +
  32 +interface DoCommandDeliverParamsType {
  33 + value: any;
  34 + deviceDetail?: EdgeDeviceItemType;
  35 + deviceId?: string;
  36 + identifier?: string;
  37 + objectModel?: Tsl;
  38 + deviceProfileId?: string;
  39 + deviceProfileDetail?: DeviceProfileModel;
  40 + way?: CommandDeliveryWayEnum;
  41 + cmdType?: CommandTypeEnum;
  42 + transportType?: TransportTypeEnum;
  43 + beforeFetch?: (
  44 + rpcCommand: RpcCommandType,
  45 + setup: SetupType
  46 + ) =>
  47 + | { rpcCommand: RpcCommandType; way?: CommandDeliveryWayEnum }
  48 + | Promise<{ rpcCommand: RpcCommandType; way?: CommandDeliveryWayEnum }>;
  49 +}
  50 +
  51 +export function useCommandDelivery() {
  52 + const loading = ref(false);
  53 + async function doSetup(params: DoCommandDeliverParamsType) {
  54 + let { deviceDetail, identifier, deviceProfileId, objectModel, transportType } = params;
  55 + const { deviceId } = params;
  56 +
  57 + const entityId = deviceId || deviceDetail?.id?.id;
  58 + if (!entityId) {
  59 + throw new Error('not found entityId');
  60 + }
  61 +
  62 + identifier = identifier || objectModel?.identifier;
  63 +
  64 + if (!identifier) {
  65 + throw new Error('not found identifier');
  66 + }
  67 +
  68 + transportType = transportType || (deviceDetail?.transportType as TransportTypeEnum);
  69 +
  70 + if (
  71 + !transportType ||
  72 + (transportType === TransportTypeEnum.TCP && !deviceDetail) ||
  73 + !deviceDetail?.deviceProfile
  74 + ) {
  75 + deviceDetail = (await getDeviceDetail(entityId)) as any as EdgeDeviceItemType;
  76 + transportType = deviceDetail?.transportType as TransportTypeEnum;
  77 + }
  78 +
  79 + const isTCPModbus =
  80 + transportType === TransportTypeEnum.TCP &&
  81 + deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  82 + TCPProtocolTypeEnum.MODBUS_RTU;
  83 +
  84 + if (isTCPModbus && !objectModel?.extensionDesc) {
  85 + // deviceProfileId = deviceDetail?.deviceProfileId || deviceProfileId || deviceProfileDetail?.id;
  86 + deviceProfileId = deviceDetail?.deviceProfileId?.id;
  87 + if (!deviceProfileId) {
  88 + throw new Error('not found deviceProfile');
  89 + }
  90 +
  91 + const objectModels = await getModelTsl({ deviceProfileId });
  92 + objectModel = objectModels.find((item) => item.identifier === identifier);
  93 + }
  94 +
  95 + const deviceCode = deviceDetail?.code;
  96 +
  97 + return {
  98 + entityId,
  99 + transportType,
  100 + isTCPModbus,
  101 + deviceCode,
  102 + deviceDetail,
  103 + objectModel,
  104 + identifier,
  105 + };
  106 + }
  107 +
  108 + async function doCommandDelivery(params: DoCommandDeliverParamsType) {
  109 + try {
  110 + loading.value = true;
  111 +
  112 + const setupResult = await doSetup(params);
  113 +
  114 + const { entityId, transportType, isTCPModbus, deviceCode, objectModel, identifier } =
  115 + setupResult;
  116 +
  117 + let command = params.value;
  118 +
  119 + if (transportType === TransportTypeEnum.TCP) {
  120 + command = params.value;
  121 + if (isTCPModbus) {
  122 + const { doCoverCommand } = useCoverModbusCommand();
  123 + command = await doCoverCommand(params.value, objectModel!, deviceCode, entityId);
  124 + }
  125 + } else {
  126 + command = {
  127 + [identifier]: command,
  128 + };
  129 + }
  130 +
  131 + let rpcCommand: RpcCommandType = {
  132 + persistent: true,
  133 + method: RPCCommandMethodEnum.THINGSKIT,
  134 + additionalInfo: {
  135 + cmdType: params.cmdType ?? CommandTypeEnum.API,
  136 + },
  137 + params: command,
  138 + };
  139 +
  140 + let way = params.way ?? CommandDeliveryWayEnum.ONE_WAY;
  141 +
  142 + if (objectModel?.functionType === FunctionTypeEnum.SERVICE) {
  143 + rpcCommand.additionalInfo.cmdType = CommandTypeEnum.SERVICE;
  144 + way =
  145 + objectModel.callType === ServiceCallTypeEnum.ASYNC
  146 + ? CommandDeliveryWayEnum.ONE_WAY
  147 + : CommandDeliveryWayEnum.TWO_WAY;
  148 + }
  149 +
  150 + const sendApi = commandIssuanceApi;
  151 +
  152 + if (params.beforeFetch && isFunction(params.beforeFetch)) {
  153 + const { rpcCommand: _rpcCommand, way: _way } = await params.beforeFetch(
  154 + rpcCommand,
  155 + setupResult
  156 + );
  157 + !isNullOrUnDef(rpcCommand) && (rpcCommand = _rpcCommand);
  158 + if (_way) way = _way;
  159 + }
  160 +
  161 + if (way === CommandDeliveryWayEnum.TWO_WAY) {
  162 + const result = await getDeviceActiveTime(entityId);
  163 + const [firsetItem] = result || [];
  164 +
  165 + if (!firsetItem.value) {
  166 + const { createMessage } = useMessage();
  167 + const message = '当前设备不在线';
  168 + createMessage.warning(message);
  169 + throw Error(message);
  170 + }
  171 + }
  172 + await sendApi(way, entityId, rpcCommand);
  173 + } finally {
  174 + loading.value = false;
  175 + }
  176 + }
  177 +
  178 + return {
  179 + loading,
  180 + doCommandDelivery,
  181 + };
  182 +}
  1 +import { useParseOriginalDataType } from './useParseOriginalDataType';
  2 +import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  3 +import { getDeviceDetail } from '/@/api/device/deviceManager';
  4 +import { ExtensionDesc, Specs, Tsl } from '/@/api/device/model/modelOfMatterModel';
  5 +import { genModbusCommand } from '/@/api/task';
  6 +import { GenModbusCommandType } from '/@/api/task/model';
  7 +import { ModbusCRCEnum, OriginalDataTypeEnum } from '/@/enums/objectModelEnum';
  8 +import { useBaseConversion } from '/@/hooks/business/useBaseConversion';
  9 +import { useMessage } from '/@/hooks/web/useMessage';
  10 +import { isNullOrUnDef } from '/@/utils/is';
  11 +import {
  12 + isFloatType,
  13 + isNumberType,
  14 + useParseOperationType,
  15 +} from '/@/views/device/profiles/components/ObjectModelForm/ExtendDesc/useParseOperationType';
  16 +
  17 +const getFloatPart = (number: string | number) => {
  18 + const isLessZero = Number(number) < 0;
  19 + number = number.toString();
  20 + const floatPartStartIndex = number.indexOf('.');
  21 + const value = ~floatPartStartIndex
  22 + ? `${isLessZero ? '-' : ''}0.${number.substring(floatPartStartIndex + 1)}`
  23 + : '0';
  24 + return Number(value);
  25 +};
  26 +
  27 +function getValueFromValueRange(value: number, valueRange?: Record<'min' | 'max', number>) {
  28 + const { min, max } = valueRange || {};
  29 + if (!isNullOrUnDef(min) && value < min) return min;
  30 + if (!isNullOrUnDef(max) && value > max) return max;
  31 + return value;
  32 +}
  33 +
  34 +async function getCurrentBitCommand(entityId: string, objectModel: Tsl, value: number) {
  35 + const deviceDetail = await getDeviceDetail(entityId);
  36 + const thingsModels = deviceDetail.deviceProfile.profileData.thingsModel;
  37 +
  38 + const { registerAddress } = objectModel.extensionDesc || {};
  39 +
  40 + const bitsModel = thingsModels?.filter(
  41 + (item) =>
  42 + item.extensionDesc?.originalDataType === OriginalDataTypeEnum.BITS &&
  43 + item.extensionDesc.registerAddress === registerAddress
  44 + );
  45 +
  46 + const valuePositionMap =
  47 + bitsModel?.reduce((prev, next) => {
  48 + return { ...prev, [next.identifier]: next.extensionDesc?.bitMask };
  49 + }, {} as Record<string, number>) || {};
  50 +
  51 + const attrKeys = Object.keys(valuePositionMap);
  52 +
  53 + const latestBitsValues = await getDeviceHistoryInfo({ entityId, keys: attrKeys.join(',') });
  54 +
  55 + const binaryArr = Array.from({ length: 16 }, () => 0);
  56 +
  57 + for (const key of attrKeys) {
  58 + const index = valuePositionMap[key];
  59 +
  60 + if (!isNullOrUnDef(index)) {
  61 + const [latest] = latestBitsValues[key];
  62 + const { value } = latest;
  63 + binaryArr[index] = Number(value);
  64 + }
  65 + }
  66 +
  67 + if (objectModel.extensionDesc?.bitMask) {
  68 + binaryArr[objectModel.extensionDesc.bitMask] = value;
  69 + }
  70 +
  71 + return [parseInt(binaryArr.reverse().join(''), 2)];
  72 +}
  73 +
  74 +export function useCoverModbusCommand() {
  75 + const { createMessage } = useMessage();
  76 +
  77 + const doCoverCommand = async (
  78 + value: number,
  79 + objectModel: Tsl,
  80 + deviceAddressCode?: string,
  81 + entityId?: string
  82 + ) => {
  83 + if (!deviceAddressCode) {
  84 + const message = '当前设备未绑定设备地址码';
  85 + createMessage.warning(message);
  86 + throw new Error(message);
  87 + }
  88 +
  89 + const {
  90 + registerAddress,
  91 + operationType,
  92 + scaling,
  93 + originalDataType,
  94 + bitMask,
  95 + registerCount: registerNumber,
  96 + } = objectModel.extensionDesc as Required<ExtensionDesc>;
  97 +
  98 + const { writeRegisterAddress } = useParseOperationType(operationType);
  99 + const { unsigned, exchangeSortFlag, registerCount } =
  100 + useParseOriginalDataType(originalDataType);
  101 +
  102 + const params: GenModbusCommandType = {
  103 + crc: ModbusCRCEnum.CRC_16_LOWER,
  104 + registerNumber: registerCount || registerNumber,
  105 + deviceCode: deviceAddressCode,
  106 + registerAddress: parseInt(registerAddress, 16),
  107 + method: writeRegisterAddress!,
  108 + registerValues: [value],
  109 + };
  110 +
  111 + if (exchangeSortFlag) params.hexByteOrderEnum = exchangeSortFlag;
  112 +
  113 + const { getRegisterValueByOriginalDataType } = useBaseConversion();
  114 +
  115 + if (isNumberType(originalDataType)) {
  116 + let newValue = Math.trunc(value) * scaling + getFloatPart(value) * scaling;
  117 +
  118 + newValue = unsigned ? newValue : Math.abs(newValue);
  119 +
  120 + newValue = getValueFromValueRange(
  121 + newValue,
  122 + (objectModel.specs?.dataType.specs as Specs).valueRange
  123 + );
  124 +
  125 + if (!isFloatType(originalDataType) && newValue % 1 !== 0) {
  126 + const message = `属性下发类型必须是整数,缩放因子为${scaling}`;
  127 + createMessage.warning(message);
  128 + throw Error(message);
  129 + }
  130 +
  131 + value = newValue;
  132 + }
  133 +
  134 + params.registerValues =
  135 + originalDataType === OriginalDataTypeEnum.BITS
  136 + ? await getCurrentBitCommand(entityId!, objectModel, value)
  137 + : getRegisterValueByOriginalDataType(value, originalDataType, {
  138 + bitMask,
  139 + registerNumber,
  140 + });
  141 +
  142 + if (!params.method) {
  143 + const message = '物模型操作类型无法进行写入';
  144 + createMessage.warning(message);
  145 + throw Error(message);
  146 + }
  147 +
  148 + return await genModbusCommand(params);
  149 + };
  150 +
  151 + return {
  152 + doCoverCommand,
  153 + };
  154 +}
  1 +import { unref } from 'vue';
  2 +import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  3 +import { DataType, Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel';
  4 +import { FormSchema } from '/@/components/Form';
  5 +import { DataTypeEnum, OriginalDataTypeEnum } from '/@/enums/objectModelEnum';
  6 +import { TCPProtocolTypeEnum } from '/@/enums/deviceEnum';
  7 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  8 +
  9 +export interface BasicCreateFormParams {
  10 + identifier: string;
  11 + functionName: string;
  12 + dataType: DataType;
  13 +}
  14 +
  15 +const validateDouble = (value: number, min?: number | string, max?: number | string) => {
  16 + min = Number(min) ?? Number.MIN_SAFE_INTEGER;
  17 + max = Number(max) ?? Number.MAX_SAFE_INTEGER;
  18 +
  19 + return {
  20 + flag: value < min || value > max,
  21 + message: `取值范围在${min}~${max}之间`,
  22 + };
  23 +};
  24 +
  25 +export const useGenerateFormSchemasByObjectModel = () => {
  26 + const createInputNumber = ({
  27 + identifier,
  28 + functionName,
  29 + dataType,
  30 + }: BasicCreateFormParams): FormSchema => {
  31 + const { specs, type } = dataType;
  32 + const { valueRange, step } = specs! as Partial<Specs>;
  33 + const { max, min } = valueRange || {};
  34 + return {
  35 + field: identifier,
  36 + label: functionName,
  37 + component: 'InputNumber',
  38 + rules: [
  39 + {
  40 + type: 'number',
  41 + trigger: 'change',
  42 + validator: (_rule, value) => {
  43 + const { flag, message } = validateDouble(value, min, max);
  44 + if (flag) {
  45 + return Promise.reject(`${functionName}${message}`);
  46 + }
  47 + return Promise.resolve(value);
  48 + },
  49 + },
  50 + ],
  51 + componentProps: {
  52 + max: max ?? Number.MAX_SAFE_INTEGER,
  53 + min: min ?? Number.MIN_SAFE_INTEGER,
  54 + step,
  55 + placeholder: `请输入${functionName}`,
  56 + precision: type === DataTypeEnum.NUMBER_INT ? 0 : 2,
  57 + },
  58 + } as FormSchema;
  59 + };
  60 +
  61 + const createInput = ({
  62 + identifier,
  63 + functionName,
  64 + dataType,
  65 + }: BasicCreateFormParams): FormSchema => {
  66 + const { specs } = dataType;
  67 + const { length = 10240 } = specs! as Partial<Specs>;
  68 + return {
  69 + field: identifier,
  70 + label: functionName,
  71 + component: 'Input',
  72 + rules: [
  73 + {
  74 + type: 'string',
  75 + trigger: 'change',
  76 + validator: (_rule, value) => {
  77 + if (value?.length > length) {
  78 + return Promise.reject(`${functionName}数据长度应该小于${length}`);
  79 + }
  80 + return Promise.resolve(value);
  81 + },
  82 + },
  83 + ],
  84 + componentProps: {
  85 + maxLength: length,
  86 + placeholder: `请输入${functionName}`,
  87 + },
  88 + } as FormSchema;
  89 + };
  90 +
  91 + const createSelect = ({
  92 + identifier,
  93 + functionName,
  94 + dataType,
  95 + }: BasicCreateFormParams): FormSchema => {
  96 + const { specs } = dataType;
  97 + const { boolClose, boolOpen } = specs! as Partial<Specs>;
  98 + return {
  99 + field: identifier,
  100 + label: functionName,
  101 + component: 'Select',
  102 + componentProps: {
  103 + options: [
  104 + { label: `${boolClose}-0`, value: 0 },
  105 + { label: `${boolOpen}-1`, value: 1 },
  106 + ],
  107 + placeholder: `请选择${functionName}`,
  108 + getPopupContainer: () => document.body,
  109 + },
  110 + };
  111 + };
  112 +
  113 + const createEnumSelect = ({
  114 + identifier,
  115 + functionName,
  116 + dataType,
  117 + }: BasicCreateFormParams): FormSchema => {
  118 + const { specsList } = dataType;
  119 + return {
  120 + field: identifier,
  121 + label: functionName,
  122 + component: 'Select',
  123 + componentProps: {
  124 + options: specsList?.map((item) => ({ label: item.name, value: item.value })),
  125 + placeholder: `请选择${functionName}`,
  126 + getPopupContainer: () => document.body,
  127 + },
  128 + };
  129 + };
  130 +
  131 + const createModbusValueInput = (objectModel: DeviceModelOfMatterAttrs): FormSchema => {
  132 + const { identifier, name, detail, extensionDesc } = objectModel;
  133 +
  134 + const { dataType } = detail || {};
  135 + const { specs } = dataType || {};
  136 + const { valueRange } = specs as Specs;
  137 + const { max, min } = valueRange || {};
  138 +
  139 + if (extensionDesc?.originalDataType === OriginalDataTypeEnum.BOOLEAN) {
  140 + const options = [
  141 + { label: '闭合', value: parseInt('FF00', 16) },
  142 + { label: '断开', value: parseInt('0000', 16) },
  143 + ];
  144 +
  145 + return {
  146 + field: identifier,
  147 + label: name,
  148 + component: 'Select',
  149 + componentProps: () => {
  150 + return {
  151 + options,
  152 + placeholder: `请选择${name}`,
  153 + getPopupContainer: () => document.body,
  154 + };
  155 + },
  156 + };
  157 + }
  158 +
  159 + const isStringType = extensionDesc?.originalDataType === OriginalDataTypeEnum.STRING;
  160 + return {
  161 + field: identifier,
  162 + label: name,
  163 + component: isStringType ? 'Input' : 'InputNumber',
  164 + rules: isStringType
  165 + ? []
  166 + : [
  167 + {
  168 + type: 'number',
  169 + validator: (_rule, value) => {
  170 + const { flag, message } = validateDouble(value, min, max);
  171 + if (flag) {
  172 + return Promise.reject(`${name}${message}`);
  173 + }
  174 + return Promise.resolve(value);
  175 + },
  176 + },
  177 + ],
  178 + componentProps: {
  179 + max: max ?? Number.MAX_SAFE_INTEGER,
  180 + min: min ?? Number.MIN_SAFE_INTEGER,
  181 + placeholder: `请输入${name}`,
  182 + // precision: floatType.includes(extensionDesc!.originalDataType) ? 2 : 0,
  183 + },
  184 + };
  185 + };
  186 +
  187 + const schemaMethod = {
  188 + [DataTypeEnum.BOOL]: createSelect,
  189 + [DataTypeEnum.NUMBER_DOUBLE]: createInputNumber,
  190 + [DataTypeEnum.NUMBER_INT]: createInputNumber,
  191 + [DataTypeEnum.STRING]: createInput,
  192 + [DataTypeEnum.ENUM]: createEnumSelect,
  193 + };
  194 +
  195 + const getFormByObjectModel = (
  196 + objectModel: DeviceModelOfMatterAttrs,
  197 + deviceDetail: EdgeDeviceItemType
  198 + ): FormSchema[] => {
  199 + const { name, identifier, detail } = objectModel;
  200 +
  201 + const isTCPModbusProduct =
  202 + unref(deviceDetail).deviceProfile?.profileData?.transportConfiguration?.protocol ===
  203 + TCPProtocolTypeEnum.MODBUS_RTU;
  204 +
  205 + const { dataType } = detail;
  206 + const { type } = dataType || {};
  207 +
  208 + if (isTCPModbusProduct) {
  209 + return [createModbusValueInput(objectModel)];
  210 + }
  211 +
  212 + if (type === DataTypeEnum.STRUCT) {
  213 + return (dataType?.specs as StructJSON[]).map((item) => {
  214 + const { functionName, identifier, dataType } = item;
  215 + const { type } = dataType || {};
  216 +
  217 + return schemaMethod[type!]?.({ identifier, functionName, dataType });
  218 + });
  219 + }
  220 +
  221 + const result = schemaMethod[type!]?.({ identifier, functionName: name, dataType: dataType! });
  222 + return result ? [result] : [];
  223 + };
  224 +
  225 + return { getFormByObjectModel };
  226 +};
  1 +import { OriginalDataTypeEnum } from '/@/enums/objectModelEnum';
  2 +
  3 +export type OriginalDataTypePrefixType<S = `${OriginalDataTypeEnum}`> = S extends string
  4 + ? S extends `${infer D}_${string}`
  5 + ? D
  6 + : S
  7 + : '';
  8 +
  9 +function getRegisterCount(originalDataType: OriginalDataTypeEnum) {
  10 + switch (originalDataType) {
  11 + case OriginalDataTypeEnum.INT16_AB:
  12 + case OriginalDataTypeEnum.INT16_BA:
  13 + case OriginalDataTypeEnum.UINT16_AB:
  14 + case OriginalDataTypeEnum.UINT16_BA:
  15 + case OriginalDataTypeEnum.BITS:
  16 + case OriginalDataTypeEnum.BOOLEAN:
  17 + return 1;
  18 +
  19 + case OriginalDataTypeEnum.INT32_AB_CD:
  20 + case OriginalDataTypeEnum.INT32_BA_DC:
  21 + case OriginalDataTypeEnum.INT32_CD_AB:
  22 + case OriginalDataTypeEnum.INT32_DC_BA:
  23 + case OriginalDataTypeEnum.UINT32_AB_CD:
  24 + case OriginalDataTypeEnum.UINT32_BA_DC:
  25 + case OriginalDataTypeEnum.UINT32_CD_AB:
  26 + case OriginalDataTypeEnum.UINT32_DC_BA:
  27 + return 2;
  28 +
  29 + case OriginalDataTypeEnum.FLOAT_AB_CD:
  30 + case OriginalDataTypeEnum.FLOAT_BA_DC:
  31 + case OriginalDataTypeEnum.FLOAT_CD_AB:
  32 + case OriginalDataTypeEnum.FLOAT_DC_BA:
  33 + return 2;
  34 +
  35 + case OriginalDataTypeEnum.DOUBLE:
  36 + return 4;
  37 + }
  38 +}
  39 +
  40 +export function useParseOriginalDataType(originalDataType: OriginalDataTypeEnum) {
  41 + const signedMatchRef = /^UN/;
  42 +
  43 + const splitArray = originalDataType.split('_') as [OriginalDataTypePrefixType];
  44 +
  45 + const [dataType] = splitArray;
  46 +
  47 + const exchangeSortFlag = splitArray.slice(1).join('_');
  48 +
  49 + return {
  50 + registerCount: getRegisterCount(originalDataType),
  51 + unsigned: !signedMatchRef.test(originalDataType),
  52 + dataType,
  53 + exchangeSortFlag: exchangeSortFlag || null,
  54 + };
  55 +}
  1 +<script lang="ts">
  2 + import { defineComponent, reactive } from 'vue';
  3 + import { BasicTable, useTable } from '/@/components/Table';
  4 + import { realTimeDataColumns } from './config';
  5 + import { useWebSocket } from '@vueuse/core';
  6 + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
  7 + import { getAuthCache } from '/@/utils/auth';
  8 + import { useMessage } from '/@/hooks/web/useMessage';
  9 + import type { socketDataType } from './types';
  10 + import { useGlobSetting } from '/@/hooks/setting';
  11 +
  12 + export default defineComponent({
  13 + name: 'RealTimeData',
  14 + components: {
  15 + BasicTable,
  16 + },
  17 + props: {
  18 + deviceDetail: {
  19 + type: Object,
  20 + required: true,
  21 + },
  22 + },
  23 + setup(props) {
  24 + const token: string = getAuthCache(JWT_TOKEN_KEY);
  25 + const { socketUrl } = useGlobSetting();
  26 + const state: any = reactive({
  27 + server: `${socketUrl}${token}`,
  28 + sendValue: JSON.stringify({
  29 + attrSubCmds: [],
  30 + tsSubCmds: [
  31 + {
  32 + entityType: 'DEVICE',
  33 + entityId: props.deviceDetail.tbDeviceId,
  34 + scope: 'LATEST_TELEMETRY',
  35 + cmdId: 1,
  36 + },
  37 + ],
  38 + historyCmds: [],
  39 + entityDataCmds: [],
  40 + entityDataUnsubscribeCmds: [],
  41 + alarmDataCmds: [],
  42 + alarmDataUnsubscribeCmds: [],
  43 + entityCountCmds: [],
  44 + entityCountUnsubscribeCmds: [],
  45 + }),
  46 + recordList: Array<socketDataType>(),
  47 + });
  48 + const { createMessage } = useMessage();
  49 + const [registerTable, { getForm, setTableData }] = useTable({
  50 + columns: realTimeDataColumns,
  51 + showTableSetting: true,
  52 + bordered: true,
  53 + showIndexColumn: false,
  54 + dataSource: state.recordList,
  55 + useSearchForm: true,
  56 + formConfig: {
  57 + labelWidth: 100,
  58 + schemas: [
  59 + {
  60 + field: 'key',
  61 + label: '键/值',
  62 + component: 'Input',
  63 + colProps: { span: 6 },
  64 + componentProps: {
  65 + placeholder: '请输入 键/值',
  66 + maxLength: 255,
  67 + },
  68 + },
  69 + ],
  70 + // 自定义前端查询
  71 + submitFunc() {
  72 + const { getFieldsValue } = getForm();
  73 + const { key } = getFieldsValue();
  74 + if (!key) {
  75 + setTableData(state.recordList);
  76 + return;
  77 + }
  78 + let newRecordList = [];
  79 + let len = state.recordList.length;
  80 + for (let i = 0; i < len; i++) {
  81 + if (
  82 + state.recordList[i].key.indexOf(key) >= 0 ||
  83 + state.recordList[i].value.indexOf(key) >= 0
  84 + ) {
  85 + newRecordList.push(state.recordList[i] as any as never);
  86 + }
  87 + }
  88 + setTableData(newRecordList);
  89 + },
  90 + resetFunc() {
  91 + setTableData(state.recordList);
  92 + },
  93 + },
  94 + });
  95 +
  96 + const { send, close } = useWebSocket(state.server, {
  97 + onConnected() {
  98 + send(state.sendValue);
  99 + },
  100 + onMessage(_, e) {
  101 + const { data } = JSON.parse(e.data);
  102 + const newArray: socketDataType[] = [];
  103 + for (const key in data) {
  104 + const [time, value] = data[key].flat(1);
  105 + let obj = {
  106 + key,
  107 + time,
  108 + value,
  109 + };
  110 + if (state.recordList.length === 0) {
  111 + state.recordList.unshift(obj);
  112 + } else {
  113 + newArray.push(obj);
  114 + }
  115 + }
  116 + newArray.forEach((item) => {
  117 + let flag = false;
  118 + state.recordList.forEach((item1) => {
  119 + if (item1.key === item.key) {
  120 + item1.value = item.value;
  121 + item1.time = item.time;
  122 + flag = true;
  123 + }
  124 + });
  125 + if (!flag) {
  126 + state.recordList.unshift(item);
  127 + }
  128 + });
  129 + },
  130 + onDisconnected() {
  131 + close();
  132 + },
  133 + onError() {
  134 + createMessage.error('webSocket连接超时,请联系管理员');
  135 + },
  136 + });
  137 + return {
  138 + registerTable,
  139 + };
  140 + },
  141 + });
  142 +</script>
  143 +
  144 +<template>
  145 + <div style="background-color: #f0f2f5">
  146 + <BasicTable @register="registerTable" />
  147 + </div>
  148 +</template>
  1 +import { BasicColumn } from '/@/components/Table';
  2 +import { formatToDateTime } from '/@/utils/dateUtil';
  3 +import { isNullOrUnDef } from '/@/utils/is';
  4 +
  5 +// 实时数据表格
  6 +export const realTimeDataColumns: BasicColumn[] = [
  7 + {
  8 + title: '键',
  9 + dataIndex: 'name',
  10 + width: 100,
  11 + },
  12 + {
  13 + title: '值',
  14 + dataIndex: 'rawValue',
  15 + width: 160,
  16 + format(text) {
  17 + return isNullOrUnDef(text) ? '--' : text;
  18 + },
  19 + },
  20 + {
  21 + title: '最后更新时间',
  22 + dataIndex: 'time',
  23 + width: 120,
  24 + format: (text) => formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss'),
  25 + },
  26 +];
  1 +import { EChartsOption } from 'echarts';
  2 +import moment from 'moment';
  3 +import { computed, ref, unref } from 'vue';
  4 +import { HistoryData } from '/@/api/alarm/position/model';
  5 +import { getDeviceAttributes } from '/@/api/dataBoard';
  6 +import { DeviceAttributeRecord } from '/@/api/dataBoard/model';
  7 +import { dateUtil } from '/@/utils/dateUtil';
  8 +import { isArray } from '/@/utils/is';
  9 +import { DEFAULT_DATE_FORMAT } from '/@/views/visual/board/detail/config/util';
  10 +import { QueryWay, SchemaFiled } from '/@/views/visual/palette/components/HistoryTrendModal/config';
  11 +import { eChartOptions } from '/@/views/device/localtion/config.data';
  12 +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  13 +
  14 +// interface DeviceOption {
  15 +// deviceProfileId: string;
  16 +// }
  17 +
  18 +export function useHistoryData() {
  19 + const deviceAttrs = ref<DeviceAttributeRecord[]>([]);
  20 +
  21 + const getDeviceKeys = computed(() => {
  22 + return unref(deviceAttrs).map((item) => item.identifier);
  23 + });
  24 +
  25 + const getDeviceAttribute = async (record: EdgeDeviceItemType) => {
  26 + try {
  27 + const { deviceProfileId } = record;
  28 + const { id } = deviceProfileId;
  29 + const list = (await getDeviceAttributes({ deviceProfileId: id })) || [];
  30 + deviceAttrs.value = isArray(list) ? list : [];
  31 + } catch (error) {
  32 + throw error;
  33 + }
  34 + };
  35 +
  36 + function getSearchParams(value: Partial<Record<SchemaFiled, string>>) {
  37 + const { startTs, endTs, interval, agg, limit, keys, way, deviceId, orderBy } = value;
  38 + const basicRecord = {
  39 + entityId: deviceId,
  40 + keys: keys ? keys : unref(getDeviceKeys).join(),
  41 + interval,
  42 + agg,
  43 + limit,
  44 + orderBy,
  45 + };
  46 + if (way === QueryWay.LATEST) {
  47 + return Object.assign(basicRecord, {
  48 + startTs: moment().subtract(startTs, 'ms').valueOf(),
  49 + endTs: Date.now(),
  50 + });
  51 + } else {
  52 + return Object.assign(basicRecord, {
  53 + startTs: moment(startTs).valueOf(),
  54 + endTs: moment(endTs).valueOf(),
  55 + });
  56 + }
  57 + }
  58 +
  59 + function setChartOptions(
  60 + data: HistoryData,
  61 + keys?: DeviceAttributeRecord | DeviceAttributeRecord[]
  62 + ) {
  63 + const dataArray: [string, string, string][] = [];
  64 + for (const key in data) {
  65 + for (const item of data[key]) {
  66 + let { value } = item;
  67 + const { ts } = item;
  68 + const time = dateUtil(ts).format(DEFAULT_DATE_FORMAT);
  69 + value = Number(value).toFixed(2);
  70 + dataArray.push([time, value, key as string]);
  71 + }
  72 + }
  73 +
  74 + keys = keys ? [keys as DeviceAttributeRecord] : unref(deviceAttrs);
  75 + const legend = keys.map((item) => item.name);
  76 +
  77 + const series: EChartsOption['series'] = (keys as DeviceAttributeRecord[]).map((item) => {
  78 + return {
  79 + name: item.name,
  80 + type: 'line',
  81 + data: dataArray.filter((temp) => temp[2] === item.identifier),
  82 + };
  83 + });
  84 +
  85 + return eChartOptions(series, legend);
  86 + }
  87 +
  88 + return {
  89 + deviceAttrs,
  90 + getDeviceKeys,
  91 + getDeviceAttribute,
  92 + getSearchParams,
  93 + setChartOptions,
  94 + };
  95 +}
  1 +export { default as EdgeDevicePhysicalModel } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { nextTick, onMounted, onUnmounted, reactive, ref, unref, computed } from 'vue';
  3 + import { List, Card, Tooltip, Space, PaginationProps } from 'ant-design-vue';
  4 + import { PageWrapper } from '/@/components/Page';
  5 + import { BasicTable, useTable } from '/@/components/Table';
  6 + import { realTimeDataColumns } from './config';
  7 + import { useWebSocket } from '@vueuse/core';
  8 + import { getAuthCache } from '/@/utils/auth';
  9 + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum';
  10 + import { useMessage } from '/@/hooks/web/useMessage';
  11 + import { formatToDateTime } from '/@/utils/dateUtil';
  12 + import { BasicForm, useForm } from '/@/components/Form';
  13 + import HistoryData from './HistoryData.vue';
  14 + import { BasicModal, useModal } from '/@/components/Modal';
  15 + import { getDeviceAttrs } from '/@/api/device/deviceManager';
  16 + import { isArray, isNull, isNullOrUnDef, isObject } from '/@/utils/is';
  17 + import { useGlobSetting } from '/@/hooks/setting';
  18 + import { ModeSwitchButton, EnumTableCardMode } from '/@/components/Widget';
  19 + import { toRaw } from 'vue';
  20 + import { DataActionModeEnum } from '/@/enums/toolEnum';
  21 + import { ReadAndWriteEnum, TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum';
  22 + import { ObjectModelCommandDeliveryModal } from './ObjectModelCommandDeliveryModal';
  23 + import { ModalParamsType } from '/#/utils';
  24 + import { AreaChartOutlined } from '@ant-design/icons-vue';
  25 + import { SvgIcon, Icon } from '/@/components/Icon';
  26 + import { DataTypeEnum } from '/@/enums/objectModelEnum';
  27 + import {
  28 + buildTableDataSourceByObjectModel,
  29 + SocketInfoDataSourceItemType,
  30 + StructValueItemType,
  31 + } from './ModelOfMatter.config';
  32 + import { useJsonParse } from '/@/hooks/business/useJsonParse';
  33 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  34 + import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel';
  35 +
  36 + interface ReceiveMessage {
  37 + data: {
  38 + [key: string]: [number, string][];
  39 + };
  40 + }
  41 +
  42 + const props = defineProps<{
  43 + deviceDetail: EdgeDeviceItemType;
  44 + }>();
  45 +
  46 + const grid = {
  47 + gutter: 8,
  48 + column: 3,
  49 + } as any;
  50 +
  51 + const listElRef = ref<Nullable<ComponentElRef<HTMLDivElement>>>(null);
  52 +
  53 + const token = getAuthCache(JWT_TOKEN_KEY);
  54 +
  55 + const { socketUrl } = useGlobSetting();
  56 +
  57 + const pagination = reactive<PaginationProps>({
  58 + size: 'small',
  59 + showSizeChanger: true,
  60 + showQuickJumper: true,
  61 + hideOnSinglePage: false,
  62 + showTotal: (total: number) => `共${total}条数据`,
  63 + onChange: handleFilterChange,
  64 + onShowSizeChange: handleFilterChange,
  65 + });
  66 +
  67 + const socketInfo = reactive({
  68 + cmdId: 2,
  69 + origin: `${socketUrl}${token}`,
  70 + attr: undefined as string | undefined,
  71 + dataSource: [] as SocketInfoDataSourceItemType[],
  72 + rawDataSource: [] as SocketInfoDataSourceItemType[],
  73 + message: {} as ReceiveMessage['data'],
  74 + attrKeys: [] as string[],
  75 + filterAttrKeys: [] as string[],
  76 + });
  77 +
  78 + function createUnsubscribeMessage(cmdId: number) {
  79 + return {
  80 + tsSubCmds: [
  81 + {
  82 + cmdId,
  83 + unsubscribe: true,
  84 + },
  85 + ],
  86 + };
  87 + }
  88 +
  89 + const getSendValue = computed(() => {
  90 + return {
  91 + tsSubCmds: [
  92 + {
  93 + entityType: 'DEVICE',
  94 + entityId: props.deviceDetail?.id?.id,
  95 + scope: 'LATEST_TELEMETRY',
  96 + cmdId: socketInfo.cmdId,
  97 + type: 'TIMESERIES',
  98 + },
  99 + ],
  100 + };
  101 + });
  102 +
  103 + const getFilterSendValue = computed(() => {
  104 + return {
  105 + tsSubCmds: [
  106 + {
  107 + entityType: 'DEVICE',
  108 + entityId: props.deviceDetail?.id?.id,
  109 + scope: 'LATEST_TELEMETRY',
  110 + cmdId: socketInfo.cmdId,
  111 + type: 'TIMESERIES',
  112 + },
  113 + ],
  114 + };
  115 + });
  116 +
  117 + const [registerForm, { getFieldsValue }] = useForm({
  118 + schemas: [
  119 + {
  120 + field: 'value',
  121 + label: '键/值',
  122 + component: 'Input',
  123 + colProps: { span: 6 },
  124 + componentProps: {
  125 + placeholder: '请输入键/值',
  126 + },
  127 + },
  128 + ],
  129 + labelWidth: 100,
  130 + compact: true,
  131 + showAdvancedButton: true,
  132 + submitFunc: async () => {
  133 + try {
  134 + const { value } = getFieldsValue() || {};
  135 +
  136 + pagination.current = 1;
  137 +
  138 + socketInfo.filterAttrKeys = value
  139 + ? unref(socketInfo.rawDataSource)
  140 + .filter(
  141 + (item) =>
  142 + item.key?.toUpperCase().includes(value.toUpperCase()) ||
  143 + item.name?.toUpperCase().includes(value.toUpperCase())
  144 + )
  145 + .map((item) => item.key)
  146 + : socketInfo.rawDataSource.map((item) => item.key);
  147 +
  148 + await nextTick();
  149 + handleFilterChange();
  150 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  151 + } catch (error) {}
  152 + },
  153 + resetFunc: async () => {
  154 + try {
  155 + socketInfo.filterAttrKeys = [];
  156 + handleFilterChange();
  157 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  158 + } catch (error) {}
  159 + },
  160 + });
  161 +
  162 + const [registerTable, { setTableData }] = useTable({
  163 + columns: realTimeDataColumns,
  164 + showTableSetting: true,
  165 + pagination: pagination as any,
  166 + bordered: true,
  167 + resizeHeightOffset: 16,
  168 + showIndexColumn: false,
  169 + });
  170 +
  171 + function handleFilterChange(
  172 + page: number = pagination.current || 1,
  173 + pageSize: number = pagination.pageSize || 10
  174 + ) {
  175 + pagination.current = page;
  176 + pagination.pageSize = pageSize;
  177 + send(JSON.stringify(createUnsubscribeMessage(socketInfo.cmdId)));
  178 + socketInfo.cmdId = socketInfo.cmdId + 1;
  179 + send(JSON.stringify(unref(getFilterSendValue)));
  180 + }
  181 +
  182 + const [registerModal, { openModal }] = useModal();
  183 +
  184 + const mode = ref<EnumTableCardMode>(EnumTableCardMode.CARD);
  185 +
  186 + const switchMode = async (value: EnumTableCardMode) => {
  187 + mode.value = value;
  188 + await nextTick();
  189 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  190 + socketInfo.filterAttrKeys = [];
  191 + };
  192 +
  193 + const { createMessage } = useMessage();
  194 +
  195 + const setDataSource = () => {
  196 + socketInfo.dataSource = socketInfo.filterAttrKeys.length
  197 + ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key))
  198 + : socketInfo.rawDataSource;
  199 + };
  200 +
  201 + const { send, close, data, open } = useWebSocket(socketInfo.origin, {
  202 + immediate: false,
  203 + autoReconnect: true,
  204 + async onConnected() {
  205 + send(JSON.stringify(unref(getSendValue)));
  206 + },
  207 + async onMessage() {
  208 + try {
  209 + const value = JSON.parse(unref(data)) as ReceiveMessage;
  210 + if (value) {
  211 + const { data } = value;
  212 + const keys = Object.keys(data);
  213 + for (const key of keys) {
  214 + const item = socketInfo.rawDataSource.find((item) => item.key === key);
  215 + if (!item) continue;
  216 + const { type } = item;
  217 + const [firstItem] = data[key] || [];
  218 + const [time, value] = firstItem;
  219 + item.rawValue = value;
  220 + if (type === DataTypeEnum.STRUCT) {
  221 + const { flag, value: structJSON } = useJsonParse(value);
  222 + if (!flag || !isObject(item.value)) continue;
  223 + const structKeys = Object.keys(structJSON);
  224 + structKeys.forEach((key) => {
  225 + if ((item.value as Record<string, StructValueItemType>)?.[key]) {
  226 + (item.value as Record<string, StructValueItemType>)[key].value = structJSON[key];
  227 + }
  228 + });
  229 + item.time = time;
  230 + continue;
  231 + }
  232 + item.value = value;
  233 + item.time = time;
  234 + }
  235 + setDataSource();
  236 + await nextTick();
  237 + unref(mode) === EnumTableCardMode.TABLE && setTableModeData();
  238 + }
  239 + } catch (error) {}
  240 + },
  241 + onDisconnected() {
  242 + // close();
  243 + },
  244 + onError() {
  245 + createMessage.error('webSocket连接超时,请联系管理员');
  246 + },
  247 + });
  248 +
  249 + function setTableModeData() {
  250 + setTableData(socketInfo.dataSource.filter((item) => !isNull(item.value)));
  251 + }
  252 +
  253 + const handleShowDetail = (record: SocketInfoDataSourceItemType) => {
  254 + const { key } = record;
  255 + socketInfo.attr = key;
  256 + openModal(true);
  257 + };
  258 +
  259 + const getIsModbusDevice = computed(
  260 + () =>
  261 + props.deviceDetail.deviceData.transportConfiguration.type === TransportTypeEnum.TCP &&
  262 + props.deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol ===
  263 + TCPProtocolTypeEnum.MODBUS_RTU
  264 + );
  265 +
  266 + onMounted(async () => {
  267 + const { deviceProfileId } = props.deviceDetail;
  268 + const { id } = deviceProfileId;
  269 + const value = await getDeviceAttrs({ deviceProfileId: id });
  270 + socketInfo.attrKeys = isArray(value) ? value.map((item) => item.identifier) : [];
  271 + socketInfo.rawDataSource = buildTableDataSourceByObjectModel(
  272 + value as never as DeviceModelOfMatterAttrs[],
  273 + props.deviceDetail
  274 + );
  275 + setDataSource();
  276 + open();
  277 + });
  278 +
  279 + const formatValue = (item: SocketInfoDataSourceItemType) => {
  280 + if (isNullOrUnDef(item) || isNullOrUnDef(item.value)) return '--';
  281 + if (unref(getIsModbusDevice) && item.type === DataTypeEnum.BOOL) {
  282 + const _result = Reflect.get(item.enum || {}, item.value as string);
  283 + return isNullOrUnDef(_result) ? item.value : _result;
  284 + }
  285 + switch (item.type) {
  286 + case DataTypeEnum.BOOL:
  287 + return !!Number(item.value) ? item.boolOpen : item.boolClose;
  288 + case DataTypeEnum.ENUM:
  289 + return item.enum?.[item.value as string];
  290 + default:
  291 + return item.value || '--';
  292 + }
  293 + };
  294 +
  295 + const [register, { openModal: openSendCommandModal }] = useModal();
  296 + const handleSendCommandModal = (data: SocketInfoDataSourceItemType) => {
  297 + openSendCommandModal(true, {
  298 + mode: DataActionModeEnum.READ,
  299 + record: {
  300 + objectModel: toRaw(unref(data.detail)),
  301 + deviceDetail: props.deviceDetail as EdgeDeviceItemType,
  302 + },
  303 + } as ModalParamsType);
  304 + };
  305 +
  306 + onMounted(() => {
  307 + const element = unref(listElRef)?.$el;
  308 + if (!element) return;
  309 + const totalHeight = document.documentElement.clientHeight;
  310 + const { top } = element?.getBoundingClientRect() || {};
  311 + const containerEl = element.querySelector('.ant-spin-container') as HTMLDivElement;
  312 + containerEl.style.height = `${totalHeight - top - 20 - 50}px`;
  313 + containerEl.style.overflowY = 'auto';
  314 + containerEl.style.overflowX = 'hidden';
  315 + });
  316 +
  317 + onUnmounted(() => close());
  318 +</script>
  319 +
  320 +<template>
  321 + <PageWrapper
  322 + dense
  323 + content-class="flex flex-col p-4 dark:text-gray-300 dark:bg-dark-700 bg-gray-100"
  324 + >
  325 + <section
  326 + class="flex flex-col justify-between w-full bg-light-50 pt-3 mb-4 dark:text-gray-300 dark:bg-dark-900"
  327 + >
  328 + <div class="flex-auto">
  329 + <BasicForm @register="registerForm" />
  330 + </div>
  331 + </section>
  332 + <section class="bg-light-50 !dark:text-gray-300 !dark:bg-dark-900">
  333 + <div
  334 + v-show="mode === EnumTableCardMode.CARD"
  335 + class="flex h-70px items-center justify-end p-2"
  336 + >
  337 + <ModeSwitchButton v-model:value="mode" @change="switchMode" />
  338 + </div>
  339 + <List
  340 + v-show="mode === EnumTableCardMode.CARD"
  341 + ref="listElRef"
  342 + class="list-mode !px-2"
  343 + :data-source="socketInfo.dataSource"
  344 + :grid="grid"
  345 + :pagination="pagination"
  346 + >
  347 + <template #renderItem="{ item }: { item: SocketInfoDataSourceItemType }">
  348 + <List.Item>
  349 + <Card class="shadow-md">
  350 + <template #title>
  351 + <span class="text-base font-normal mr-2">{{ item.name }}</span>
  352 + </template>
  353 + <template #extra>
  354 + <Space>
  355 + <Tooltip
  356 + :title="item.expand ? '收起' : '展开'"
  357 + v-if="item.type === DataTypeEnum.STRUCT"
  358 + >
  359 + <Icon
  360 + :icon="item.expand ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"
  361 + class="cursor-pointer svg:text-blue-500"
  362 + @click="item.expand = !item.expand"
  363 + />
  364 + </Tooltip>
  365 + <Tooltip title="属性下发">
  366 + <SvgIcon
  367 + name="send-command"
  368 + prefix="iconfont"
  369 + v-if="ReadAndWriteEnum.READ_AND_WRITE === item.accessMode"
  370 + class="cursor-pointer text-lg text-blue-500"
  371 + @click="handleSendCommandModal(item)"
  372 + />
  373 + </Tooltip>
  374 +
  375 + <Tooltip title="历史趋势">
  376 + <AreaChartOutlined
  377 + class="cursor-pointer text-lg svg:fill-blue-500"
  378 + @click="handleShowDetail(item)"
  379 + />
  380 + </Tooltip>
  381 + </Space>
  382 + </template>
  383 + <section class="min-h-16 flex flex-col justify-between">
  384 + <div
  385 + v-if="item.type !== DataTypeEnum.STRUCT"
  386 + class="flex font-bold text-lg mb-4 gap-2"
  387 + >
  388 + <Tooltip :title="formatValue(item) as any" placement="topLeft">
  389 + <div class="truncate">{{ formatValue(item) }}</div>
  390 + </Tooltip>
  391 + <div class="text-xs flex items-center">{{ item.unitName }}</div>
  392 + </div>
  393 + <div
  394 + class="mb-4 overflow-hidden flex flex-col gap-2 relative"
  395 + v-if="item.type == DataTypeEnum.STRUCT && isObject(item.value)"
  396 + :style="{ height: item.expand ? 'fit-content' : '28px' }"
  397 + >
  398 + <div
  399 + v-for="key in Object.keys(item.value)"
  400 + :key="key"
  401 + class="cursor-pointer flex justify-between items-center gap-2"
  402 + >
  403 + <div class="font-medium whitespace-nowrap">{{ item.value[key].name }}</div>
  404 + <div class="flex-auto font-bold text-lg text-left ml-4 truncate">
  405 + <Tooltip
  406 + :title="formatValue(item.value[key] as SocketInfoDataSourceItemType) as any"
  407 + placement="topLeft"
  408 + >
  409 + <span>
  410 + {{ formatValue(item.value[key] as SocketInfoDataSourceItemType) }}
  411 + </span>
  412 + </Tooltip>
  413 + <span class="text-xs font-normal ml-2">
  414 + {{ item.value[key].unitName }}
  415 + </span>
  416 + </div>
  417 + </div>
  418 + <Tooltip title="点击展开">
  419 + <div
  420 + v-show="!item.expand"
  421 + class="absolute top-2 right-0 text-blue-400 cursor-pointer text-xs"
  422 + @click="item.expand = !item.expand"
  423 + >
  424 + 更多
  425 + </div>
  426 + </Tooltip>
  427 + </div>
  428 + <div class="text-dark-800 text-xs">
  429 + {{
  430 + item.time && item?.rawValue
  431 + ? formatToDateTime(item.time, 'YYYY-MM-DD HH:mm:ss')
  432 + : '--'
  433 + }}
  434 + </div>
  435 + </section>
  436 + </Card>
  437 + </List.Item>
  438 + </template>
  439 + </List>
  440 + </section>
  441 + <BasicTable
  442 + v-if="mode === EnumTableCardMode.TABLE"
  443 + @register="registerTable"
  444 + class="device-things-model-table-mode"
  445 + >
  446 + <template #toolbar>
  447 + <div
  448 + v-show="mode === EnumTableCardMode.TABLE"
  449 + class="flex h-70px items-center justify-end p-2"
  450 + >
  451 + <ModeSwitchButton v-model:value="mode" @change="switchMode" />
  452 + </div>
  453 + </template>
  454 + </BasicTable>
  455 + <BasicModal
  456 + @register="registerModal"
  457 + title="历史数据"
  458 + width="50%"
  459 + destroy-on-close
  460 + dialogClass="history-modal"
  461 + >
  462 + <HistoryData :deviceDetail="props.deviceDetail" :attr="socketInfo.attr" />
  463 + </BasicModal>
  464 + <ObjectModelCommandDeliveryModal
  465 + @register="register"
  466 + :deviceId="deviceDetail.id.id"
  467 + :device-name="deviceDetail.name"
  468 + />
  469 + </PageWrapper>
  470 +</template>
  471 +
  472 +<style scoped lang="less">
  473 + .list-mode:deep(.ant-card-head) {
  474 + border-bottom: 0;
  475 + }
  476 +
  477 + .list-mode:deep(.ant-card-body) {
  478 + padding-top: 0;
  479 + }
  480 +
  481 + .list-mode:deep(.ant-card-head-title) {
  482 + height: 64px;
  483 + display: flex;
  484 + align-items: center;
  485 + }
  486 +
  487 + .device-things-model-table-mode:deep(.ant-table-placeholder) {
  488 + height: auto;
  489 + }
  490 +
  491 + .model-collapse {
  492 + border: none;
  493 +
  494 + :deep(.ant-collapse-header) {
  495 + display: none;
  496 + }
  497 +
  498 + :deep(.ant-collapse-item) {
  499 + border: none;
  500 + }
  501 +
  502 + :deep(.ant-collapse-content) {
  503 + border: none;
  504 + }
  505 +
  506 + :deep(.ant-collapse-content-box) {
  507 + padding: 0;
  508 + }
  509 + }
  510 +</style>
  511 +
  512 +<style>
  513 + .history-modal .ant-input-number {
  514 + min-width: 0 !important;
  515 + width: 100% !important;
  516 + }
  517 +</style>
  1 +export interface DeviceType {
  2 + alarmStatus: number;
  3 + createTime: string;
  4 + creator: string;
  5 + deviceInfo: Object;
  6 + deviceProfile: Object;
  7 + deviceState: string;
  8 + deviceToken: string;
  9 + deviceType: string;
  10 + enabled: boolean;
  11 + id: string;
  12 + key: string;
  13 + label: string;
  14 + name: string;
  15 + organizationDTO: Object;
  16 + organizationId: string;
  17 + profileId: string;
  18 + tenantCode: string;
  19 + updateTime: string;
  20 +}
  21 +
  22 +export interface socketDataType {
  23 + key: string;
  24 + value: string;
  25 + time: number;
  26 +}
  1 +import { DescItem } from '/@/components/Description/src/typing';
  2 +
  3 +export const descSchema = (): DescItem[] => {
  4 + return [
  5 + {
  6 + field: 'name',
  7 + label: '设备名称',
  8 + },
  9 + {
  10 + field: 'label',
  11 + label: '设备标签',
  12 + },
  13 + {
  14 + field: 'routingKey',
  15 + label: '边缘键',
  16 + },
  17 + {
  18 + field: 'secret',
  19 + label: '边缘密钥',
  20 + },
  21 + {
  22 + field: 'createdTime',
  23 + label: '创建时间',
  24 + },
  25 + {
  26 + field: 'additionalInfo.description',
  27 + label: '描述',
  28 + },
  29 + ];
  30 +};
  1 +export { default as EdgeDeviceTabInfo } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { Tabs } from 'ant-design-vue';
  3 + import { ref } from 'vue';
  4 + import type { PropType } from 'vue';
  5 + import { EdgeDevicePhysicalModel } from '../EdgeDevicePhysicalModel';
  6 + import { EdgeDeviceAlarm } from '../EdgeDeviceAlarm';
  7 + import { EdgeDeviceEvent } from '../EdgeDeviceEvent';
  8 + import { EdgeDeviceCommand } from '../EdgeDeviceCommand';
  9 + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance';
  10 +
  11 + defineProps({
  12 + recordData: {
  13 + type: Object as PropType<EdgeDeviceItemType>,
  14 + default: () => {},
  15 + },
  16 + });
  17 +
  18 + enum ActiveKey {
  19 + PHYSICALMODEL = 'PhysicalModel',
  20 + ALARM = 'Alarm',
  21 + COMMANDISSUANCERECORD = 'CommandIssuanceRecord',
  22 + EVENTMANAGEMENT = 'EventManagement',
  23 + }
  24 +
  25 + const activeKey = ref(ActiveKey.PHYSICALMODEL);
  26 +</script>
  27 +
  28 +<template>
  29 + <section class="w-full h-full p-1">
  30 + <main>
  31 + <Tabs
  32 + v-model:activeKey="activeKey"
  33 + type="card"
  34 + class="w-full h-full bg-light-50 !p-4 dark:bg-dark-900"
  35 + >
  36 + <Tabs.TabPane tab="物模型" :key="ActiveKey.PHYSICALMODEL">
  37 + <EdgeDevicePhysicalModel :deviceDetail="recordData" />
  38 + </Tabs.TabPane>
  39 + <Tabs.TabPane tab="告警" :key="ActiveKey.ALARM">
  40 + <EdgeDeviceAlarm :recordData="recordData" />
  41 + </Tabs.TabPane>
  42 + <Tabs.TabPane tab="命令下发" :key="ActiveKey.COMMANDISSUANCERECORD">
  43 + <EdgeDeviceCommand :recordData="recordData" />
  44 + </Tabs.TabPane>
  45 + <Tabs.TabPane tab="事件管理" :key="ActiveKey.EVENTMANAGEMENT">
  46 + <EdgeDeviceEvent :recordData="recordData" />
  47 + </Tabs.TabPane>
  48 + </Tabs>
  49 + </main>
  50 + </section>
  51 +</template>
  52 +
  53 +<style lang="less" scoped></style>
  1 +import { Tag } from 'ant-design-vue';
  2 +import { BasicColumn } from '/@/components/Table';
  3 +import { formatToDateTime } from '/@/utils/dateUtil';
  4 +import { h } from 'vue';
  5 +
  6 +// 表格配置
  7 +export const columns: BasicColumn[] = [
  8 + {
  9 + title: '事件时间',
  10 + dataIndex: 'createdTime',
  11 + format(text) {
  12 + return formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss');
  13 + },
  14 + },
  15 + {
  16 + title: '服务器',
  17 + dataIndex: 'body.server',
  18 + },
  19 + {
  20 + title: '事件',
  21 + dataIndex: 'body.event',
  22 + },
  23 + {
  24 + title: '状态',
  25 + dataIndex: 'body.success',
  26 + customRender: ({ record }) => {
  27 + const color = record.body.success ? 'success' : 'error';
  28 + const text = record.body.success ? '成功' : '失败';
  29 + return h(Tag, { color: color }, () => text);
  30 + },
  31 + },
  32 + {
  33 + title: '错误',
  34 + dataIndex: 'error',
  35 + slots: { customRender: 'errorDetail' },
  36 + },
  37 +];
  1 +export { default as EdgeEvents } from './index.vue';
  1 +<script lang="ts" setup>
  2 + import { BasicTable, useTable } from '/@/components/Table';
  3 + import { columns } from './config';
  4 + import { edgeEventPage } from '/@/api/edgeManage/edgeInstance';
  5 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  6 + import { BasicModal, useModal } from '/@/components/Modal';
  7 + import { Input, Button } from 'ant-design-vue';
  8 + import { ref } from 'vue';
  9 +
  10 + const props = defineProps({
  11 + recordData: {
  12 + type: Object as PropType<EdgeInstanceItemType>,
  13 + default: () => {},
  14 + },
  15 + });
  16 +
  17 + const outputData = ref<string>();
  18 +
  19 + const [registerTable] = useTable({
  20 + columns,
  21 + api: async ({ page, pageSize }) => {
  22 + const res = await edgeEventPage(
  23 + {
  24 + page: page - 1 < 0 ? 0 : page - 1,
  25 + pageSize,
  26 + tenantId: props?.recordData?.tenantId?.id,
  27 + },
  28 + props?.recordData?.id?.id
  29 + );
  30 + return {
  31 + total: res?.totalElements,
  32 + items: res?.data,
  33 + };
  34 + },
  35 + showIndexColumn: false,
  36 + clickToRowSelect: false,
  37 + showTableSetting: true,
  38 + bordered: true,
  39 + });
  40 +
  41 + const [registerModal, { openModal, closeModal }] = useModal();
  42 +
  43 + const handleEventIsDetail = (record: Recordable) => {
  44 + outputData.value = record?.body?.error;
  45 + openModal(true);
  46 + };
  47 +</script>
  48 +
  49 +<template>
  50 + <div>
  51 + <BasicTable :clickToRowSelect="false" @register="registerTable">
  52 + <template #errorDetail="{ record }">
  53 + <Button
  54 + v-if="record?.body?.error"
  55 + type="link"
  56 + class="ml-2"
  57 + @click="handleEventIsDetail(record)"
  58 + >
  59 + 查看
  60 + </Button>
  61 + <Button v-else type="link" class="ml-2"> - </Button>
  62 + </template>
  63 + </BasicTable>
  64 + <BasicModal title="详情" @register="registerModal" @ok="closeModal">
  65 + <Input.TextArea v-model:value="outputData" :autosize="true" />
  66 + </BasicModal>
  67 + </div>
  68 +</template>
  1 +<script lang="ts" setup>
  2 + import { nextTick, ref, unref, computed } from 'vue';
  3 + import { BasicForm, useForm } from '/@/components/Form';
  4 + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  5 + import { FormFieldsEnum, formSchema } from './config';
  6 + import { HandleOperationEnum } from '../../config';
  7 + import { useMessage } from '/@/hooks/web/useMessage';
  8 + import { createOrEditEdgeInstance } from '/@/api/edgeManage/edgeInstance';
  9 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  10 + import { buildUUID, randomString } from '/@/utils/uuid';
  11 + import { CopyOutlined } from '@ant-design/icons-vue';
  12 + import { handeleCopy } from '/@/views/device/profiles/step/topic';
  13 +
  14 + const emits = defineEmits(['success', 'register']);
  15 +
  16 + const { createMessage } = useMessage();
  17 +
  18 + const isUpdate = ref<Boolean>();
  19 +
  20 + const isUpdateText = ref<String>();
  21 +
  22 + const recordData = ref<EdgeInstanceItemType>();
  23 +
  24 + const [registerForm, { resetFields, validate, setFieldsValue }] = useForm({
  25 + labelWidth: 120,
  26 + schemas: formSchema,
  27 + showActionButtonGroup: false,
  28 + });
  29 +
  30 + const cacheTitle = computed(() => (!unref(isUpdate) ? '新增实例' : '编辑实例'));
  31 +
  32 + const handleCopyRoutingKey = (text: string) => handeleCopy(text);
  33 +
  34 + const handleCopySecret = (text: string) => handeleCopy(text);
  35 +
  36 + const setDefaultValue = (event: HandleOperationEnum) => {
  37 + if (event === HandleOperationEnum.CREATE) {
  38 + setFieldsValue({
  39 + [FormFieldsEnum.ROUTINGKEY]: buildUUID(),
  40 + [FormFieldsEnum.SECRET]: randomString(),
  41 + });
  42 + }
  43 + };
  44 +
  45 + const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
  46 + setDrawerProps({ loading: true });
  47 + await resetFields();
  48 + setDefaultValue(data.event);
  49 + isUpdate.value = data.isUpdate;
  50 + isUpdateText.value = data.isUpdateText;
  51 + recordData.value = data.record;
  52 + try {
  53 + await nextTick();
  54 + setFieldsValue(data.record);
  55 + setFieldsValue({
  56 + [FormFieldsEnum.DESCRIPTION]: data.record?.additionalInfo[FormFieldsEnum.DESCRIPTION],
  57 + });
  58 + } finally {
  59 + setDrawerProps({ loading: false });
  60 + }
  61 + });
  62 +
  63 + const getValue = async () => {
  64 + const values = await validate();
  65 + if (!values) return;
  66 + const additionalInfo = {
  67 + description: values[FormFieldsEnum.DESCRIPTION],
  68 + };
  69 + const mergeValues = {
  70 + ...recordData.value,
  71 + ...values,
  72 + ...{ additionalInfo },
  73 + };
  74 + Reflect.deleteProperty(mergeValues, FormFieldsEnum.DESCRIPTION);
  75 + await createOrEditEdgeInstance(mergeValues);
  76 + createMessage.success(`${isUpdateText.value}成功`);
  77 + closeDrawer();
  78 + setTimeout(() => {
  79 + emits('success');
  80 + }, 500);
  81 + };
  82 +
  83 + const handleSubmit = () => getValue();
  84 +</script>
  85 +
  86 +<template>
  87 + <div>
  88 + <BasicDrawer
  89 + destroyOnClose
  90 + v-bind="$attrs"
  91 + showFooter
  92 + :title="cacheTitle"
  93 + width="30%"
  94 + :maskClosable="true"
  95 + @register="registerDrawer"
  96 + @ok="handleSubmit"
  97 + >
  98 + <BasicForm @register="registerForm">
  99 + <template #routingKey="{ model, field }">
  100 + <div class="!flex justify-between items-center gap-5">
  101 + <a-input disabled v-model:value="model[field]" />
  102 + <CopyOutlined
  103 + @click="handleCopyRoutingKey(model[field])"
  104 + class="cursor-pointer"
  105 + style="font-size: 32px"
  106 + />
  107 + </div>
  108 + </template>
  109 + <template #secret="{ model, field }">
  110 + <div class="!flex justify-between items-center gap-5">
  111 + <a-input disabled v-model:value="model[field]" />
  112 + <CopyOutlined
  113 + @click="handleCopySecret(model[field])"
  114 + class="cursor-pointer"
  115 + style="font-size: 32px"
  116 + />
  117 + </div>
  118 + </template>
  119 + </BasicForm>
  120 + </BasicDrawer>
  121 + </div>
  122 +</template>
  123 +
  124 +<style lang="less" scoped></style>
  1 +import { FormSchema } from '/@/components/Form';
  2 +
  3 +export enum FormFieldsEnum {
  4 + NAME = 'name',
  5 + LABEL = 'label',
  6 + ROUTINGKEY = 'routingKey',
  7 + SECRET = 'secret',
  8 + TYPE = 'type',
  9 + DESCRIPTION = 'description',
  10 + TEXTSEARCH = 'textSearch',
  11 +}
  12 +
  13 +enum FormFieldsNameEnum {
  14 + NAME = '名称',
  15 + LABEL = '标签',
  16 + ROUTINGKEY = '边缘Key',
  17 + SECRET = '边缘密钥',
  18 + TYPE = '边缘类型',
  19 + DESCRIPTION = '描述',
  20 + TEXTSEARCH = '实例名称',
  21 +}
  22 +
  23 +export const formSchema: FormSchema[] = [
  24 + {
  25 + field: FormFieldsEnum.NAME,
  26 + label: FormFieldsNameEnum.NAME,
  27 + component: 'Input',
  28 + required: true,
  29 + componentProps: {
  30 + maxLength: 255,
  31 + placeholder: '请输入实例名称',
  32 + },
  33 + },
  34 + {
  35 + field: FormFieldsEnum.TYPE,
  36 + label: FormFieldsNameEnum.TYPE,
  37 + component: 'Input',
  38 + required: true,
  39 + defaultValue: 'default',
  40 + componentProps: {
  41 + disabled: true,
  42 + },
  43 + },
  44 + {
  45 + field: FormFieldsEnum.ROUTINGKEY,
  46 + label: FormFieldsNameEnum.ROUTINGKEY,
  47 + slot: 'routingKey',
  48 + required: true,
  49 + component: 'Input',
  50 + },
  51 + {
  52 + field: FormFieldsEnum.SECRET,
  53 + label: FormFieldsNameEnum.SECRET,
  54 + slot: 'secret',
  55 + required: true,
  56 + component: 'Input',
  57 + },
  58 + {
  59 + field: FormFieldsEnum.LABEL,
  60 + label: FormFieldsNameEnum.LABEL,
  61 + component: 'Input',
  62 + componentProps: {
  63 + maxLength: 255,
  64 + placeholder: '请输入标签',
  65 + },
  66 + },
  67 + {
  68 + field: FormFieldsEnum.DESCRIPTION,
  69 + label: FormFieldsNameEnum.DESCRIPTION,
  70 + component: 'Input',
  71 + componentProps: {
  72 + maxLength: 255,
  73 + placeholder: '请输入描述',
  74 + },
  75 + },
  76 +];
  77 +
  78 +export const searchFormSchema: FormSchema[] = [
  79 + {
  80 + field: FormFieldsEnum.TEXTSEARCH,
  81 + label: FormFieldsNameEnum.TEXTSEARCH,
  82 + component: 'Input',
  83 + colProps: { span: 6 },
  84 + componentProps: {
  85 + maxLength: 255,
  86 + placeholder: '请输入实例名称',
  87 + },
  88 + },
  89 +];
  1 +export { default as EdgeInstanceDetailDrawer } from './index.vue';
  2 +export { default as EdgeInstanceFormDrawer } from './EdgeInstanceFormDrawer.vue';
  1 +<script lang="ts" setup>
  2 + import { ref } from 'vue';
  3 + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance';
  4 + import { infoEdgeInstance } from '/@/api/edgeManage/edgeInstance';
  5 + import { EdgeInstanceBasicInfo } from '../EdgeInstanceBasicInfo';
  6 + import { EdgeInstanceTabInfo } from '../EdgeInstanceTabInfo';
  7 + import { useRoute } from 'vue-router';
  8 + import { PageWrapper } from '/@/components/Page';
  9 + import { useGo } from '/@/hooks/web/usePage';
  10 +
  11 + defineEmits(['register']);
  12 +
  13 + defineOptions({ name: 'EdgeDetail' });
  14 +
  15 + const route = useRoute();
  16 +
  17 + const go = useGo();
  18 +
  19 + const edgeId = ref(route.params?.id);
  20 +
  21 + const recordData = ref<EdgeInstanceItemType>();
  22 +
  23 + infoEdgeInstance(edgeId.value as string).then((res) => {
  24 + recordData.value = res;
  25 + });
  26 +
  27 + function goBack() {
  28 + go('/edge/instance');
  29 + }
  30 +</script>
  31 +
  32 +<template>
  33 + <PageWrapper :title="`边缘详情`" contentBackground @back="goBack">
  34 + <!-- 基础信息 -->
  35 + <div class="m-4">
  36 + <EdgeInstanceBasicInfo v-if="recordData" :recordData="recordData" />
  37 + </div>
  38 + <!-- Tab信息 -->
  39 + <div>
  40 + <EdgeInstanceTabInfo v-if="recordData" :recordData="recordData" />
  41 + </div>
  42 + </PageWrapper>
  43 +</template>
  44 +
  45 +<style lang="less" scoped></style>
  1 +import { Tag } from 'ant-design-vue';
  2 +import { h } from 'vue';
  3 +import { DescItem } from '/@/components/Description/src/typing';
  4 +import { handeleCopy } from '/@/views/device/profiles/step/topic';
  5 +import { formatToDateTime } from '/@/utils/dateUtil';
  6 +
  7 +export const descSchema = (): DescItem[] => {
  8 + return [
  9 + {
  10 + field: 'name',
  11 + label: '边缘名称',
  12 + },
  13 + {
  14 + field: 'label',
  15 + label: '设备标签',
  16 + render: (text) => {
  17 + return h(
  18 + Tag,
  19 + {
  20 + color: '#00B42A',
  21 + },
  22 + text
  23 + );
  24 + },
  25 + },
  26 + {
  27 + field: 'routingKey',
  28 + label: '边缘键',
  29 + render: (text) => {
  30 + return h(
  31 + 'span',
  32 + {
  33 + style: { cursor: 'pointer', color: '#165DFF' },
  34 + onClick: () => {
  35 + handeleCopy(text);
  36 + },
  37 + },
  38 + text
  39 + );
  40 + },
  41 + },
  42 + {
  43 + field: 'secret',
  44 + label: '边缘密钥',
  45 + render: (text) => {
  46 + return h(
  47 + 'span',
  48 + {
  49 + style: { cursor: 'pointer', color: '#165DFF' },
  50 + onClick: () => {
  51 + handeleCopy(text);
  52 + },
  53 + },
  54 + text
  55 + );
  56 + },
  57 + },
  58 + {
  59 + field: 'createdTime',
  60 + label: '创建时间',
  61 + render: (_, data) => {
  62 + return formatToDateTime(data.createdTime, 'YYYY-MM-DD HH:mm:ss');
  63 + },
  64 + },
  65 + {
  66 + field: 'additionalInfo.description',
  67 + label: '描述',
  68 + },
  69 + ];
  70 +};
  1 +export { default as EdgeInstanceBasicInfo } from './index.vue';