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 { |
src/api/edgeManage/edgeInstance.ts
0 → 100644
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 | +}; |
src/api/edgeManage/model/edgeInstance.ts
0 → 100644
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 | +} |
src/assets/images/edgeDevice.png
0 → 100644
1.62 KB
src/assets/images/edgeInstance.png
0 → 100644
13.9 KB
src/assets/images/edgeStatusIsOffline.png
0 → 100644
388 Bytes
src/assets/images/edgeStatusIsOnline.png
0 → 100644
366 Bytes
@@ -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 | +<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 | +} |
src/views/edge/instance/components/EdgeDevicePhysicalModel/ObjectModelCommandDeliveryModal/index.ts
0 → 100644
1 | +export { default as ObjectModelCommandDeliveryModal } from './index.vue'; |
src/views/edge/instance/components/EdgeDevicePhysicalModel/ObjectModelCommandDeliveryModal/index.vue
0 → 100644
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 | +<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'; |