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 | 9 | ASC = 'ASC', |
10 | 10 | DESC = 'DESC', |
11 | 11 | } |
12 | +enum SortProperty { | |
13 | + CREATEtIME = 'createdTime', | |
14 | +} | |
12 | 15 | |
13 | 16 | export interface BaseQueryParams { |
14 | 17 | pageSize: number; |
15 | 18 | page: number; |
16 | 19 | orderFiled?: string; |
17 | 20 | orderType?: OrderType; |
21 | + sortProperty?: SortProperty; | |
18 | 22 | } |
19 | 23 | |
20 | 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 | 4 | |
5 | 5 | menuMap.set('/visual/board/detail/:boardId/:boardName/:platform/:organizationId?', '/visual/board'); |
6 | 6 | menuMap.set('/rule/chain/:id', '/rule/chain'); |
7 | +menuMap.set('/edge/edge_detail/:id', '/edge'); | |
7 | 8 | |
8 | 9 | export const useMenuActiveFix = (route: RouteLocationNormalizedLoaded) => { |
9 | 10 | let flag = false; | ... | ... |
... | ... | @@ -173,7 +173,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) { |
173 | 173 | // authentication schemes,e.g: Bearer |
174 | 174 | // authenticationScheme: 'Bearer', |
175 | 175 | authenticationScheme: 'Bearer', |
176 | - timeout: 10 * 1000, | |
176 | + timeout: 26 * 1000, | |
177 | 177 | // 基础接口地址 |
178 | 178 | // baseURL: globSetting.apiUrl, |
179 | 179 | // 接口可能会有通用的地址部分,可以统一抽取出来 | ... | ... |
... | ... | @@ -89,3 +89,18 @@ export const withInstall = <T>(component: T, alias?: string) => { |
89 | 89 | }; |
90 | 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'; | ... | ... |