Commit 2dcb746a4bad300319cf1a73e86f74836d80852b
Merge branch 'main_dev' into 'main'
Main dev See merge request yunteng/thingskit-front!1449
Showing
90 changed files
with
3658 additions
and
115 deletions
Too many changes to show.
To preserve performance only 90 of 163 files are displayed.
Dockerfile
0 → 100644
default.conf
0 → 100644
1 | +server { | |
2 | + listen 80; | |
3 | + listen [::]:80; | |
4 | + server_name 192.168.1.48; | |
5 | + #charset koi8-r; | |
6 | + #access_log /var/log/nginx/host.access.log main; | |
7 | + | |
8 | + root /usr/share/nginx/html; | |
9 | + index index.html index.htm; | |
10 | + try_files $uri $uri/ /index.html; | |
11 | + | |
12 | + | |
13 | + | |
14 | + location /api/ { | |
15 | + proxy_pass http://192.168.1.48:8080; | |
16 | + proxy_http_version 1.1; | |
17 | + proxy_set_header Upgrade $http_upgrade; | |
18 | + proxy_set_header Connection "Upgrade"; | |
19 | + proxy_set_header Host $host; | |
20 | + proxy_cache_bypass $http_upgrade; | |
21 | + } | |
22 | + | |
23 | + location /yt/ { | |
24 | + proxy_pass http://192.168.1.48:8080; | |
25 | + proxy_http_version 1.1; | |
26 | + proxy_set_header Upgrade $http_upgrade; | |
27 | + proxy_set_header Connection "Upgrade"; | |
28 | + proxy_set_header Host $host; | |
29 | + proxy_cache_bypass $http_upgrade; | |
30 | + } | |
31 | + | |
32 | + location /upload/ { | |
33 | + proxy_pass http://192.168.1.48:8080; | |
34 | + proxy_http_version 1.1; | |
35 | + proxy_set_header Upgrade $http_upgrade; | |
36 | + proxy_set_header Connection "Upgrade"; | |
37 | + proxy_set_header Host $host; | |
38 | + proxy_cache_bypass $http_upgrade; | |
39 | + } | |
40 | + | |
41 | + #error_page 404 /404.html; | |
42 | + | |
43 | + # redirect server error pages to the static page /50x.html | |
44 | + # | |
45 | + error_page 500 502 503 504 /50x.html; | |
46 | + location = /50x.html { | |
47 | + root /usr/share/nginx/html; | |
48 | + } | |
49 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -5,10 +5,13 @@ import { isString } from '/@/utils/is'; |
5 | 5 | import { OrderByEnum } from '/@/views/device/localtion/cpns/TimePeriodForm/config'; |
6 | 6 | |
7 | 7 | // 获取设备配置,很多地方使用,不能修改这里 |
8 | -export const getDeviceProfile = (deviceType?: string) => { | |
8 | +export const getDeviceProfile = (deviceType?: string, isSceneLinkage?: Boolean) => { | |
9 | 9 | return defHttp.get<DeviceProfileModel[]>({ |
10 | 10 | url: '/device_profile/me/list', |
11 | - params: { deviceType: isString(deviceType) ? deviceType : undefined }, | |
11 | + params: { | |
12 | + deviceType: isString(deviceType) ? deviceType : undefined, | |
13 | + isSceneLinkage: isSceneLinkage ? isSceneLinkage : undefined, | |
14 | + }, | |
12 | 15 | }); |
13 | 16 | }; |
14 | 17 | ... | ... |
... | ... | @@ -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 { | ... | ... |
... | ... | @@ -264,13 +264,16 @@ export const getGATEWAYdevice = async (params: { |
264 | 264 | ); |
265 | 265 | }; |
266 | 266 | |
267 | -export const getGatewayDevice = (params: Record<'organizationId' | 'transportType', string>) => { | |
268 | - const { organizationId, transportType } = params; | |
267 | +export const getGatewayDevice = ( | |
268 | + params: Record<'organizationId' | 'transportType' | 'gatewayId', string> | |
269 | +) => { | |
270 | + const { organizationId, transportType, gatewayId } = params; | |
269 | 271 | return defHttp.get<DeviceRecord[]>({ |
270 | 272 | url: DeviceManagerApi.GATEWAY_DEVICE, |
271 | 273 | params: { |
272 | 274 | organizationId, |
273 | 275 | transportType, |
276 | + gatewayId, | |
274 | 277 | }, |
275 | 278 | }); |
276 | 279 | }; | ... | ... |
... | ... | @@ -102,3 +102,9 @@ export interface VideoChanneControlType { |
102 | 102 | tbDeviceId?: string | number | object; |
103 | 103 | channelId?: string | number | object; |
104 | 104 | } |
105 | + | |
106 | +export interface EzvizControlType { | |
107 | + entityId: string; | |
108 | + action: number; | |
109 | + controllingType: number; | |
110 | +} | ... | ... |
... | ... | @@ -3,6 +3,7 @@ import { |
3 | 3 | VideoChannelPlayAddressType, |
4 | 4 | VideoChannelQueryParamsType, |
5 | 5 | VideoChanneControlType, |
6 | + EzvizControlType, | |
6 | 7 | } from './model/videoChannelModel'; |
7 | 8 | import { PaginationResult } from '/#/axios'; |
8 | 9 | import { defHttp } from '/@/utils/http/axios'; |
... | ... | @@ -12,6 +13,7 @@ enum Api { |
12 | 13 | GET_VIDEO_CONTROL_START = '/video/control/start', |
13 | 14 | GET_VIDEO_CONTROL_STOP = '/video/control/stop', |
14 | 15 | SET_VIDEO_CONTROL_CONTROL = '/video/control/control', |
16 | + SET_EZVIZ_CONTROL = '/video/ezviz/controlling', | |
15 | 17 | } |
16 | 18 | |
17 | 19 | export const getVideoChannelList = (params: VideoChannelQueryParamsType) => { |
... | ... | @@ -43,3 +45,10 @@ export const setVideoControl = (tbDeviceId, channelId, params: VideoChanneContro |
43 | 45 | params, |
44 | 46 | }); |
45 | 47 | }; |
48 | + | |
49 | +export const setEzvizControl = (params: EzvizControlType) => { | |
50 | + return defHttp.get({ | |
51 | + url: Api.SET_EZVIZ_CONTROL, | |
52 | + params, | |
53 | + }); | |
54 | +}; | ... | ... |
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 { | |
2 | + Configuration, | |
3 | + ProvisionConfiguration, | |
4 | + TransportConfiguration, | |
5 | +} from '../../device/model/deviceModel'; | |
6 | +import { ModelOfMatterParams } from '../../device/model/modelOfMatterModel'; | |
7 | + | |
8 | +export type QueryEdgeInstancePageParams = { | |
9 | + name?: string; | |
10 | + tenantId?: string; | |
11 | + textSearch?: string; | |
12 | + page: number; | |
13 | + pageSize: number; | |
14 | + sortOrder?: string; | |
15 | + sortProperty?: string; | |
16 | +}; | |
17 | + | |
18 | +export interface EdgeInstanceItemType { | |
19 | + id?: { | |
20 | + entityType: string; | |
21 | + id: string; | |
22 | + }; | |
23 | + createdTime?: number; | |
24 | + tenantId?: { | |
25 | + entityType: string; | |
26 | + id: string; | |
27 | + }; | |
28 | + customerId?: { | |
29 | + entityType: string; | |
30 | + id: string; | |
31 | + }; | |
32 | + rootRuleChainId?: { | |
33 | + entityType: string; | |
34 | + id: string; | |
35 | + }; | |
36 | + name: string; | |
37 | + label: string; | |
38 | + additionalInfo: { | |
39 | + description: string; | |
40 | + }; | |
41 | + status: number; | |
42 | + type: string; | |
43 | + routingKey: string; | |
44 | + secret: string; | |
45 | + active?: boolean; | |
46 | +} | |
47 | + | |
48 | +export interface ProfileData { | |
49 | + configuration: Configuration; | |
50 | + transportConfiguration: TransportConfiguration; | |
51 | + provisionConfiguration: ProvisionConfiguration; | |
52 | + alarms?: any; | |
53 | + thingsModel?: ModelOfMatterParams[]; | |
54 | +} | |
55 | + | |
56 | +export interface EdgeDeviceItemType { | |
57 | + creator: string; | |
58 | + createTime: string; | |
59 | + codeType?: string; | |
60 | + code?: string; | |
61 | + name: string; | |
62 | + transportType: string; | |
63 | + provisionType: string; | |
64 | + deviceType: string; | |
65 | + deviceCount: number; | |
66 | + tbDeviceId: string; | |
67 | + tbProfileId: string; | |
68 | + defaultQueueName: string; | |
69 | + deviceInfo?: { | |
70 | + avatar: string; | |
71 | + }; | |
72 | + image: string; | |
73 | + type: string; | |
74 | + default: boolean; | |
75 | + defaultRuleChainId: string; | |
76 | + profileId: string; | |
77 | + alias?: string; | |
78 | + brand?: string; | |
79 | + organizationId: string; | |
80 | + organizationDTO: { | |
81 | + name: string; | |
82 | + }; | |
83 | + alarmStatus: number; | |
84 | + deviceProfile: { | |
85 | + default: boolean; | |
86 | + name: string; | |
87 | + transportType: string; | |
88 | + profileData: ProfileData; | |
89 | + }; | |
90 | + customerAdditionalInfo?: { | |
91 | + isPublic?: boolean; | |
92 | + }; | |
93 | + ifShowClass?: Boolean; | |
94 | + sip?: { | |
95 | + cameraCode: string; | |
96 | + localIp: string; | |
97 | + manufacturer: string; | |
98 | + streamMode: string; | |
99 | + }; | |
100 | + customerName?: string; | |
101 | + gatewayId?: string; | |
102 | + id: { | |
103 | + entityType: string; | |
104 | + id: string; | |
105 | + }; | |
106 | + createdTime: number; | |
107 | + additionalInfo: { | |
108 | + gateway: boolean; | |
109 | + overwriteActivityTime: boolean; | |
110 | + description: string; | |
111 | + }; | |
112 | + tenantId: { | |
113 | + entityType: string; | |
114 | + id: string; | |
115 | + }; | |
116 | + customerId: { | |
117 | + entityType: string; | |
118 | + id: string; | |
119 | + }; | |
120 | + label: string; | |
121 | + deviceProfileId: { | |
122 | + entityType: string; | |
123 | + id: string; | |
124 | + }; | |
125 | + deviceData: { | |
126 | + configuration: { | |
127 | + type: string; | |
128 | + }; | |
129 | + transportConfiguration: { | |
130 | + type: string; | |
131 | + }; | |
132 | + }; | |
133 | + firmwareId: null; | |
134 | + softwareId: null; | |
135 | + externalId: null; | |
136 | + customerTitle: null; | |
137 | + customerIsPublic: boolean; | |
138 | + deviceProfileName: string; | |
139 | + active: boolean; | |
140 | + deviceState: string; | |
141 | + deviceToken?: string; | |
142 | + gatewayName?: string; | |
143 | + gatewayAlias?: string; | |
144 | + sn: string; | |
145 | +} | ... | ... |
... | ... | @@ -3,8 +3,17 @@ import { FileUploadResponse } from '/@/api/oss/FileUploadResponse'; |
3 | 3 | |
4 | 4 | enum Api { |
5 | 5 | BaseUploadUrl = '/oss/upload', |
6 | + BaseDeleteUrl = '/oss', | |
6 | 7 | } |
7 | 8 | |
8 | 9 | export const upload = (file) => { |
9 | 10 | return defHttp.post<FileUploadResponse>({ url: Api.BaseUploadUrl, params: file }); |
10 | 11 | }; |
12 | + | |
13 | +export const deleteFilePath = (deleteFilePath?: string) => { | |
14 | + if (!deleteFilePath) return; | |
15 | + const deleteParams = `?deleteFilePath=${deleteFilePath}`; | |
16 | + return defHttp.delete({ | |
17 | + url: `${Api.BaseDeleteUrl}${deleteParams}`, | |
18 | + }); | |
19 | +}; | ... | ... |
... | ... | @@ -125,11 +125,16 @@ export const getAttribute = (orgId) => { |
125 | 125 | export const byOrganizationIdGetMasterDevice = (params: { |
126 | 126 | organizationId: string; |
127 | 127 | deviceProfileId?: string; |
128 | + isSceneLinkage?: Boolean; | |
128 | 129 | }) => { |
129 | - const { organizationId, deviceProfileId } = params; | |
130 | + const { organizationId, deviceProfileId, isSceneLinkage } = params; | |
130 | 131 | return defHttp.get<DeviceModel[]>({ |
131 | 132 | url: `${ScreenManagerApi.MASTER_GET_DEVICE}`, |
132 | - params: { deviceProfileId, organizationId }, | |
133 | + params: { | |
134 | + deviceProfileId, | |
135 | + organizationId, | |
136 | + isSceneLinkage: isSceneLinkage ? isSceneLinkage : undefined, | |
137 | + }, | |
133 | 138 | }); |
134 | 139 | }; |
135 | 140 | //TODO-fengtao | ... | ... |
src/assets/icons/cpu.svg
0 → 100644
1 | +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.933" viewBox="0 0 16 15.933"><defs><style>.a{fill:#343c4c;}</style></defs><path class="a" d="M345.333,337.067h-2.667a1.337,1.337,0,0,0-1.333,1.333v2.667a1.337,1.337,0,0,0,1.333,1.333h2.667a1.337,1.337,0,0,0,1.333-1.333V338.4A1.337,1.337,0,0,0,345.333,337.067Zm0,4h-2.667V338.4h2.667Z" transform="translate(-335.999 -331.8)"/><path class="a" d="M15.333,8.533a.667.667,0,1,0,0-1.333H14V5.867h1.333A.63.63,0,0,0,16,5.2a.63.63,0,0,0-.667-.667H14A2.7,2.7,0,0,0,11.333,2V.667A.667.667,0,1,0,10,.667V2H8.667V.667a.667.667,0,0,0-1.333,0V2H6V.667A.63.63,0,0,0,5.333,0a.668.668,0,0,0-.667.667V2A2.7,2.7,0,0,0,2,4.533H.667A.63.63,0,0,0,0,5.2a.63.63,0,0,0,.667.667H2V7.2H.667a.667.667,0,0,0,0,1.333H2V9.867H.667a.667.667,0,1,0,0,1.333H2v.133A2.675,2.675,0,0,0,4.667,14v1.267a.63.63,0,0,0,.667.667A.63.63,0,0,0,6,15.267V14H7.333v1.267a.667.667,0,1,0,1.333,0V14H10v1.267a.667.667,0,1,0,1.333,0V14A2.675,2.675,0,0,0,14,11.333V11.2h1.333a.667.667,0,0,0,0-1.333H14V8.533h1.333Zm-2.667,2.8a1.337,1.337,0,0,1-1.333,1.333H4.667a1.337,1.337,0,0,1-1.333-1.333V4.667A1.337,1.337,0,0,1,4.667,3.333h6.667a1.337,1.337,0,0,1,1.333,1.333v6.667Z"/></svg> | |
\ No newline at end of file | ... | ... |
src/assets/icons/disk.svg
0 → 100644
1 | +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15.873" viewBox="0 0 16 15.873"><defs><style>.a{fill:#343c4c;}</style></defs><g transform="translate(-75.041 -99.03)"><path class="a" d="M90.95,111.459l-1.7-11.191,0-.014a1.344,1.344,0,0,0-1.276-1.225H78.36a1.356,1.356,0,0,0-1.236,1.2l0,.011-2.049,11.425.011,0a2.32,2.32,0,0,0-.042.446v.384a2.4,2.4,0,0,0,2.4,2.4h11.2a2.4,2.4,0,0,0,2.4-2.4v-.384a2.4,2.4,0,0,0-.09-.658ZM78.292,100.645c.1-.389.256-.424.344-.434h9.108c.223,0,.3.335.317.416l1.38,9.1a2.292,2.292,0,0,0-1.065-.263H77.706a2.29,2.29,0,0,0-1.046.252ZM89.936,112.4a1.378,1.378,0,0,1-1.415,1.339H77.56a1.378,1.378,0,0,1-1.415-1.339v-.356a1.378,1.378,0,0,1,1.415-1.339H88.522a1.412,1.412,0,0,1,1.34.907l.066.418h.008v.369h0Z"/><path class="a" d="M160.413,229.5H158.5a.48.48,0,1,0,0,.96h1.916a.48.48,0,0,0,0-.96Zm1.964-3.83h5.032a1.864,1.864,0,0,0,1.391-.587,2.344,2.344,0,0,0,.522-1.826,2.536,2.536,0,0,0-1.747-1.987,2.511,2.511,0,0,0-2.416-2.261,2.382,2.382,0,0,0-1.789.835,1.835,1.835,0,0,0-1.685,1.245,2.49,2.49,0,0,0-1.953,2.063,1.87,1.87,0,0,0,.415,1.428A3.433,3.433,0,0,0,162.378,225.673Zm-1.514-2.45c.094-.792.66-1.029,1.454-1.287l.267-.089.054-.261a.852.852,0,0,1,.794-.719.616.616,0,0,1,.121.011l.291.048.167-.24a1.354,1.354,0,0,1,1.12-.617,1.509,1.509,0,0,1,1.414,1.559l0,.293.3.132c.7.31,1.293.614,1.352,1.244a1.34,1.34,0,0,1-.261,1.045.826.826,0,0,1-.652.267h-4.739a2.362,2.362,0,0,1-1.478-.708A.86.86,0,0,1,160.864,223.223Z" transform="translate(-81.382 -117.677)"/></g></svg> | |
\ No newline at end of file | ... | ... |
src/assets/icons/edgefornt.svg
0 → 100644
1 | +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1722937185750" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5222" id="mx_n_1722937185750" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M0 0v1024a1024 1024 0 0 1 1024-1024H0z" p-id="5223" fill="#1890ff"></path></svg> | ... | ... |
src/assets/icons/memory.svg
0 → 100644
1 | +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.a{fill:#fff;opacity:0;}.b{fill:#343c4c;}</style></defs><g transform="translate(-562 -613)"><rect class="a" width="16" height="16" transform="translate(562 613)"/><g transform="translate(563.294 613)"><path class="b" d="M148.64,80.44H138.412a1.594,1.594,0,0,1-1.592-1.592V66.032a1.594,1.594,0,0,1,1.592-1.592h6.36a1.567,1.567,0,0,1,1.132.487l3.9,4.115a1.554,1.554,0,0,1,.428,1.073v8.733a1.594,1.594,0,0,1-1.592,1.592ZM138.412,65.714a.319.319,0,0,0-.318.318V78.848a.319.319,0,0,0,.318.318H148.64a.319.319,0,0,0,.318-.318V70.115a.285.285,0,0,0-.079-.2l-3.9-4.115a.287.287,0,0,0-.208-.089Z" transform="translate(-136.82 -64.44)"/><path class="b" d="M310.723,460.845h-6.046a.7.7,0,0,1-.7-.7v-5.04a.7.7,0,0,1,.7-.7h6.046a.7.7,0,0,1,.7.7v5.04a.7.7,0,0,1-.7.7Zm-5.787-.955h5.53v-4.524h-5.53Z" transform="translate(-300.992 -447.439)"/><path class="b" d="M426.518,592.369a.478.478,0,0,1-.478-.478v-2.774a.478.478,0,0,1,.955,0v2.774A.477.477,0,0,1,426.518,592.369Zm2.378,0a.478.478,0,0,1-.478-.478v-2.774a.478.478,0,0,1,.955,0v2.774A.478.478,0,0,1,428.9,592.369Z" transform="translate(-420.87 -579.27)"/></g></g></svg> | |
\ No newline at end of file | ... | ... |
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
... | ... | @@ -71,10 +71,10 @@ |
71 | 71 | if (!value) { |
72 | 72 | return true; |
73 | 73 | } |
74 | - let { name } = props || {}; | |
74 | + let { title } = props || {}; | |
75 | 75 | value = value.toLowerCase(); |
76 | - name = name.toLowerCase(); | |
77 | - return name.includes(value); | |
76 | + title = title.toLowerCase(); | |
77 | + return title.includes(value); | |
78 | 78 | } |
79 | 79 | |
80 | 80 | async function fetch() { | ... | ... |
... | ... | @@ -21,7 +21,7 @@ |
21 | 21 | } |
22 | 22 | |
23 | 23 | const { prefixCls } = useDesign('api-upload'); |
24 | - const emit = defineEmits(['update:fileList', 'preview', 'download']); | |
24 | + const emit = defineEmits(['update:fileList', 'preview', 'download', 'delete']); | |
25 | 25 | |
26 | 26 | const { createMessage } = useMessage(); |
27 | 27 | |
... | ... | @@ -71,9 +71,12 @@ |
71 | 71 | return false; |
72 | 72 | } |
73 | 73 | } |
74 | - | |
75 | 74 | if (file.size > props.maxSize) { |
76 | - createMessage.warning(`文件大小超过${Math.floor(props.maxSize / 1024 / 1024)}mb`); | |
75 | + createMessage.warning( | |
76 | + `文件大小超过${Math.floor( | |
77 | + props.maxSize > 1024 * 1024 ? props.maxSize / 1024 / 1024 : props.maxSize / 1024 | |
78 | + )}${props.maxSize > 1024 * 1024 * 1 ? 'mb' : 'kb'}` | |
79 | + ); | |
77 | 80 | return false; |
78 | 81 | } |
79 | 82 | handleUpload(file); |
... | ... | @@ -129,6 +132,7 @@ |
129 | 132 | const index = _fileList.findIndex((item) => item.uid === file.uid); |
130 | 133 | ~index && _fileList.splice(index, 1); |
131 | 134 | emit('update:fileList', _fileList); |
135 | + emit('delete', file.url); | |
132 | 136 | }; |
133 | 137 | |
134 | 138 | const handlePreview = (file: FileItem) => { | ... | ... |
... | ... | @@ -136,7 +136,8 @@ export interface FormSchema { |
136 | 136 | helpMessage?: |
137 | 137 | | string |
138 | 138 | | string[] |
139 | - | ((renderCallbackParams: RenderCallbackParams) => string | string[]); | |
139 | + | ((renderCallbackParams: RenderCallbackParams) => string | string[]) | |
140 | + | Boolean; | |
140 | 141 | // BaseHelp component props |
141 | 142 | helpComponentProps?: Partial<HelpComponentProps>; |
142 | 143 | // Label width, if it is passed, the labelCol and WrapperCol configured by itemProps will be invalid | ... | ... |
... | ... | @@ -59,7 +59,7 @@ |
59 | 59 | </span> |
60 | 60 | </Menu.Item> |
61 | 61 | <Menu.Item :disabled="item.disabled" v-if="item.popconfirm"> |
62 | - <Popconfirm v-bind="item.popconfirm"> | |
62 | + <Popconfirm :disabled="item.disabled" v-bind="item.popconfirm"> | |
63 | 63 | <template v-if="item.popconfirm.icon" #icon> |
64 | 64 | <Icon :icon="item.popconfirm.icon" /> |
65 | 65 | </template> | ... | ... |
... | ... | @@ -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; | ... | ... |
... | ... | @@ -2,10 +2,12 @@ |
2 | 2 | <Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`"> |
3 | 3 | <span :class="[prefixCls, `${prefixCls}--${theme}`]" class="flex"> |
4 | 4 | <img :class="`${prefixCls}__header`" :src="getUserInfo.avatar" /> |
5 | - <span :class="`${prefixCls}__info hidden md:block`"> | |
6 | - <span :class="`${prefixCls}__name `" class="truncate"> | |
7 | - {{ getUserInfo.realName }} | |
8 | - </span> | |
5 | + <span :class="`${prefixCls}__info hidden md:block truncate`"> | |
6 | + <Tooltip :trigger="['click']" :title="getUserInfo.realName"> | |
7 | + <span :class="`${prefixCls}__name `" class="truncate"> | |
8 | + {{ getUserInfo.realName?.slice(0, 10) }} | |
9 | + </span> | |
10 | + </Tooltip> | |
9 | 11 | </span> |
10 | 12 | </span> |
11 | 13 | |
... | ... | @@ -53,7 +55,7 @@ |
53 | 55 | </template> |
54 | 56 | <script lang="ts"> |
55 | 57 | // components |
56 | - import { Dropdown, Menu } from 'ant-design-vue'; | |
58 | + import { Dropdown, Menu, Tooltip } from 'ant-design-vue'; | |
57 | 59 | import { defineComponent, computed, ref, reactive } from 'vue'; |
58 | 60 | import { useUserStore } from '/@/store/modules/user'; |
59 | 61 | import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; |
... | ... | @@ -83,6 +85,7 @@ |
83 | 85 | LockAction: createAsyncComponent(() => import('../lock/LockModal.vue')), |
84 | 86 | PersonalChild: createAsyncComponent(() => import('../personal/index.vue')), |
85 | 87 | AboutSoftwareModal, |
88 | + Tooltip, | |
86 | 89 | }, |
87 | 90 | props: { |
88 | 91 | theme: propTypes.oneOf(['dark', 'light']), | ... | ... |
... | ... | @@ -28,6 +28,7 @@ import { RouteRecordRaw } from 'vue-router'; |
28 | 28 | import { PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic'; |
29 | 29 | import { createLocalStorage } from '/@/utils/cache/index'; |
30 | 30 | import { getEntitiesId } from '/@/api/dashboard/index'; |
31 | +import { useRole } from '/@/hooks/business/useRole'; | |
31 | 32 | |
32 | 33 | interface PlatInfoType { |
33 | 34 | id: string; |
... | ... | @@ -283,7 +284,10 @@ export const useUserStore = defineStore({ |
283 | 284 | title: t('sys.app.logoutTip'), |
284 | 285 | content: t('sys.app.logoutMessage'), |
285 | 286 | onOk: async () => { |
286 | - await logoutApi(null, 'modal'); //新增退出登录接口 | |
287 | + const { isPlatformAdmin } = useRole(); | |
288 | + if (!isPlatformAdmin.value) { | |
289 | + await logoutApi(null, 'modal'); //新增退出登录接口 | |
290 | + } | |
287 | 291 | await this.logout(true); |
288 | 292 | }, |
289 | 293 | }); | ... | ... |
... | ... | @@ -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 | +}; | ... | ... |
... | ... | @@ -7,10 +7,10 @@ export const createOrganizationSearch = () => { |
7 | 7 | if (!value) { |
8 | 8 | return true; |
9 | 9 | } |
10 | - let { name } = props || {}; | |
10 | + let { title } = props || {}; | |
11 | 11 | value = value?.toLowerCase(); |
12 | - name = name?.toLowerCase(); | |
13 | - return name.includes(value); | |
12 | + title = title?.toLowerCase(); | |
13 | + return title.includes(value); | |
14 | 14 | }, |
15 | 15 | }; |
16 | 16 | }; | ... | ... |
... | ... | @@ -247,11 +247,11 @@ export const alarmSchemasForm: FormSchema[] = [ |
247 | 247 | disabled: true, |
248 | 248 | }, |
249 | 249 | }, |
250 | - { | |
251 | - field: 'details', | |
252 | - label: '详情', | |
253 | - component: 'InputTextArea', | |
254 | - }, | |
250 | + // { | |
251 | + // field: 'details', | |
252 | + // label: '详情', | |
253 | + // component: 'InputTextArea', | |
254 | + // }, | |
255 | 255 | ]; |
256 | 256 | |
257 | 257 | export function getAlarmStatus({ | ... | ... |
... | ... | @@ -51,6 +51,7 @@ |
51 | 51 | import { useDrawer } from '/@/components/Drawer'; |
52 | 52 | import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; |
53 | 53 | import { buildUUID } from '/@/utils/uuid'; |
54 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
54 | 55 | |
55 | 56 | export default defineComponent({ |
56 | 57 | name: 'ContactDrawer', |
... | ... | @@ -161,6 +162,11 @@ |
161 | 162 | } else { |
162 | 163 | delete values.id; |
163 | 164 | } |
165 | + | |
166 | + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) { | |
167 | + await deleteFilePath(values?.deleteUrl); | |
168 | + Reflect.deleteProperty(values, 'deleteUrl'); | |
169 | + } | |
164 | 170 | let saveMessage = '添加成功'; |
165 | 171 | let updateMessage = '修改成功'; |
166 | 172 | ... | ... |
... | ... | @@ -100,7 +100,6 @@ |
100 | 100 | AccessMode, |
101 | 101 | PageMode, |
102 | 102 | CameraPermission, |
103 | - VideoPlatformEnum, | |
104 | 103 | accessModeConfig, |
105 | 104 | getPlayUrl, |
106 | 105 | } from './config.data'; |
... | ... | @@ -199,14 +198,12 @@ |
199 | 198 | const handleViewVideo = (record: CameraRecord) => { |
200 | 199 | const { videoPlatformDTO, params } = record; |
201 | 200 | const { type } = videoPlatformDTO || {}; |
202 | - | |
203 | 201 | openModal(true, { |
204 | 202 | record: { |
205 | 203 | id: record.id, |
206 | - canControl: | |
207 | - [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode) && | |
208 | - type !== VideoPlatformEnum.FLUORITE, | |
204 | + canControl: [AccessMode.Streaming, AccessMode.GBT28181].includes(record.accessMode), | |
209 | 205 | isGBT: record.accessMode === AccessMode.GBT28181, |
206 | + videoPlatformType: type, | |
210 | 207 | channelId: params?.channelNo, |
211 | 208 | tbDeviceId: params?.deviceId, |
212 | 209 | getPlayUrl: async () => { | ... | ... |
... | ... | @@ -92,10 +92,11 @@ |
92 | 92 | const { getResult } = useFingerprint(); |
93 | 93 | const beforeVideoPlay = async (record: CameraRecordItem) => { |
94 | 94 | if (isNullOrUnDef(record.accessMode)) return; |
95 | - const { url, type } = await getPlayUrl(record); | |
95 | + const { url, type, withToken } = (await getPlayUrl(record)) || {}; | |
96 | 96 | record.playSourceUrl = url; |
97 | 97 | record.streamType = type; |
98 | 98 | record.isTransform = true; |
99 | + record.withToken = withToken; | |
99 | 100 | }; |
100 | 101 | |
101 | 102 | const gridLayout = ref({ gutter: 1, column: 2 }); | ... | ... |
... | ... | @@ -192,7 +192,7 @@ export const formSchema: QFormSchema[] = [ |
192 | 192 | component: 'ApiUpload', |
193 | 193 | changeEvent: 'update:fileList', |
194 | 194 | valueField: 'fileList', |
195 | - componentProps: () => { | |
195 | + componentProps: ({ formModel }) => { | |
196 | 196 | return { |
197 | 197 | listType: 'picture-card', |
198 | 198 | maxFileLimit: 1, |
... | ... | @@ -214,10 +214,19 @@ export const formSchema: QFormSchema[] = [ |
214 | 214 | onPreview: (fileList: FileItem) => { |
215 | 215 | createImgPreview({ imageList: [fileList.url!] }); |
216 | 216 | }, |
217 | + onDelete(url: string) { | |
218 | + formModel.deleteUrl = url!; | |
219 | + }, | |
217 | 220 | }; |
218 | 221 | }, |
219 | 222 | }, |
220 | 223 | { |
224 | + field: 'deleteUrl', | |
225 | + label: '', | |
226 | + component: 'Input', | |
227 | + show: false, | |
228 | + }, | |
229 | + { | |
221 | 230 | field: 'name', |
222 | 231 | label: '视频名字', |
223 | 232 | required: true, |
... | ... | @@ -623,9 +632,7 @@ export const formGBTSchema: QFormSchema[] = [ |
623 | 632 | }, |
624 | 633 | ]; |
625 | 634 | |
626 | -export async function getPlayUrl( | |
627 | - params: CameraRecord | |
628 | -): Promise<{ url: string; type: PlayerStreamType }> { | |
635 | +export async function getPlayUrl(params: CameraRecord) { | |
629 | 636 | const { accessMode } = params; |
630 | 637 | if (accessMode === AccessMode.ManuallyEnter) { |
631 | 638 | const { videoUrl } = params; |
... | ... | @@ -635,7 +642,11 @@ export async function getPlayUrl( |
635 | 642 | if (isRTSPPlay) { |
636 | 643 | const { getResult } = useFingerprint(); |
637 | 644 | const fingerprint = await getResult(); |
638 | - return { url: getFlvPlayUrl(videoUrl, fingerprint.visitorId), type: 'flv' }; | |
645 | + return { | |
646 | + url: getFlvPlayUrl(videoUrl, fingerprint.visitorId), | |
647 | + type: 'flv', | |
648 | + withToken: true, | |
649 | + }; | |
639 | 650 | } else { |
640 | 651 | return { url: videoUrl, type: 'auto' }; |
641 | 652 | } | ... | ... |
... | ... | @@ -52,6 +52,7 @@ |
52 | 52 | import { createPickerSearch } from '/@/utils/pickerSearch'; |
53 | 53 | import type { queryPageParams } from '/@/api/configuration/center/model/configurationCenterModal'; |
54 | 54 | import { getDeviceProfile } from '/@/api/alarm/position'; |
55 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
55 | 56 | |
56 | 57 | export default defineComponent({ |
57 | 58 | name: 'ConfigurationDrawer', |
... | ... | @@ -263,6 +264,11 @@ |
263 | 264 | values.thumbnail = file.url || null; |
264 | 265 | } |
265 | 266 | setDrawerProps({ confirmLoading: true }); |
267 | + | |
268 | + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) { | |
269 | + await deleteFilePath(values?.deleteUrl); | |
270 | + Reflect.deleteProperty(values, 'deleteUrl'); | |
271 | + } | |
266 | 272 | let saveMessage = '添加成功'; |
267 | 273 | let updateMessage = '修改成功'; |
268 | 274 | values.defaultContent = getDefaultContent(values.platform); | ... | ... |
... | ... | @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form'; |
6 | 6 | import { OrgTreeSelect } from '../../common/OrgTreeSelect'; |
7 | 7 | import { getDeviceProfile } from '/@/api/alarm/position'; |
8 | 8 | import { buildUUID } from '/@/utils/uuid'; |
9 | +import { createPickerSearch } from '/@/utils/pickerSearch'; | |
9 | 10 | |
10 | 11 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); |
11 | 12 | export enum Platform { |
... | ... | @@ -90,7 +91,7 @@ export const formSchema: FormSchema[] = [ |
90 | 91 | component: 'ApiUpload', |
91 | 92 | changeEvent: 'update:fileList', |
92 | 93 | valueField: 'fileList', |
93 | - componentProps: () => { | |
94 | + componentProps: ({ formModel }) => { | |
94 | 95 | return { |
95 | 96 | listType: 'picture-card', |
96 | 97 | maxFileLimit: 1, |
... | ... | @@ -112,10 +113,19 @@ export const formSchema: FormSchema[] = [ |
112 | 113 | onPreview: (fileList: FileItem) => { |
113 | 114 | createImgPreview({ imageList: [fileList.url!] }); |
114 | 115 | }, |
116 | + onDelete(url: string) { | |
117 | + formModel.deleteUrl = url!; | |
118 | + }, | |
115 | 119 | }; |
116 | 120 | }, |
117 | 121 | }, |
118 | 122 | { |
123 | + field: 'deleteUrl', | |
124 | + label: '', | |
125 | + component: 'Input', | |
126 | + show: false, | |
127 | + }, | |
128 | + { | |
119 | 129 | field: 'name', |
120 | 130 | label: '组态名称', |
121 | 131 | required: true, |
... | ... | @@ -210,14 +220,7 @@ export const formSchema: FormSchema[] = [ |
210 | 220 | labelField: 'name', |
211 | 221 | valueField: 'id', |
212 | 222 | placeholder: '请选择产品', |
213 | - getPopupContainer: (triggerNode) => triggerNode.parentNode, | |
214 | - filterOption: (inputValue: string, option: Record<'label' | 'value', string>) => { | |
215 | - let { label, value } = option; | |
216 | - label = label.toLowerCase(); | |
217 | - value = value.toLowerCase(); | |
218 | - inputValue = inputValue.toLowerCase(); | |
219 | - return label.includes(inputValue) || value.includes(inputValue); | |
220 | - }, | |
223 | + ...createPickerSearch(), | |
221 | 224 | }, |
222 | 225 | ifShow: ({ values }) => values['enableTemplate'] === 0, |
223 | 226 | }, | ... | ... |
... | ... | @@ -21,6 +21,7 @@ |
21 | 21 | import { buildUUID } from '/@/utils/uuid'; |
22 | 22 | import { getDeviceProfile } from '/@/api/alarm/position'; |
23 | 23 | import { PC_DEFAULT_CONTENT, PHONE_DEFAULT_CONTENT, Platform } from '../center/center.data'; |
24 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
24 | 25 | |
25 | 26 | export default defineComponent({ |
26 | 27 | name: 'ConfigurationDrawer', |
... | ... | @@ -91,6 +92,10 @@ |
91 | 92 | const file = (values.thumbnail || []).at(0) || {}; |
92 | 93 | values.thumbnail = file.url || null; |
93 | 94 | } |
95 | + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) { | |
96 | + await deleteFilePath(values?.deleteUrl); | |
97 | + Reflect.deleteProperty(values, 'deleteUrl'); | |
98 | + } | |
94 | 99 | setDrawerProps({ confirmLoading: true }); |
95 | 100 | let saveMessage = '添加成功'; |
96 | 101 | let updateMessage = '修改成功'; | ... | ... |
... | ... | @@ -6,6 +6,7 @@ import { useComponentRegister } from '/@/components/Form'; |
6 | 6 | import { OrgTreeSelect } from '../../common/OrgTreeSelect'; |
7 | 7 | import { getDeviceProfile } from '/@/api/alarm/position'; |
8 | 8 | import { Platform } from '../center/center.data'; |
9 | +import { createPickerSearch } from '/@/utils/pickerSearch'; | |
9 | 10 | |
10 | 11 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); |
11 | 12 | |
... | ... | @@ -79,7 +80,7 @@ export const formSchema: FormSchema[] = [ |
79 | 80 | component: 'ApiUpload', |
80 | 81 | changeEvent: 'update:fileList', |
81 | 82 | valueField: 'fileList', |
82 | - componentProps: () => { | |
83 | + componentProps: ({ formModel }) => { | |
83 | 84 | return { |
84 | 85 | listType: 'picture-card', |
85 | 86 | maxFileLimit: 1, |
... | ... | @@ -101,9 +102,18 @@ export const formSchema: FormSchema[] = [ |
101 | 102 | onPreview: (fileList: FileItem) => { |
102 | 103 | createImgPreview({ imageList: [fileList.url!] }); |
103 | 104 | }, |
105 | + onDelete(url: string) { | |
106 | + formModel.deleteUrl = url!; | |
107 | + }, | |
104 | 108 | }; |
105 | 109 | }, |
106 | 110 | }, |
111 | + { | |
112 | + field: 'deleteUrl', | |
113 | + label: '', | |
114 | + component: 'Input', | |
115 | + show: false, | |
116 | + }, | |
107 | 117 | |
108 | 118 | { |
109 | 119 | field: 'name', |
... | ... | @@ -131,6 +141,7 @@ export const formSchema: FormSchema[] = [ |
131 | 141 | mode: 'multiple', |
132 | 142 | labelField: 'name', |
133 | 143 | valueField: 'id', |
144 | + ...createPickerSearch(), | |
134 | 145 | }, |
135 | 146 | }, |
136 | 147 | { | ... | ... |
... | ... | @@ -19,6 +19,7 @@ |
19 | 19 | import { saveOrUpdateBigScreenCenter } from '/@/api/bigscreen/center/bigscreenCenter'; |
20 | 20 | import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; |
21 | 21 | import { buildUUID } from '/@/utils/uuid'; |
22 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
22 | 23 | |
23 | 24 | export default defineComponent({ |
24 | 25 | name: 'BigScreenDrawer', |
... | ... | @@ -67,6 +68,10 @@ |
67 | 68 | const file = (values.thumbnail || []).at(0) || {}; |
68 | 69 | values.thumbnail = file.url || null; |
69 | 70 | } |
71 | + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) { | |
72 | + await deleteFilePath(values?.deleteUrl); | |
73 | + Reflect.deleteProperty(values, 'deleteUrl'); | |
74 | + } | |
70 | 75 | setDrawerProps({ confirmLoading: true }); |
71 | 76 | let saveMessage = '添加成功'; |
72 | 77 | let updateMessage = '修改成功'; | ... | ... |
... | ... | @@ -56,7 +56,7 @@ export const formSchema: FormSchema[] = [ |
56 | 56 | component: 'ApiUpload', |
57 | 57 | changeEvent: 'update:fileList', |
58 | 58 | valueField: 'fileList', |
59 | - componentProps: () => { | |
59 | + componentProps: ({ formModel }) => { | |
60 | 60 | return { |
61 | 61 | listType: 'picture-card', |
62 | 62 | maxFileLimit: 1, |
... | ... | @@ -80,9 +80,18 @@ export const formSchema: FormSchema[] = [ |
80 | 80 | onPreview: (fileList: FileItem) => { |
81 | 81 | createImgPreview({ imageList: [fileList.url!] }); |
82 | 82 | }, |
83 | + onDelete(url: string) { | |
84 | + formModel.deleteUrl = url!; | |
85 | + }, | |
83 | 86 | }; |
84 | 87 | }, |
85 | 88 | }, |
89 | + { | |
90 | + field: 'deleteUrl', | |
91 | + label: '', | |
92 | + component: 'Input', | |
93 | + show: false, | |
94 | + }, | |
86 | 95 | |
87 | 96 | { |
88 | 97 | field: 'name', | ... | ... |
... | ... | @@ -48,7 +48,10 @@ |
48 | 48 | rowSelection: { |
49 | 49 | type: 'checkbox', |
50 | 50 | getCheckboxProps: (record: any) => { |
51 | - return { disabled: !!record.status }; | |
51 | + return { | |
52 | + disabled: | |
53 | + getAuthCache(USER_INFO_KEY).tenantId === record.tenantId ? !!record.status : true, | |
54 | + }; | |
52 | 55 | }, |
53 | 56 | }, |
54 | 57 | }); |
... | ... | @@ -86,12 +89,14 @@ |
86 | 89 | |
87 | 90 | // 详情 |
88 | 91 | const registerDetailRecord = ref<any>({}); |
92 | + const isCurrentTenant = ref<Boolean>(false); | |
89 | 93 | const handleDetail = (record?: any) => { |
90 | 94 | openDrawer(true); |
91 | 95 | registerDetailRecord.value = { |
92 | 96 | ...record, |
93 | 97 | ifShowClass: true, |
94 | 98 | }; |
99 | + isCurrentTenant.value = getAuthCache(USER_INFO_KEY).tenantId === record.tenantId; | |
95 | 100 | }; |
96 | 101 | |
97 | 102 | // 状态->编辑 |
... | ... | @@ -99,7 +104,7 @@ |
99 | 104 | switchLoading.value = true; |
100 | 105 | await deviceProfileCategory({ ...record, status: e }); |
101 | 106 | switchLoading.value = false; |
102 | - createMessage.success('操作成功'); | |
107 | + createMessage.success(`${!e ? '禁用' : '启用'}成功`); | |
103 | 108 | handleReload(); |
104 | 109 | }; |
105 | 110 | |
... | ... | @@ -145,6 +150,7 @@ |
145 | 150 | <Switch |
146 | 151 | @click="(e) => handleSwitch(e, record)" |
147 | 152 | :loading="switchLoading" |
153 | + :disabled="getAuthCache(USER_INFO_KEY).tenantId !== record.tenantId" | |
148 | 154 | v-model:checked="record.status" |
149 | 155 | checked-children="启用" |
150 | 156 | un-checked-children="禁用" |
... | ... | @@ -165,14 +171,17 @@ |
165 | 171 | label: '编辑', |
166 | 172 | auth: 'api:yt:product:category:update', |
167 | 173 | icon: 'clarity:note-edit-line', |
168 | - ifShow: authBtn(role), | |
174 | + ifShow: authBtn(role) && getAuthCache(USER_INFO_KEY).tenantId === record.tenantId, | |
169 | 175 | onClick: handleEdit.bind(null, record), |
170 | 176 | }, |
171 | 177 | { |
172 | 178 | label: '删除', |
173 | 179 | auth: 'api:yt:product:category:delete', |
174 | 180 | icon: 'ant-design:delete-outlined', |
175 | - ifShow: authBtn(role) && !record.status, | |
181 | + ifShow: | |
182 | + authBtn(role) && | |
183 | + !record.status && | |
184 | + getAuthCache(USER_INFO_KEY).tenantId === record.tenantId, | |
176 | 185 | color: 'error', |
177 | 186 | popConfirm: { |
178 | 187 | title: '是否确认删除', |
... | ... | @@ -185,7 +194,10 @@ |
185 | 194 | </BasicTable> |
186 | 195 | <classModal @register="registerModal" @handleReload="handleReload" /> |
187 | 196 | <BasicDrawer title="物模型" @register="registerDetailDrawer" width="60%" destroy-on-close> |
188 | - <PhysicalModelManagementStep :record="registerDetailRecord" /> | |
197 | + <PhysicalModelManagementStep | |
198 | + :isCurrentTenant="isCurrentTenant" | |
199 | + :record="registerDetailRecord" | |
200 | + /> | |
189 | 201 | </BasicDrawer> |
190 | 202 | </div> |
191 | 203 | </template> | ... | ... |
... | ... | @@ -49,7 +49,7 @@ export const step1Schemas: FormSchema[] = [ |
49 | 49 | component: 'ApiUpload', |
50 | 50 | changeEvent: 'update:fileList', |
51 | 51 | valueField: 'fileList', |
52 | - componentProps: () => { | |
52 | + componentProps: ({ formModel }) => { | |
53 | 53 | return { |
54 | 54 | listType: 'picture-card', |
55 | 55 | maxFileLimit: 1, |
... | ... | @@ -71,10 +71,19 @@ export const step1Schemas: FormSchema[] = [ |
71 | 71 | onPreview: (fileList: FileItem) => { |
72 | 72 | createImgPreview({ imageList: [fileList.url!] }); |
73 | 73 | }, |
74 | + onDelete(url: string) { | |
75 | + formModel.deleteUrl = url!; | |
76 | + }, | |
74 | 77 | }; |
75 | 78 | }, |
76 | 79 | }, |
77 | 80 | { |
81 | + field: 'deleteUrl', | |
82 | + label: '', | |
83 | + component: 'Input', | |
84 | + show: false, | |
85 | + }, | |
86 | + { | |
78 | 87 | field: 'alias', |
79 | 88 | label: '别名 ', |
80 | 89 | component: 'Input', |
... | ... | @@ -193,8 +202,29 @@ export const step1Schemas: FormSchema[] = [ |
193 | 202 | }, |
194 | 203 | }, |
195 | 204 | { |
205 | + field: 'addressType', | |
206 | + label: '', | |
207 | + component: 'Input', | |
208 | + show: false, | |
209 | + defaultValue: 'HEX', | |
210 | + }, | |
211 | + { | |
212 | + field: 'deviceState', | |
213 | + label: '', | |
214 | + component: 'Input', | |
215 | + show: false, | |
216 | + }, | |
217 | + { | |
196 | 218 | field: 'addressCode', |
197 | 219 | label: '地址码', |
220 | + dynamicDisabled({ values }) { | |
221 | + return ( | |
222 | + values.isUpdate && | |
223 | + values.deviceType === DeviceTypeEnum.SENSOR && | |
224 | + values.deviceState === 'ONLINE' && | |
225 | + values.transportType === TransportTypeEnum.TCP | |
226 | + ); | |
227 | + }, | |
198 | 228 | dynamicRules({ values }) { |
199 | 229 | return [ |
200 | 230 | { |
... | ... | @@ -202,18 +232,31 @@ export const step1Schemas: FormSchema[] = [ |
202 | 232 | values?.transportType === TransportTypeEnum.TCP && |
203 | 233 | values?.tcpDeviceProtocol === TCPProtocolTypeEnum.MODBUS_RTU, |
204 | 234 | message: '地址码范围为00~FF', |
205 | - pattern: /^[0-9A-Fa-f]{1,2}$/, | |
235 | + pattern: values?.addressType === 'HEX' ? /^[0-9A-Fa-f]{2}$/ : /^[0-9A-Fa-f]{1,2}$/, | |
206 | 236 | }, |
207 | 237 | ]; |
208 | 238 | }, |
209 | - helpMessage: ['地址码范围为00~FF'], | |
239 | + helpMessage({ values }) { | |
240 | + return [ | |
241 | + '地址码范围为00~FF', | |
242 | + values.transportType === TransportTypeEnum.TCP && | |
243 | + values.deviceType === DeviceTypeEnum.SENSOR | |
244 | + ? 'tcp网关子设备在线时,不能修改设备标识或地址码' | |
245 | + : '', | |
246 | + ]; | |
247 | + }, | |
210 | 248 | component: 'HexInput', |
211 | 249 | changeEvent: 'update:value', |
212 | 250 | valueField: 'value', |
213 | - componentProps: { | |
214 | - type: InputTypeEnum.HEX, | |
215 | - maxValue: parseInt('FF', 16), | |
216 | - placeholder: '请输入寄存器地址', | |
251 | + componentProps: ({ formModel }) => { | |
252 | + return { | |
253 | + type: InputTypeEnum.HEX, | |
254 | + maxValue: parseInt('FF', 16), | |
255 | + placeholder: '请输入寄存器地址', | |
256 | + onHexChange: (e) => { | |
257 | + formModel.addressType = e; | |
258 | + }, | |
259 | + }; | |
217 | 260 | }, |
218 | 261 | ifShow: ({ values }) => { |
219 | 262 | return ( |
... | ... | @@ -235,6 +278,22 @@ export const step1Schemas: FormSchema[] = [ |
235 | 278 | }, |
236 | 279 | ]; |
237 | 280 | }, |
281 | + dynamicDisabled({ values }) { | |
282 | + return ( | |
283 | + values.isUpdate && | |
284 | + values.deviceType === DeviceTypeEnum.SENSOR && | |
285 | + values.deviceState === 'ONLINE' && | |
286 | + values.transportType === TransportTypeEnum.TCP | |
287 | + ); | |
288 | + }, | |
289 | + helpMessage({ values }) { | |
290 | + return ( | |
291 | + values.transportType === TransportTypeEnum.TCP && | |
292 | + values.deviceType === DeviceTypeEnum.SENSOR | |
293 | + ? ['tcp网关子设备在线时,不能修改设备标识或地址码'] | |
294 | + : false | |
295 | + ) as any; | |
296 | + }, | |
238 | 297 | component: 'Input', |
239 | 298 | componentProps: () => { |
240 | 299 | return { |
... | ... | @@ -276,7 +335,7 @@ export const step1Schemas: FormSchema[] = [ |
276 | 335 | component: 'ApiSelect', |
277 | 336 | ifShow: ({ values }) => values.deviceType === 'SENSOR', |
278 | 337 | componentProps: ({ formModel, formActionType }) => { |
279 | - const { transportType } = formModel; | |
338 | + const { transportType, deviceType, gatewayId } = formModel; | |
280 | 339 | const { setFieldsValue } = formActionType; |
281 | 340 | if (!transportType) return {}; |
282 | 341 | return { |
... | ... | @@ -291,6 +350,7 @@ export const step1Schemas: FormSchema[] = [ |
291 | 350 | showSearch: true, |
292 | 351 | params: { |
293 | 352 | transportType, |
353 | + gatewayId: deviceType === DeviceTypeEnum.SENSOR && gatewayId ? gatewayId : null, | |
294 | 354 | }, |
295 | 355 | placeholder: '请选择网关设备', |
296 | 356 | valueField: 'tbDeviceId', | ... | ... |
... | ... | @@ -6,6 +6,7 @@ import { deviceProfile } from '/@/api/device/deviceManager'; |
6 | 6 | import { h } from 'vue'; |
7 | 7 | import { Tag, Tooltip } from 'ant-design-vue'; |
8 | 8 | import { handeleCopy } from '../../profiles/step/topic'; |
9 | +import edgefornt from '/@/assets/icons/edgefornt.svg'; | |
9 | 10 | |
10 | 11 | export enum DeviceListAuthEnum { |
11 | 12 | /** |
... | ... | @@ -84,8 +85,9 @@ export const columns: BasicColumn[] = [ |
84 | 85 | title: '别名/设备名称', |
85 | 86 | width: 210, |
86 | 87 | slots: { customRender: 'name', title: 'deviceTitle' }, |
88 | + className: 'device-name-edge', | |
87 | 89 | customRender: ({ record }) => { |
88 | - return h('div', { style: 'display:flex;flex-direction:column' }, [ | |
90 | + return h('div', { class: 'py-3 px-3.5' }, [ | |
89 | 91 | record.alias && |
90 | 92 | h( |
91 | 93 | 'div', |
... | ... | @@ -118,6 +120,19 @@ export const columns: BasicColumn[] = [ |
118 | 120 | () => `${record.name}` |
119 | 121 | ) |
120 | 122 | ), |
123 | + record.isEdge | |
124 | + ? h('div', { class: 'absolute top-0 left-0', fill: '#1890ff' }, [ | |
125 | + h('img', { src: edgefornt, class: 'w-12.5 h-12.5' }), | |
126 | + h( | |
127 | + 'span', | |
128 | + { | |
129 | + class: | |
130 | + 'absolute top-0.5 left-0.5 text-light-50 transform -rotate-45 translate-y-0 !text-10px', | |
131 | + }, | |
132 | + '边' | |
133 | + ), | |
134 | + ]) | |
135 | + : '', | |
121 | 136 | ]); |
122 | 137 | }, |
123 | 138 | }, |
... | ... | @@ -162,7 +177,9 @@ export const columns: BasicColumn[] = [ |
162 | 177 | { |
163 | 178 | title: '最后断开时间', |
164 | 179 | dataIndex: 'lastOfflineTime', |
165 | - format: (text) => text && formatToDate(text, 'YYYY-MM-DD HH:mm:ss'), | |
180 | + format: (text) => { | |
181 | + return text ? formatToDate(text, 'YYYY-MM-DD HH:mm:ss') : ''; | |
182 | + }, | |
166 | 183 | width: 160, |
167 | 184 | }, |
168 | 185 | ]; | ... | ... |
... | ... | @@ -41,6 +41,7 @@ |
41 | 41 | import { Steps } from 'ant-design-vue'; |
42 | 42 | import { useMessage } from '/@/hooks/web/useMessage'; |
43 | 43 | import { credentialTypeEnum } from '../../config/data'; |
44 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
44 | 45 | |
45 | 46 | export default defineComponent({ |
46 | 47 | name: 'DeviceModal', |
... | ... | @@ -161,8 +162,13 @@ |
161 | 162 | avatar: stepRecord?.icon, |
162 | 163 | ...DeviceStep1Ref.value?.devicePositionState, |
163 | 164 | }, |
165 | + alarmStatus: currentDeviceData?.alarmStatus, | |
164 | 166 | }; |
165 | 167 | validateNameLength(stepRecord.name); |
168 | + if (Reflect.has(editData, 'deleteUrl')) { | |
169 | + await deleteFilePath(editData?.deleteUrl); | |
170 | + Reflect.deleteProperty(editData, 'deleteUrl'); | |
171 | + } | |
166 | 172 | await createOrEditDevice(editData); |
167 | 173 | } else { |
168 | 174 | const stepRecord = unref(stepState); | ... | ... |
... | ... | @@ -401,16 +401,16 @@ |
401 | 401 | // 父组件调用更新字段值的方法 |
402 | 402 | function parentSetFieldsValue(data) { |
403 | 403 | const { deviceInfo = {} } = data; |
404 | - positionState.longitude = deviceInfo.longitude; | |
405 | - positionState.latitude = deviceInfo.latitude; | |
406 | - positionState.address = deviceInfo.address; | |
404 | + positionState.longitude = deviceInfo?.longitude; | |
405 | + positionState.latitude = deviceInfo?.latitude; | |
406 | + positionState.address = deviceInfo?.address; | |
407 | 407 | devicePositionState.value = { ...toRaw(positionState) }; |
408 | - devicePic.value = deviceInfo.avatar; | |
408 | + devicePic.value = deviceInfo?.avatar; | |
409 | 409 | |
410 | - if (deviceInfo.avatar) { | |
410 | + if (deviceInfo?.avatar) { | |
411 | 411 | setFieldsValue({ |
412 | 412 | isUpdate: unref(isUpdate1), |
413 | - icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem], | |
413 | + icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo?.avatar } as FileItem], | |
414 | 414 | }); |
415 | 415 | } |
416 | 416 | setFieldsValue({ | ... | ... |
... | ... | @@ -119,6 +119,7 @@ |
119 | 119 | ], |
120 | 120 | }; |
121 | 121 | }); |
122 | + const cacheSearchValue = ref(''); | |
122 | 123 | |
123 | 124 | const [registerForm, { getFieldsValue }] = useForm({ |
124 | 125 | schemas: [ |
... | ... | @@ -141,6 +142,8 @@ |
141 | 142 | |
142 | 143 | pagination.current = 1; |
143 | 144 | |
145 | + cacheSearchValue.value = value; | |
146 | + | |
144 | 147 | socketInfo.filterAttrKeys = value |
145 | 148 | ? unref(socketInfo.rawDataSource) |
146 | 149 | .filter( |
... | ... | @@ -161,6 +164,7 @@ |
161 | 164 | resetFunc: async () => { |
162 | 165 | try { |
163 | 166 | socketInfo.filterAttrKeys = []; |
167 | + cacheSearchValue.value = ''; | |
164 | 168 | handleFilterChange(); |
165 | 169 | unref(mode) === EnumTableCardMode.TABLE && setTableModeData(); |
166 | 170 | } catch (error) {} |
... | ... | @@ -201,8 +205,15 @@ |
201 | 205 | const { createMessage } = useMessage(); |
202 | 206 | |
203 | 207 | const setDataSource = () => { |
208 | + const filterValueByCacheSearchValue = socketInfo.rawDataSource.filter( | |
209 | + (item) => | |
210 | + item.key?.toUpperCase().includes(cacheSearchValue.value.toUpperCase()) || | |
211 | + item.name?.toUpperCase().includes(cacheSearchValue.value.toUpperCase()) | |
212 | + ); | |
204 | 213 | socketInfo.dataSource = socketInfo.filterAttrKeys.length |
205 | 214 | ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key)) |
215 | + : filterValueByCacheSearchValue.length === 0 | |
216 | + ? [] | |
206 | 217 | : socketInfo.rawDataSource; |
207 | 218 | }; |
208 | 219 | ... | ... |
1 | 1 | import { h } from 'vue'; |
2 | 2 | import { BasicColumn, FormSchema } from '/@/components/Table'; |
3 | 3 | import { Tag } from 'ant-design-vue'; |
4 | +import { VideoPlatformEnum } from '/@/views/camera/manage/config.data'; | |
4 | 5 | export type VideoCancelModalParamsType = { |
5 | 6 | canControl?: boolean; |
7 | + videoPlatformType: VideoPlatformEnum; | |
6 | 8 | isGBT?: boolean; |
7 | 9 | tbDeviceId?: string; |
8 | 10 | channelId?: string; |
... | ... | @@ -101,3 +103,25 @@ export const searchFormSchema: FormSchema[] | any = [ |
101 | 103 | }, |
102 | 104 | }, |
103 | 105 | ]; |
106 | + | |
107 | +export enum VideoControlEnum { | |
108 | + Up = 'UP', | |
109 | + Right = 'RIGHT', | |
110 | + Left = 'LEFT', | |
111 | + Down = 'DOWN', | |
112 | + ZoomIn = 'ZOOM_IN', | |
113 | + ZoomOut = 'ZOOM_OUT', | |
114 | +} | |
115 | + | |
116 | +export enum EzvizVideoControlEnum { | |
117 | + UP, | |
118 | + DOWN, | |
119 | + LEFT, | |
120 | + RIGHT, | |
121 | + LEFT_UP, | |
122 | + LEFT_DOWN, | |
123 | + RIGHT_UP, | |
124 | + RIGHT_DOWN, | |
125 | + ZOOM_IN, | |
126 | + ZOOM_OUT, | |
127 | +} | ... | ... |
... | ... | @@ -13,20 +13,14 @@ |
13 | 13 | } from '@ant-design/icons-vue'; |
14 | 14 | import { Button, Slider } from 'ant-design-vue'; |
15 | 15 | import { controlling } from '/@/api/camera/cameraManager'; |
16 | - import { setVideoControl } from '/@/api/device/videoChannel'; | |
16 | + import { setEzvizControl, setVideoControl } from '/@/api/device/videoChannel'; | |
17 | 17 | import XGPlayer from '/@/components/Video/src/XGPlayer.vue'; |
18 | - import PresetPlayer from 'xgplayer'; | |
18 | + import { EzvizVideoControlEnum, VideoCancelModalParamsType, VideoControlEnum } from './config'; | |
19 | + import { VideoPlatformEnum } from '/@/views/camera/manage/config.data'; | |
19 | 20 | |
20 | 21 | const props = defineProps<{ |
21 | 22 | playUrl?: string; |
22 | - options?: { | |
23 | - canControl?: boolean; | |
24 | - isGBT?: boolean; | |
25 | - tbDeviceId?: string; | |
26 | - channelId?: string; | |
27 | - id?: string; | |
28 | - playerProps?: Recordable; | |
29 | - }; | |
23 | + options?: VideoCancelModalParamsType; | |
30 | 24 | }>(); |
31 | 25 | |
32 | 26 | const playerRef = ref<InstanceType<typeof XGPlayer>>(); |
... | ... | @@ -64,12 +58,24 @@ |
64 | 58 | }); |
65 | 59 | }; |
66 | 60 | |
61 | + const handleEzvizControl = (actionType: string, start: boolean) => { | |
62 | + const { id } = props.options || {}; | |
63 | + const action = EzvizVideoControlEnum[actionType as VideoControlEnum]; | |
64 | + setEzvizControl({ entityId: id!, action, controllingType: Number(!start) }); | |
65 | + }; | |
66 | + | |
67 | 67 | //长按开始 |
68 | 68 | const moveStart = (action: string) => { |
69 | 69 | if (unref(props.options?.isGBT)) { |
70 | 70 | handleGBTControl(action, unref(sliderValue)); |
71 | 71 | return; |
72 | 72 | } |
73 | + | |
74 | + if (props.options?.videoPlatformType === VideoPlatformEnum.FLUORITE) { | |
75 | + handleEzvizControl(action, true); | |
76 | + return; | |
77 | + } | |
78 | + | |
73 | 79 | handleControl(0, action); |
74 | 80 | }; |
75 | 81 | |
... | ... | @@ -79,6 +85,14 @@ |
79 | 85 | handleGBTControl('STOP', unref(sliderValue)); |
80 | 86 | return; |
81 | 87 | } |
88 | + | |
89 | + if (props.options?.videoPlatformType === VideoPlatformEnum.FLUORITE) { | |
90 | + setTimeout(() => { | |
91 | + handleEzvizControl(action, false); | |
92 | + }, 1000); | |
93 | + return; | |
94 | + } | |
95 | + | |
82 | 96 | handleControl(1, action); |
83 | 97 | }; |
84 | 98 | |
... | ... | @@ -122,15 +136,15 @@ |
122 | 136 | <div> |
123 | 137 | <Button |
124 | 138 | class="left-top in-block" |
125 | - @mousedown="moveStart('UP')" | |
126 | - @mouseup="moveStop('UP')" | |
139 | + @mousedown="moveStart(VideoControlEnum.Up)" | |
140 | + @mouseup="moveStop(VideoControlEnum.Up)" | |
127 | 141 | > |
128 | 142 | <CaretUpOutlined class="icon-rotate child-icon" /> |
129 | 143 | </Button> |
130 | 144 | <Button |
131 | 145 | class="right-top in-block" |
132 | - @mousedown="moveStart('RIGHT')" | |
133 | - @mouseup="moveStop('RIGHT')" | |
146 | + @mousedown="moveStart(VideoControlEnum.Right)" | |
147 | + @mouseup="moveStop(VideoControlEnum.Right)" | |
134 | 148 | > |
135 | 149 | <CaretRightOutlined class="icon-rotate child-icon" /> |
136 | 150 | </Button> |
... | ... | @@ -138,15 +152,15 @@ |
138 | 152 | <div> |
139 | 153 | <Button |
140 | 154 | class="left-bottom in-block" |
141 | - @mousedown="moveStart('LEFT')" | |
142 | - @mouseup="moveStop('LEFT')" | |
155 | + @mousedown="moveStart(VideoControlEnum.Left)" | |
156 | + @mouseup="moveStop(VideoControlEnum.Left)" | |
143 | 157 | > |
144 | 158 | <CaretLeftOutlined class="icon-rotate child-icon" /> |
145 | 159 | </Button> |
146 | 160 | <Button |
147 | 161 | class="right-bottom in-block" |
148 | - @mousedown="moveStart('DOWN')" | |
149 | - @mouseup="moveStop('DOWN')" | |
162 | + @mousedown="moveStart(VideoControlEnum.Down)" | |
163 | + @mouseup="moveStop(VideoControlEnum.Down)" | |
150 | 164 | > |
151 | 165 | <CaretDownOutlined class="icon-rotate child-icon" /> |
152 | 166 | </Button> |
... | ... | @@ -161,16 +175,16 @@ |
161 | 175 | <div class="flex justify-center mt-8"> |
162 | 176 | <Button |
163 | 177 | class="button-icon" |
164 | - @mousedown="moveStart('ZOOM_IN')" | |
165 | - @mouseup="moveStop('ZOOM_IN')" | |
178 | + @mousedown="moveStart(VideoControlEnum.ZoomIn)" | |
179 | + @mouseup="moveStop(VideoControlEnum.ZoomIn)" | |
166 | 180 | style="border-radius: 50%" |
167 | 181 | > |
168 | 182 | <ZoomInOutlined style="color: #315a9c; font-size: 1.5rem" /> |
169 | 183 | </Button> |
170 | 184 | <Button |
171 | 185 | class="ml-10 button-icon" |
172 | - @mousedown="moveStart('ZOOM_OUT')" | |
173 | - @mouseup="moveStop('ZOOM_OUT')" | |
186 | + @mousedown="moveStart(VideoControlEnum.ZoomOut)" | |
187 | + @mouseup="moveStop(VideoControlEnum.ZoomOut)" | |
174 | 188 | style="border-radius: 50%" |
175 | 189 | > |
176 | 190 | <ZoomOutOutlined style="color: #315a9c; font-size: 1.5rem" /> | ... | ... |
... | ... | @@ -147,6 +147,8 @@ |
147 | 147 | ? 'warning' |
148 | 148 | : record.deviceState == DeviceState.ONLINE |
149 | 149 | ? 'success' |
150 | + : record.deviceState == DeviceState.ACTIVE | |
151 | + ? 'success' | |
150 | 152 | : 'error' |
151 | 153 | " |
152 | 154 | class="ml-2" |
... | ... | @@ -156,6 +158,8 @@ |
156 | 158 | ? '待激活' |
157 | 159 | : record.deviceState == DeviceState.ONLINE |
158 | 160 | ? '在线' |
161 | + : record.deviceState == DeviceState.ACTIVE | |
162 | + ? '活动' | |
159 | 163 | : '离线' |
160 | 164 | }} |
161 | 165 | </Tag> |
... | ... | @@ -231,7 +235,7 @@ |
231 | 235 | ifShow: authBtn(role) && record.customerId === undefined, |
232 | 236 | color: 'error', |
233 | 237 | popConfirm: { |
234 | - title: '是否确认删除', | |
238 | + title: !!record.isEdge ? '此设备来自边端,请谨慎删除' : '是否确认删除', | |
235 | 239 | confirm: handleDelete.bind(null, record), |
236 | 240 | }, |
237 | 241 | }, |
... | ... | @@ -707,4 +711,11 @@ |
707 | 711 | border-right: 30px solid transparent; |
708 | 712 | } |
709 | 713 | } |
714 | + | |
715 | + .device-name-edge { | |
716 | + width: 100%; | |
717 | + height: 100%; | |
718 | + padding: 0 !important; | |
719 | + position: relative; | |
720 | + } | |
710 | 721 | </style> | ... | ... |
... | ... | @@ -24,6 +24,7 @@ |
24 | 24 | import { BasicCardList, useCardList } from '/@/components/CardList'; |
25 | 25 | import { useRoute } from 'vue-router'; |
26 | 26 | import { ref } from 'vue'; |
27 | + import edgefornt from '/@/assets/icons/edgefornt.svg'; | |
27 | 28 | |
28 | 29 | defineProps<{ |
29 | 30 | mode: EnumTableCardMode; |
... | ... | @@ -154,7 +155,7 @@ |
154 | 155 | <template #renderItem="{ item }: BasicCardListRenderItem<ProfileRecord>"> |
155 | 156 | <Card hoverable> |
156 | 157 | <template #cover> |
157 | - <div class="h-full w-full !flex justify-center items-center text-center p-1"> | |
158 | + <div class="h-full w-full !flex justify-center items-center text-center p-1 relative"> | |
158 | 159 | <Image |
159 | 160 | @click.stop |
160 | 161 | wrapper-class-name="!w-32 !h-32 !flex !items-center overflow-hidden" |
... | ... | @@ -162,6 +163,13 @@ |
162 | 163 | placeholder |
163 | 164 | :fallback="IMAGE_FALLBACK" |
164 | 165 | /> |
166 | + <div v-if="item.isEdge" class="absolute top-0 left-0"> | |
167 | + <img :src="edgefornt" /> | |
168 | + <span | |
169 | + class="absolute top-0 left-0 text-light-50 transform -rotate-45 translate-y-1 translate-x-0.5" | |
170 | + >边</span | |
171 | + > | |
172 | + </div> | |
165 | 173 | </div> |
166 | 174 | </template> |
167 | 175 | <template class="ant-card-actions" #actions> |
... | ... | @@ -199,7 +207,7 @@ |
199 | 207 | auth: ProductPermission.DELETE, |
200 | 208 | icon: 'ant-design:delete-outlined', |
201 | 209 | popconfirm: { |
202 | - title: '是否确认删除操作?', | |
210 | + title: !!item.isEdge ? '此产品来自边端,请谨慎删除' : '是否确认删除', | |
203 | 211 | onConfirm: handleDelete.bind(null, [item.id]), |
204 | 212 | disabled: item.default || item.name == 'default', |
205 | 213 | }, | ... | ... |
... | ... | @@ -86,6 +86,7 @@ |
86 | 86 | import TransportConfigurationStep from './step/TransportConfigurationStep.vue'; |
87 | 87 | import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue'; |
88 | 88 | import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; |
89 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
89 | 90 | |
90 | 91 | const emits = defineEmits(['success', 'register']); |
91 | 92 | const activeKey = ref('1'); |
... | ... | @@ -188,6 +189,10 @@ |
188 | 189 | await getDeviceConfFormData(); |
189 | 190 | await getTransConfData(); |
190 | 191 | const isEmptyObj = isEmpty(transportConfData.profileData.transportConfiguration); |
192 | + if (Reflect.has(postSubmitFormData.deviceConfData, 'deleteUrl') && values.deleteUrl.deleteUrl) { | |
193 | + await deleteFilePath(postSubmitFormData.deviceConfData?.deleteUrl); | |
194 | + Reflect.deleteProperty(postSubmitFormData.deviceConfData, 'deleteUrl'); | |
195 | + } | |
191 | 196 | await deviceConfigAddOrEdit({ |
192 | 197 | ...postSubmitFormData.deviceConfData, |
193 | 198 | ...{ transportType: !isEmptyObj ? transportTypeStr.value : 'DEFAULT' }, | ... | ... |
... | ... | @@ -23,7 +23,7 @@ |
23 | 23 | } |
24 | 24 | ); |
25 | 25 | |
26 | - const emits = defineEmits(['update:value', 'change']); | |
26 | + const emits = defineEmits(['update:value', 'change', 'hexChange']); | |
27 | 27 | |
28 | 28 | const typOptions = Object.values(InputTypeEnum).map((value) => ({ label: value, value })); |
29 | 29 | |
... | ... | @@ -114,6 +114,10 @@ |
114 | 114 | ? getHexToDec(unref(getInputValue)!) |
115 | 115 | : `${hexWithPrefix ? '0x' : ''}${Number(unref(getInputValue)).toString(16).toUpperCase()}`; |
116 | 116 | }); |
117 | + | |
118 | + const handleInputTypeChange = (e) => { | |
119 | + emits('hexChange', e); | |
120 | + }; | |
117 | 121 | </script> |
118 | 122 | |
119 | 123 | <template> |
... | ... | @@ -124,6 +128,7 @@ |
124 | 128 | class="min-w-20" |
125 | 129 | :options="typOptions" |
126 | 130 | :disabled="disabled" |
131 | + @change="handleInputTypeChange" | |
127 | 132 | /> |
128 | 133 | <Input v-bind="$attrs" v-model:value="getInputValue" class="!w-full" :disabled="disabled" /> |
129 | 134 | <div class="min-w-14 h-full px-2 flex-grow flex-shrink-0 text-center leading-8 bg-gray-200"> | ... | ... |
... | ... | @@ -13,7 +13,13 @@ |
13 | 13 | <BasicForm @register="registerForm"> |
14 | 14 | <template #importType="{ model }"> |
15 | 15 | <RadioGroup v-model:value="model.importType"> |
16 | - <Authority :value="['api:yt:things_model:category:import', 'api:yt:things_model:import']"> | |
16 | + <Authority | |
17 | + :value=" | |
18 | + record?.ifShowClass | |
19 | + ? ['api:yt:things_model:category:import'] | |
20 | + : ['api:yt:things_model:import'] | |
21 | + " | |
22 | + > | |
17 | 23 | <Radio value="2">JSON导入</Radio> |
18 | 24 | </Authority> |
19 | 25 | <Authority :value="['api:yt:things_model:excel_import']"> |
... | ... | @@ -80,10 +86,11 @@ |
80 | 86 | label: '导入类型', |
81 | 87 | component: 'RadioGroup', |
82 | 88 | slot: 'importType', |
83 | - defaultValue: hasPermission([ | |
84 | - 'api:yt:things_model:category:import', | |
85 | - 'api:yt:things_model:import', | |
86 | - ]) | |
89 | + defaultValue: ( | |
90 | + props.record?.ifShowClass | |
91 | + ? hasPermission(['api:yt:things_model:category:import']) | |
92 | + : hasPermission(['api:yt:things_model:import']) | |
93 | + ) | |
87 | 94 | ? '2' |
88 | 95 | : '1', |
89 | 96 | helpMessage: | ... | ... |
... | ... | @@ -182,7 +182,7 @@ export const step1Schemas: FormSchema[] = [ |
182 | 182 | component: 'ApiUpload', |
183 | 183 | changeEvent: 'update:fileList', |
184 | 184 | valueField: 'fileList', |
185 | - componentProps: () => { | |
185 | + componentProps: ({ formModel }) => { | |
186 | 186 | return { |
187 | 187 | listType: 'picture-card', |
188 | 188 | maxFileLimit: 1, |
... | ... | @@ -204,10 +204,19 @@ export const step1Schemas: FormSchema[] = [ |
204 | 204 | onPreview: (fileList: FileItem) => { |
205 | 205 | createImgPreview({ imageList: [fileList.url!] }); |
206 | 206 | }, |
207 | + onDelete(url: string) { | |
208 | + formModel.deleteUrl = url!; | |
209 | + }, | |
207 | 210 | }; |
208 | 211 | }, |
209 | 212 | }, |
210 | 213 | { |
214 | + field: 'deleteUrl', | |
215 | + label: '', | |
216 | + component: 'Input', | |
217 | + show: false, | |
218 | + }, | |
219 | + { | |
211 | 220 | field: 'deviceType', |
212 | 221 | component: 'ApiRadioGroup', |
213 | 222 | label: '设备类型', | ... | ... |
... | ... | @@ -8,7 +8,7 @@ |
8 | 8 | > |
9 | 9 | <template #toolbar> |
10 | 10 | <div class="flex-auto"> |
11 | - <div class="mb-2"> | |
11 | + <div class="mb-2" v-if="isCurrentTenant"> | |
12 | 12 | <Alert type="info" show-icon> |
13 | 13 | <template #message> |
14 | 14 | <span v-if="!isShowBtn"> |
... | ... | @@ -39,6 +39,7 @@ |
39 | 39 | <Button type="primary" @click="handleExport" :loading="loading">导出物模型</Button> |
40 | 40 | </Authority> |
41 | 41 | <Authority |
42 | + v-if="isCurrentTenant" | |
42 | 43 | :value="[ |
43 | 44 | 'api:yt:things_model:import', |
44 | 45 | 'api:yt:things_model:category:import', |
... | ... | @@ -47,12 +48,18 @@ |
47 | 48 | > |
48 | 49 | <Button type="primary" @click="handleSelectImport">导入物模型</Button> |
49 | 50 | </Authority> |
50 | - <Button type="primary" v-if="!isShowBtn" @click="handleEditPhysicalModel" | |
51 | + <Button | |
52 | + type="primary" | |
53 | + v-if="!isShowBtn && isCurrentTenant" | |
54 | + @click="handleEditPhysicalModel" | |
51 | 55 | >编辑物模型</Button |
52 | 56 | > |
53 | 57 | </div> |
54 | 58 | <div class="flex gap-2"> |
55 | - <Authority :value="[ModelOfMatterPermission.RELEASE]"> | |
59 | + <Authority | |
60 | + v-if="!props.record.ifShowClass" | |
61 | + :value="[ModelOfMatterPermission.RELEASE]" | |
62 | + > | |
56 | 63 | <Popconfirm |
57 | 64 | title="是否需要发布上线?" |
58 | 65 | ok-text="确定" |
... | ... | @@ -159,11 +166,12 @@ |
159 | 166 | import { DataActionModeEnum } from '/@/enums/toolEnum'; |
160 | 167 | import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; |
161 | 168 | |
162 | - const { isPlatformAdmin, isSysadmin } = useRole(); | |
169 | + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole(); | |
163 | 170 | defineEmits(['register']); |
164 | 171 | |
165 | 172 | const props = defineProps<{ |
166 | 173 | record: DeviceProfileDetail; |
174 | + isCurrentTenant: Boolean; | |
167 | 175 | }>(); |
168 | 176 | |
169 | 177 | const { createMessage } = useMessage(); |
... | ... | @@ -224,7 +232,8 @@ |
224 | 232 | |
225 | 233 | const handleDeleteOrBatchDelete = async (record?: ModelOfMatterItemRecordType) => { |
226 | 234 | const deleteFn = |
227 | - props.record.ifShowClass && (unref(isPlatformAdmin) || unref(isSysadmin)) | |
235 | + props.record.ifShowClass && | |
236 | + (unref(isPlatformAdmin) || unref(isSysadmin) || unref(isTenantAdmin)) | |
228 | 237 | ? deleteModelCategory |
229 | 238 | : deleteModel; |
230 | 239 | |
... | ... | @@ -265,7 +274,10 @@ |
265 | 274 | const handleSelectImport = () => { |
266 | 275 | openModalSelect(true, { |
267 | 276 | id: props.record.id, |
268 | - isCateGory: (unref(isPlatformAdmin) || unref(isSysadmin)) && props.record.ifShowClass, | |
277 | + isCateGory: | |
278 | + unref(isPlatformAdmin) || | |
279 | + unref(isSysadmin) || | |
280 | + (unref(isTenantAdmin) && props.record.ifShowClass), | |
269 | 281 | }); |
270 | 282 | }; |
271 | 283 | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | import { DataActionModeEnum, DataActionModeNameEnum } from '/@/enums/toolEnum'; |
32 | 32 | import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; |
33 | 33 | |
34 | - const { isPlatformAdmin, isSysadmin } = useRole(); | |
34 | + const { isPlatformAdmin, isSysadmin, isTenantAdmin } = useRole(); | |
35 | 35 | |
36 | 36 | const emit = defineEmits(['register', 'success']); |
37 | 37 | |
... | ... | @@ -77,7 +77,9 @@ |
77 | 77 | const value = unref(objectModelElRef)!.getFieldsValue(); |
78 | 78 | |
79 | 79 | const isCategoryAction = |
80 | - (unref(isSysadmin) || unref(isPlatformAdmin)) && props.record.ifShowClass; | |
80 | + unref(isSysadmin) || | |
81 | + unref(isPlatformAdmin) || | |
82 | + (unref(isTenantAdmin) && props.record.ifShowClass); | |
81 | 83 | |
82 | 84 | const submitFn = |
83 | 85 | unref(openModalMode) === DataActionModeEnum.CREATE | ... | ... |
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 | + sortProperty: 'createdTime', | |
47 | + sortOrder: 'DESC', | |
48 | + }); | |
49 | + return { | |
50 | + total: res?.totalElements, | |
51 | + items: res?.data, | |
52 | + }; | |
53 | + }, | |
54 | + useSearchForm: true, | |
55 | + gutter: 4, | |
56 | + rowKey: 'routingKey', //id是对象 | |
57 | + formConfig: { | |
58 | + schemas: searchFormSchema, | |
59 | + labelWidth: 100, | |
60 | + model: { | |
61 | + name: (query as Recordable)?.name ? decodeURIComponent((query as Recordable)?.name) : null, | |
62 | + }, | |
63 | + }, | |
64 | + selections: { | |
65 | + beforeSelectValidate: () => { | |
66 | + return true; | |
67 | + }, | |
68 | + onSelect: (_record, _flag, allSelecteds) => { | |
69 | + disabledDeleteFlag.value = !allSelecteds.length; | |
70 | + }, | |
71 | + onSelectAll: () => { | |
72 | + // 全选事件 | |
73 | + disabledDeleteFlag.value = false; | |
74 | + }, | |
75 | + onUnSelectAll: () => { | |
76 | + // 反选事件 | |
77 | + disabledDeleteFlag.value = true; | |
78 | + }, | |
79 | + onSelectToggle: (status: boolean) => { | |
80 | + // 全选是false,反选是true | |
81 | + if (!status) disabledDeleteFlag.value = false; | |
82 | + else disabledDeleteFlag.value = true; | |
83 | + }, | |
84 | + }, | |
85 | + }); | |
86 | + | |
87 | + const [registerEdgeInstanceFormDrawer, { openDrawer: openEdgeInstanceFormDrawer }] = useDrawer(); | |
88 | + | |
89 | + const handleEventIsSuccess = () => reload(); | |
90 | + | |
91 | + const handleGoDetail = (record: EdgeInstanceItemType | null) => { | |
92 | + go('/edge/edge_detail/' + record?.id?.id); | |
93 | + }; | |
94 | + | |
95 | + const handleOperationEvent = ( | |
96 | + event: HandleOperationEnum, | |
97 | + record: EdgeInstanceItemType | null | |
98 | + ) => { | |
99 | + const isUpdate = event === HandleOperationEnum.CREATE ? false : true; | |
100 | + const isUpdateText = | |
101 | + event === HandleOperationEnum.CREATE | |
102 | + ? HandleOperationNameEnum.CREATE | |
103 | + : event === HandleOperationEnum.UPDATE | |
104 | + ? HandleOperationNameEnum.UPDATE | |
105 | + : HandleOperationNameEnum.VIEW; | |
106 | + if (event === HandleOperationEnum.VIEW) { | |
107 | + } else { | |
108 | + openEdgeInstanceFormDrawer(true, { isUpdate, record, isUpdateText, event }); | |
109 | + } | |
110 | + }; | |
111 | + | |
112 | + const handleDelete = async (event: HandleOperationEnum, id?: string | null) => { | |
113 | + try { | |
114 | + if (event === HandleOperationEnum.BATCH_DELETE) { | |
115 | + const batchDeleteIds = getSelectedRecords().map( | |
116 | + (rowRecord: EdgeInstanceItemType) => rowRecord?.id?.id | |
117 | + ); | |
118 | + if (isArray(batchDeleteIds) && batchDeleteIds.length === 0) return; | |
119 | + for (let item of batchDeleteIds) await deleteEdgeInstance(item!); | |
120 | + } else { | |
121 | + await deleteEdgeInstance(id!); | |
122 | + } | |
123 | + createMessage.success('删除成功'); | |
124 | + clearSelectedKeys(); | |
125 | + disabledDeleteFlag.value = true; | |
126 | + await reload(); | |
127 | + } catch (error) { | |
128 | + throw error; | |
129 | + } | |
130 | + }; | |
131 | +</script> | |
132 | + | |
133 | +<template> | |
134 | + <section> | |
135 | + <BasicCardList @register="registerCardList"> | |
136 | + <template #toolbar> | |
137 | + <Button type="primary" @click="handleOperationEvent(HandleOperationEnum.CREATE, null)" | |
138 | + >新增实例</Button | |
139 | + > | |
140 | + <Popconfirm | |
141 | + title="您确定要批量删除数据" | |
142 | + ok-text="确定" | |
143 | + cancel-text="取消" | |
144 | + @confirm="handleDelete(HandleOperationEnum.BATCH_DELETE, null)" | |
145 | + :disabled="disabledDeleteFlag" | |
146 | + > | |
147 | + <Button type="primary" danger :disabled="disabledDeleteFlag"> 批量删除 </Button> | |
148 | + </Popconfirm> | |
149 | + </template> | |
150 | + <template #renderItem="{ item }: BasicCardListRenderItem<EdgeInstanceItemType>"> | |
151 | + <Card hoverable> | |
152 | + <template #cover> | |
153 | + <div class="w-full h-full !flex flex-col justify-between m-3"> | |
154 | + <div class="!flex justify-between align-center text-center"> | |
155 | + <Tooltip :title="item.name"> | |
156 | + <span class="truncate font-bold fill-dark-900 text-sm"> {{ item.name }} </span> | |
157 | + </Tooltip> | |
158 | + <div class="mr-6"> | |
159 | + <a-tag class="!flex items-center" :color="item.active ? '#E8FFEA' : '#FFECE8'"> | |
160 | + <template #icon> | |
161 | + <template v-if="item.active"> | |
162 | + <Image :width="12" :height="12" :src="edgeStatusIsOnlinePng" /> | |
163 | + </template> | |
164 | + <template v-else> | |
165 | + <Image :width="12" :height="12" :src="edgeStatusIsOfflinePng" /> | |
166 | + </template> | |
167 | + </template> | |
168 | + <span class="ml-1" :style="{ color: item.active ? '#00B42A' : '#F53F3F' }">{{ | |
169 | + item.active ? '在线' : '离线' | |
170 | + }}</span> | |
171 | + </a-tag> | |
172 | + </div> | |
173 | + </div> | |
174 | + <div class="!flex justify-between align-center text-center"> | |
175 | + <span class="truncate text-xs" style="color: #86909c"> | |
176 | + {{ moment(item.createdTime).format('YYYY-MM-DD HH:mm:ss') }} | |
177 | + </span> | |
178 | + </div> | |
179 | + </div> | |
180 | + </template> | |
181 | + <template class="ant-card-actions" #actions> | |
182 | + <Tooltip title="详情"> | |
183 | + <AuthIcon | |
184 | + class="!text-lg" | |
185 | + icon="ant-design:eye-outlined" | |
186 | + @click.stop="handleGoDetail(item)" | |
187 | + /> | |
188 | + </Tooltip> | |
189 | + <Tooltip title="编辑"> | |
190 | + <AuthIcon | |
191 | + class="!text-lg" | |
192 | + icon="ant-design:form-outlined" | |
193 | + @click.stop="handleOperationEvent(HandleOperationEnum.UPDATE, item)" | |
194 | + /> | |
195 | + </Tooltip> | |
196 | + <AuthDropDown | |
197 | + @click.stop | |
198 | + :trigger="['hover']" | |
199 | + :drop-menu-list="[ | |
200 | + { | |
201 | + text: '删除', | |
202 | + event: DropMenuEvent.DELETE, | |
203 | + icon: 'ant-design:delete-outlined', | |
204 | + popconfirm: { | |
205 | + title: '是否确认删除操作?', | |
206 | + onConfirm: handleDelete.bind(null, HandleOperationEnum.DELETE, item?.id?.id), | |
207 | + }, | |
208 | + }, | |
209 | + ]" | |
210 | + /> | |
211 | + </template> | |
212 | + <Card.Meta> | |
213 | + <template #description> | |
214 | + <div class="truncate h-17 !flex justify-between flex-col"> | |
215 | + <div class="truncate !flex"> | |
216 | + <span class="text-xs" style="color: #86909c">标签</span> | |
217 | + <Tooltip :title="item.label"> | |
218 | + <span class="truncate ml-7.5 text-xs" style="color: #00b42a">{{ | |
219 | + item.label | |
220 | + }}</span> | |
221 | + </Tooltip> | |
222 | + </div> | |
223 | + <div class="truncate !flex"> | |
224 | + <span class="text-xs" style="color: #86909c">边缘类型</span> | |
225 | + <Tooltip :title="item.type"> | |
226 | + <span style="color: #4e5969" class="truncate ml-7.5 text-xs">{{ | |
227 | + item.type | |
228 | + }}</span> | |
229 | + </Tooltip> | |
230 | + </div> | |
231 | + <div class="truncate !flex"> | |
232 | + <span class="text-xs" style="color: #86909c">描述</span> | |
233 | + <Tooltip :title="item?.additionalInfo?.description"> | |
234 | + <span style="color: #4e5969" class="truncate ml-7.5 text-xs"> | |
235 | + {{ item?.additionalInfo?.description }} | |
236 | + </span> | |
237 | + </Tooltip> | |
238 | + </div> | |
239 | + </div> | |
240 | + </template> | |
241 | + </Card.Meta> | |
242 | + </Card> | |
243 | + </template> | |
244 | + </BasicCardList> | |
245 | + <EdgeInstanceFormDrawer | |
246 | + @register="registerEdgeInstanceFormDrawer" | |
247 | + @success="handleEventIsSuccess" | |
248 | + /> | |
249 | + </section> | |
250 | +</template> | |
251 | + | |
252 | +<style lang="less" scoped> | |
253 | + .profile-list:deep(.ant-image-img) { | |
254 | + @apply !w-full !h-full; | |
255 | + } | |
256 | + | |
257 | + .profile-list:deep(.ant-card-body) { | |
258 | + @apply !p-4; | |
259 | + } | |
260 | + | |
261 | + :deep(.ant-card-body) { | |
262 | + padding: 12px; | |
263 | + } | |
264 | +</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_detail/' + 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 { CSSProperties, h, 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 { Badge } from 'ant-design-vue'; | |
13 | + import Icon from '/@/components/Icon'; | |
14 | + | |
15 | + defineEmits(['register']); | |
16 | + | |
17 | + defineProps({ | |
18 | + recordData: { | |
19 | + type: Object as PropType<EdgeInstanceItemType>, | |
20 | + default: () => {}, | |
21 | + }, | |
22 | + }); | |
23 | + | |
24 | + const route = useRoute(); | |
25 | + | |
26 | + const edgeId = ref(route.params?.id as string); | |
27 | + | |
28 | + const go = useGo(); | |
29 | + | |
30 | + const { createMessage } = useMessage(); | |
31 | + | |
32 | + const [registerEdgeDeviceDistributionModal, { openModal }] = useModal(); | |
33 | + | |
34 | + const handleEventIsDistribution = () => { | |
35 | + openModal(true, { | |
36 | + record: 1, | |
37 | + }); | |
38 | + }; | |
39 | + | |
40 | + const handleEventIsCancelDistribution = async (record: EdgeDeviceItemType) => { | |
41 | + await edgeDeviceDeleteDistribution(edgeId.value as string, record.id.id); | |
42 | + createMessage.success('取消分配成功'); | |
43 | + reload(); | |
44 | + }; | |
45 | + | |
46 | + const AlarmDetailActionButton = ({ hasAlarm }: { hasAlarm?: boolean }) => | |
47 | + h( | |
48 | + Badge, | |
49 | + { offset: [0, -5] }, | |
50 | + { | |
51 | + default: () => h('span', { style: { color: '#377dff' } }, '详情'), | |
52 | + count: () => | |
53 | + h( | |
54 | + 'div', | |
55 | + { | |
56 | + style: { | |
57 | + visibility: hasAlarm ? 'visible' : 'hidden', | |
58 | + width: '14px', | |
59 | + height: '14px', | |
60 | + display: 'flex', | |
61 | + justifyContent: 'center', | |
62 | + alignItems: 'center', | |
63 | + border: '1px solid #f46161', | |
64 | + borderRadius: '50%', | |
65 | + } as CSSProperties, | |
66 | + }, | |
67 | + h(Icon, { icon: 'mdi:bell-warning', color: '#f46161', size: 12 }) | |
68 | + ), | |
69 | + } | |
70 | + ); | |
71 | + | |
72 | + const [registerTable, { reload }] = useTable({ | |
73 | + title: '边缘设备', | |
74 | + columns, | |
75 | + api: async ({ page, pageSize, textSearch }) => { | |
76 | + const res = await edgeDevicePage( | |
77 | + { | |
78 | + page: page - 1 < 0 ? 0 : page - 1, | |
79 | + pageSize, | |
80 | + textSearch, | |
81 | + sortProperty: 'createdTime', | |
82 | + sortOrder: 'DESC', | |
83 | + }, | |
84 | + edgeId.value as string | |
85 | + ); | |
86 | + return { | |
87 | + total: res?.totalElements, | |
88 | + items: res?.data, | |
89 | + }; | |
90 | + }, | |
91 | + formConfig: { | |
92 | + labelWidth: 100, | |
93 | + schemas: searchFormSchema, | |
94 | + }, | |
95 | + useSearchForm: true, | |
96 | + showIndexColumn: false, | |
97 | + clickToRowSelect: false, | |
98 | + showTableSetting: true, | |
99 | + bordered: true, | |
100 | + rowKey: 'name', | |
101 | + actionColumn: { | |
102 | + width: 140, | |
103 | + title: '操作', | |
104 | + slots: { customRender: 'action' }, | |
105 | + fixed: 'right', | |
106 | + }, | |
107 | + }); | |
108 | + | |
109 | + const handleEventIsSuccess = () => reload(); | |
110 | + | |
111 | + function handleGoDeviceDetail(record: Recordable) { | |
112 | + go(`/edge/edge_device/edge_device_detail/${record?.id?.id}/${edgeId.value}`); | |
113 | + } | |
114 | +</script> | |
115 | + | |
116 | +<template> | |
117 | + <BasicTable :clickToRowSelect="false" @register="registerTable"> | |
118 | + <template #toolbar> | |
119 | + <a-button type="primary" @click="handleEventIsDistribution"> 分配设备 </a-button> | |
120 | + </template> | |
121 | + <template #action="{ record }"> | |
122 | + <TableAction | |
123 | + :actions="[ | |
124 | + { | |
125 | + // label: '详情', | |
126 | + label: AlarmDetailActionButton({ hasAlarm: !!record.alarmStatus }), | |
127 | + icon: 'ant-design:eye-outlined', | |
128 | + onClick: handleGoDeviceDetail.bind(null, record), | |
129 | + }, | |
130 | + { | |
131 | + label: '取消分配', | |
132 | + icon: 'mdi:account-arrow-left', | |
133 | + onClick: handleEventIsCancelDistribution.bind(null, record), | |
134 | + }, | |
135 | + ]" | |
136 | + /> | |
137 | + </template> | |
138 | + </BasicTable> | |
139 | + <EdgeDeviceDistribution | |
140 | + @register="registerEdgeDeviceDistributionModal" | |
141 | + :edgeId="edgeId" | |
142 | + @success="handleEventIsSuccess" | |
143 | + /> | |
144 | +</template> | |
145 | + | |
146 | +<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="recordData?.deviceInfo?.avatar || 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 | +} | ... | ... |