Commit 2dcb746a4bad300319cf1a73e86f74836d80852b
Merge branch 'main_dev' into 'main'
Main dev See merge request yunteng/thingskit-front!1449
Showing
163 changed files
with
7310 additions
and
366 deletions
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 | +} | ... | ... |
1 | +import { unref } from 'vue'; | |
2 | +import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel'; | |
3 | +import { DataType, Specs, StructJSON } from '/@/api/device/model/modelOfMatterModel'; | |
4 | +import { FormSchema } from '/@/components/Form'; | |
5 | +import { DataTypeEnum, OriginalDataTypeEnum } from '/@/enums/objectModelEnum'; | |
6 | +import { TCPProtocolTypeEnum } from '/@/enums/deviceEnum'; | |
7 | +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
8 | + | |
9 | +export interface BasicCreateFormParams { | |
10 | + identifier: string; | |
11 | + functionName: string; | |
12 | + dataType: DataType; | |
13 | +} | |
14 | + | |
15 | +const validateDouble = (value: number, min?: number | string, max?: number | string) => { | |
16 | + min = Number(min) ?? Number.MIN_SAFE_INTEGER; | |
17 | + max = Number(max) ?? Number.MAX_SAFE_INTEGER; | |
18 | + | |
19 | + return { | |
20 | + flag: value < min || value > max, | |
21 | + message: `取值范围在${min}~${max}之间`, | |
22 | + }; | |
23 | +}; | |
24 | + | |
25 | +export const useGenerateFormSchemasByObjectModel = () => { | |
26 | + const createInputNumber = ({ | |
27 | + identifier, | |
28 | + functionName, | |
29 | + dataType, | |
30 | + }: BasicCreateFormParams): FormSchema => { | |
31 | + const { specs, type } = dataType; | |
32 | + const { valueRange, step } = specs! as Partial<Specs>; | |
33 | + const { max, min } = valueRange || {}; | |
34 | + return { | |
35 | + field: identifier, | |
36 | + label: functionName, | |
37 | + component: 'InputNumber', | |
38 | + rules: [ | |
39 | + { | |
40 | + type: 'number', | |
41 | + trigger: 'change', | |
42 | + validator: (_rule, value) => { | |
43 | + const { flag, message } = validateDouble(value, min, max); | |
44 | + if (flag) { | |
45 | + return Promise.reject(`${functionName}${message}`); | |
46 | + } | |
47 | + return Promise.resolve(value); | |
48 | + }, | |
49 | + }, | |
50 | + ], | |
51 | + componentProps: { | |
52 | + max: max ?? Number.MAX_SAFE_INTEGER, | |
53 | + min: min ?? Number.MIN_SAFE_INTEGER, | |
54 | + step, | |
55 | + placeholder: `请输入${functionName}`, | |
56 | + precision: type === DataTypeEnum.NUMBER_INT ? 0 : 2, | |
57 | + }, | |
58 | + } as FormSchema; | |
59 | + }; | |
60 | + | |
61 | + const createInput = ({ | |
62 | + identifier, | |
63 | + functionName, | |
64 | + dataType, | |
65 | + }: BasicCreateFormParams): FormSchema => { | |
66 | + const { specs } = dataType; | |
67 | + const { length = 10240 } = specs! as Partial<Specs>; | |
68 | + return { | |
69 | + field: identifier, | |
70 | + label: functionName, | |
71 | + component: 'Input', | |
72 | + rules: [ | |
73 | + { | |
74 | + type: 'string', | |
75 | + trigger: 'change', | |
76 | + validator: (_rule, value) => { | |
77 | + if (value?.length > length) { | |
78 | + return Promise.reject(`${functionName}数据长度应该小于${length}`); | |
79 | + } | |
80 | + return Promise.resolve(value); | |
81 | + }, | |
82 | + }, | |
83 | + ], | |
84 | + componentProps: { | |
85 | + maxLength: length, | |
86 | + placeholder: `请输入${functionName}`, | |
87 | + }, | |
88 | + } as FormSchema; | |
89 | + }; | |
90 | + | |
91 | + const createSelect = ({ | |
92 | + identifier, | |
93 | + functionName, | |
94 | + dataType, | |
95 | + }: BasicCreateFormParams): FormSchema => { | |
96 | + const { specs } = dataType; | |
97 | + const { boolClose, boolOpen } = specs! as Partial<Specs>; | |
98 | + return { | |
99 | + field: identifier, | |
100 | + label: functionName, | |
101 | + component: 'Select', | |
102 | + componentProps: { | |
103 | + options: [ | |
104 | + { label: `${boolClose}-0`, value: 0 }, | |
105 | + { label: `${boolOpen}-1`, value: 1 }, | |
106 | + ], | |
107 | + placeholder: `请选择${functionName}`, | |
108 | + getPopupContainer: () => document.body, | |
109 | + }, | |
110 | + }; | |
111 | + }; | |
112 | + | |
113 | + const createEnumSelect = ({ | |
114 | + identifier, | |
115 | + functionName, | |
116 | + dataType, | |
117 | + }: BasicCreateFormParams): FormSchema => { | |
118 | + const { specsList } = dataType; | |
119 | + return { | |
120 | + field: identifier, | |
121 | + label: functionName, | |
122 | + component: 'Select', | |
123 | + componentProps: { | |
124 | + options: specsList?.map((item) => ({ label: item.name, value: item.value })), | |
125 | + placeholder: `请选择${functionName}`, | |
126 | + getPopupContainer: () => document.body, | |
127 | + }, | |
128 | + }; | |
129 | + }; | |
130 | + | |
131 | + const createModbusValueInput = (objectModel: DeviceModelOfMatterAttrs): FormSchema => { | |
132 | + const { identifier, name, detail, extensionDesc } = objectModel; | |
133 | + | |
134 | + const { dataType } = detail || {}; | |
135 | + const { specs } = dataType || {}; | |
136 | + const { valueRange } = specs as Specs; | |
137 | + const { max, min } = valueRange || {}; | |
138 | + | |
139 | + if (extensionDesc?.originalDataType === OriginalDataTypeEnum.BOOLEAN) { | |
140 | + const options = [ | |
141 | + { label: '闭合', value: parseInt('FF00', 16) }, | |
142 | + { label: '断开', value: parseInt('0000', 16) }, | |
143 | + ]; | |
144 | + | |
145 | + return { | |
146 | + field: identifier, | |
147 | + label: name, | |
148 | + component: 'Select', | |
149 | + componentProps: () => { | |
150 | + return { | |
151 | + options, | |
152 | + placeholder: `请选择${name}`, | |
153 | + getPopupContainer: () => document.body, | |
154 | + }; | |
155 | + }, | |
156 | + }; | |
157 | + } | |
158 | + | |
159 | + const isStringType = extensionDesc?.originalDataType === OriginalDataTypeEnum.STRING; | |
160 | + return { | |
161 | + field: identifier, | |
162 | + label: name, | |
163 | + component: isStringType ? 'Input' : 'InputNumber', | |
164 | + rules: isStringType | |
165 | + ? [] | |
166 | + : [ | |
167 | + { | |
168 | + type: 'number', | |
169 | + validator: (_rule, value) => { | |
170 | + const { flag, message } = validateDouble(value, min, max); | |
171 | + if (flag) { | |
172 | + return Promise.reject(`${name}${message}`); | |
173 | + } | |
174 | + return Promise.resolve(value); | |
175 | + }, | |
176 | + }, | |
177 | + ], | |
178 | + componentProps: { | |
179 | + max: max ?? Number.MAX_SAFE_INTEGER, | |
180 | + min: min ?? Number.MIN_SAFE_INTEGER, | |
181 | + placeholder: `请输入${name}`, | |
182 | + // precision: floatType.includes(extensionDesc!.originalDataType) ? 2 : 0, | |
183 | + }, | |
184 | + }; | |
185 | + }; | |
186 | + | |
187 | + const schemaMethod = { | |
188 | + [DataTypeEnum.BOOL]: createSelect, | |
189 | + [DataTypeEnum.NUMBER_DOUBLE]: createInputNumber, | |
190 | + [DataTypeEnum.NUMBER_INT]: createInputNumber, | |
191 | + [DataTypeEnum.STRING]: createInput, | |
192 | + [DataTypeEnum.ENUM]: createEnumSelect, | |
193 | + }; | |
194 | + | |
195 | + const getFormByObjectModel = ( | |
196 | + objectModel: DeviceModelOfMatterAttrs, | |
197 | + deviceDetail: EdgeDeviceItemType | |
198 | + ): FormSchema[] => { | |
199 | + const { name, identifier, detail } = objectModel; | |
200 | + | |
201 | + const isTCPModbusProduct = | |
202 | + unref(deviceDetail).deviceProfile?.profileData?.transportConfiguration?.protocol === | |
203 | + TCPProtocolTypeEnum.MODBUS_RTU; | |
204 | + | |
205 | + const { dataType } = detail; | |
206 | + const { type } = dataType || {}; | |
207 | + | |
208 | + if (isTCPModbusProduct) { | |
209 | + return [createModbusValueInput(objectModel)]; | |
210 | + } | |
211 | + | |
212 | + if (type === DataTypeEnum.STRUCT) { | |
213 | + return (dataType?.specs as StructJSON[]).map((item) => { | |
214 | + const { functionName, identifier, dataType } = item; | |
215 | + const { type } = dataType || {}; | |
216 | + | |
217 | + return schemaMethod[type!]?.({ identifier, functionName, dataType }); | |
218 | + }); | |
219 | + } | |
220 | + | |
221 | + const result = schemaMethod[type!]?.({ identifier, functionName: name, dataType: dataType! }); | |
222 | + return result ? [result] : []; | |
223 | + }; | |
224 | + | |
225 | + return { getFormByObjectModel }; | |
226 | +}; | ... | ... |
1 | +import { OriginalDataTypeEnum } from '/@/enums/objectModelEnum'; | |
2 | + | |
3 | +export type OriginalDataTypePrefixType<S = `${OriginalDataTypeEnum}`> = S extends string | |
4 | + ? S extends `${infer D}_${string}` | |
5 | + ? D | |
6 | + : S | |
7 | + : ''; | |
8 | + | |
9 | +function getRegisterCount(originalDataType: OriginalDataTypeEnum) { | |
10 | + switch (originalDataType) { | |
11 | + case OriginalDataTypeEnum.INT16_AB: | |
12 | + case OriginalDataTypeEnum.INT16_BA: | |
13 | + case OriginalDataTypeEnum.UINT16_AB: | |
14 | + case OriginalDataTypeEnum.UINT16_BA: | |
15 | + case OriginalDataTypeEnum.BITS: | |
16 | + case OriginalDataTypeEnum.BOOLEAN: | |
17 | + return 1; | |
18 | + | |
19 | + case OriginalDataTypeEnum.INT32_AB_CD: | |
20 | + case OriginalDataTypeEnum.INT32_BA_DC: | |
21 | + case OriginalDataTypeEnum.INT32_CD_AB: | |
22 | + case OriginalDataTypeEnum.INT32_DC_BA: | |
23 | + case OriginalDataTypeEnum.UINT32_AB_CD: | |
24 | + case OriginalDataTypeEnum.UINT32_BA_DC: | |
25 | + case OriginalDataTypeEnum.UINT32_CD_AB: | |
26 | + case OriginalDataTypeEnum.UINT32_DC_BA: | |
27 | + return 2; | |
28 | + | |
29 | + case OriginalDataTypeEnum.FLOAT_AB_CD: | |
30 | + case OriginalDataTypeEnum.FLOAT_BA_DC: | |
31 | + case OriginalDataTypeEnum.FLOAT_CD_AB: | |
32 | + case OriginalDataTypeEnum.FLOAT_DC_BA: | |
33 | + return 2; | |
34 | + | |
35 | + case OriginalDataTypeEnum.DOUBLE: | |
36 | + return 4; | |
37 | + } | |
38 | +} | |
39 | + | |
40 | +export function useParseOriginalDataType(originalDataType: OriginalDataTypeEnum) { | |
41 | + const signedMatchRef = /^UN/; | |
42 | + | |
43 | + const splitArray = originalDataType.split('_') as [OriginalDataTypePrefixType]; | |
44 | + | |
45 | + const [dataType] = splitArray; | |
46 | + | |
47 | + const exchangeSortFlag = splitArray.slice(1).join('_'); | |
48 | + | |
49 | + return { | |
50 | + registerCount: getRegisterCount(originalDataType), | |
51 | + unsigned: !signedMatchRef.test(originalDataType), | |
52 | + dataType, | |
53 | + exchangeSortFlag: exchangeSortFlag || null, | |
54 | + }; | |
55 | +} | ... | ... |
1 | +<script lang="ts"> | |
2 | + import { defineComponent, reactive } from 'vue'; | |
3 | + import { BasicTable, useTable } from '/@/components/Table'; | |
4 | + import { realTimeDataColumns } from './config'; | |
5 | + import { useWebSocket } from '@vueuse/core'; | |
6 | + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum'; | |
7 | + import { getAuthCache } from '/@/utils/auth'; | |
8 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
9 | + import type { socketDataType } from './types'; | |
10 | + import { useGlobSetting } from '/@/hooks/setting'; | |
11 | + | |
12 | + export default defineComponent({ | |
13 | + name: 'RealTimeData', | |
14 | + components: { | |
15 | + BasicTable, | |
16 | + }, | |
17 | + props: { | |
18 | + deviceDetail: { | |
19 | + type: Object, | |
20 | + required: true, | |
21 | + }, | |
22 | + }, | |
23 | + setup(props) { | |
24 | + const token: string = getAuthCache(JWT_TOKEN_KEY); | |
25 | + const { socketUrl } = useGlobSetting(); | |
26 | + const state: any = reactive({ | |
27 | + server: `${socketUrl}${token}`, | |
28 | + sendValue: JSON.stringify({ | |
29 | + attrSubCmds: [], | |
30 | + tsSubCmds: [ | |
31 | + { | |
32 | + entityType: 'DEVICE', | |
33 | + entityId: props.deviceDetail.tbDeviceId, | |
34 | + scope: 'LATEST_TELEMETRY', | |
35 | + cmdId: 1, | |
36 | + }, | |
37 | + ], | |
38 | + historyCmds: [], | |
39 | + entityDataCmds: [], | |
40 | + entityDataUnsubscribeCmds: [], | |
41 | + alarmDataCmds: [], | |
42 | + alarmDataUnsubscribeCmds: [], | |
43 | + entityCountCmds: [], | |
44 | + entityCountUnsubscribeCmds: [], | |
45 | + }), | |
46 | + recordList: Array<socketDataType>(), | |
47 | + }); | |
48 | + const { createMessage } = useMessage(); | |
49 | + const [registerTable, { getForm, setTableData }] = useTable({ | |
50 | + columns: realTimeDataColumns, | |
51 | + showTableSetting: true, | |
52 | + bordered: true, | |
53 | + showIndexColumn: false, | |
54 | + dataSource: state.recordList, | |
55 | + useSearchForm: true, | |
56 | + formConfig: { | |
57 | + labelWidth: 100, | |
58 | + schemas: [ | |
59 | + { | |
60 | + field: 'key', | |
61 | + label: '键/值', | |
62 | + component: 'Input', | |
63 | + colProps: { span: 6 }, | |
64 | + componentProps: { | |
65 | + placeholder: '请输入 键/值', | |
66 | + maxLength: 255, | |
67 | + }, | |
68 | + }, | |
69 | + ], | |
70 | + // 自定义前端查询 | |
71 | + submitFunc() { | |
72 | + const { getFieldsValue } = getForm(); | |
73 | + const { key } = getFieldsValue(); | |
74 | + if (!key) { | |
75 | + setTableData(state.recordList); | |
76 | + return; | |
77 | + } | |
78 | + let newRecordList = []; | |
79 | + let len = state.recordList.length; | |
80 | + for (let i = 0; i < len; i++) { | |
81 | + if ( | |
82 | + state.recordList[i].key.indexOf(key) >= 0 || | |
83 | + state.recordList[i].value.indexOf(key) >= 0 | |
84 | + ) { | |
85 | + newRecordList.push(state.recordList[i] as any as never); | |
86 | + } | |
87 | + } | |
88 | + setTableData(newRecordList); | |
89 | + }, | |
90 | + resetFunc() { | |
91 | + setTableData(state.recordList); | |
92 | + }, | |
93 | + }, | |
94 | + }); | |
95 | + | |
96 | + const { send, close } = useWebSocket(state.server, { | |
97 | + onConnected() { | |
98 | + send(state.sendValue); | |
99 | + }, | |
100 | + onMessage(_, e) { | |
101 | + const { data } = JSON.parse(e.data); | |
102 | + const newArray: socketDataType[] = []; | |
103 | + for (const key in data) { | |
104 | + const [time, value] = data[key].flat(1); | |
105 | + let obj = { | |
106 | + key, | |
107 | + time, | |
108 | + value, | |
109 | + }; | |
110 | + if (state.recordList.length === 0) { | |
111 | + state.recordList.unshift(obj); | |
112 | + } else { | |
113 | + newArray.push(obj); | |
114 | + } | |
115 | + } | |
116 | + newArray.forEach((item) => { | |
117 | + let flag = false; | |
118 | + state.recordList.forEach((item1) => { | |
119 | + if (item1.key === item.key) { | |
120 | + item1.value = item.value; | |
121 | + item1.time = item.time; | |
122 | + flag = true; | |
123 | + } | |
124 | + }); | |
125 | + if (!flag) { | |
126 | + state.recordList.unshift(item); | |
127 | + } | |
128 | + }); | |
129 | + }, | |
130 | + onDisconnected() { | |
131 | + close(); | |
132 | + }, | |
133 | + onError() { | |
134 | + createMessage.error('webSocket连接超时,请联系管理员'); | |
135 | + }, | |
136 | + }); | |
137 | + return { | |
138 | + registerTable, | |
139 | + }; | |
140 | + }, | |
141 | + }); | |
142 | +</script> | |
143 | + | |
144 | +<template> | |
145 | + <div style="background-color: #f0f2f5"> | |
146 | + <BasicTable @register="registerTable" /> | |
147 | + </div> | |
148 | +</template> | ... | ... |
1 | +import { BasicColumn } from '/@/components/Table'; | |
2 | +import { formatToDateTime } from '/@/utils/dateUtil'; | |
3 | +import { isNullOrUnDef } from '/@/utils/is'; | |
4 | + | |
5 | +// 实时数据表格 | |
6 | +export const realTimeDataColumns: BasicColumn[] = [ | |
7 | + { | |
8 | + title: '键', | |
9 | + dataIndex: 'name', | |
10 | + width: 100, | |
11 | + }, | |
12 | + { | |
13 | + title: '值', | |
14 | + dataIndex: 'rawValue', | |
15 | + width: 160, | |
16 | + format(text) { | |
17 | + return isNullOrUnDef(text) ? '--' : text; | |
18 | + }, | |
19 | + }, | |
20 | + { | |
21 | + title: '最后更新时间', | |
22 | + dataIndex: 'time', | |
23 | + width: 120, | |
24 | + format: (text) => formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss'), | |
25 | + }, | |
26 | +]; | ... | ... |
1 | +import { EChartsOption } from 'echarts'; | |
2 | +import moment from 'moment'; | |
3 | +import { computed, ref, unref } from 'vue'; | |
4 | +import { HistoryData } from '/@/api/alarm/position/model'; | |
5 | +import { getDeviceAttributes } from '/@/api/dataBoard'; | |
6 | +import { DeviceAttributeRecord } from '/@/api/dataBoard/model'; | |
7 | +import { dateUtil } from '/@/utils/dateUtil'; | |
8 | +import { isArray } from '/@/utils/is'; | |
9 | +import { DEFAULT_DATE_FORMAT } from '/@/views/visual/board/detail/config/util'; | |
10 | +import { QueryWay, SchemaFiled } from '/@/views/visual/palette/components/HistoryTrendModal/config'; | |
11 | +import { eChartOptions } from '/@/views/device/localtion/config.data'; | |
12 | +import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
13 | + | |
14 | +// interface DeviceOption { | |
15 | +// deviceProfileId: string; | |
16 | +// } | |
17 | + | |
18 | +export function useHistoryData() { | |
19 | + const deviceAttrs = ref<DeviceAttributeRecord[]>([]); | |
20 | + | |
21 | + const getDeviceKeys = computed(() => { | |
22 | + return unref(deviceAttrs).map((item) => item.identifier); | |
23 | + }); | |
24 | + | |
25 | + const getDeviceAttribute = async (record: EdgeDeviceItemType) => { | |
26 | + try { | |
27 | + const { deviceProfileId } = record; | |
28 | + const { id } = deviceProfileId; | |
29 | + const list = (await getDeviceAttributes({ deviceProfileId: id })) || []; | |
30 | + deviceAttrs.value = isArray(list) ? list : []; | |
31 | + } catch (error) { | |
32 | + throw error; | |
33 | + } | |
34 | + }; | |
35 | + | |
36 | + function getSearchParams(value: Partial<Record<SchemaFiled, string>>) { | |
37 | + const { startTs, endTs, interval, agg, limit, keys, way, deviceId, orderBy } = value; | |
38 | + const basicRecord = { | |
39 | + entityId: deviceId, | |
40 | + keys: keys ? keys : unref(getDeviceKeys).join(), | |
41 | + interval, | |
42 | + agg, | |
43 | + limit, | |
44 | + orderBy, | |
45 | + }; | |
46 | + if (way === QueryWay.LATEST) { | |
47 | + return Object.assign(basicRecord, { | |
48 | + startTs: moment().subtract(startTs, 'ms').valueOf(), | |
49 | + endTs: Date.now(), | |
50 | + }); | |
51 | + } else { | |
52 | + return Object.assign(basicRecord, { | |
53 | + startTs: moment(startTs).valueOf(), | |
54 | + endTs: moment(endTs).valueOf(), | |
55 | + }); | |
56 | + } | |
57 | + } | |
58 | + | |
59 | + function setChartOptions( | |
60 | + data: HistoryData, | |
61 | + keys?: DeviceAttributeRecord | DeviceAttributeRecord[] | |
62 | + ) { | |
63 | + const dataArray: [string, string, string][] = []; | |
64 | + for (const key in data) { | |
65 | + for (const item of data[key]) { | |
66 | + let { value } = item; | |
67 | + const { ts } = item; | |
68 | + const time = dateUtil(ts).format(DEFAULT_DATE_FORMAT); | |
69 | + value = Number(value).toFixed(2); | |
70 | + dataArray.push([time, value, key as string]); | |
71 | + } | |
72 | + } | |
73 | + | |
74 | + keys = keys ? [keys as DeviceAttributeRecord] : unref(deviceAttrs); | |
75 | + const legend = keys.map((item) => item.name); | |
76 | + | |
77 | + const series: EChartsOption['series'] = (keys as DeviceAttributeRecord[]).map((item) => { | |
78 | + return { | |
79 | + name: item.name, | |
80 | + type: 'line', | |
81 | + data: dataArray.filter((temp) => temp[2] === item.identifier), | |
82 | + }; | |
83 | + }); | |
84 | + | |
85 | + return eChartOptions(series, legend); | |
86 | + } | |
87 | + | |
88 | + return { | |
89 | + deviceAttrs, | |
90 | + getDeviceKeys, | |
91 | + getDeviceAttribute, | |
92 | + getSearchParams, | |
93 | + setChartOptions, | |
94 | + }; | |
95 | +} | ... | ... |
1 | +export { default as EdgeDevicePhysicalModel } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { nextTick, onMounted, onUnmounted, reactive, ref, unref, computed } from 'vue'; | |
3 | + import { List, Card, Tooltip, Space, PaginationProps } from 'ant-design-vue'; | |
4 | + import { PageWrapper } from '/@/components/Page'; | |
5 | + import { BasicTable, useTable } from '/@/components/Table'; | |
6 | + import { realTimeDataColumns } from './config'; | |
7 | + import { useWebSocket } from '@vueuse/core'; | |
8 | + import { getAuthCache } from '/@/utils/auth'; | |
9 | + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum'; | |
10 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
11 | + import { formatToDateTime } from '/@/utils/dateUtil'; | |
12 | + import { BasicForm, useForm } from '/@/components/Form'; | |
13 | + import HistoryData from './HistoryData.vue'; | |
14 | + import { BasicModal, useModal } from '/@/components/Modal'; | |
15 | + import { getDeviceAttrs } from '/@/api/device/deviceManager'; | |
16 | + import { isArray, isNull, isNullOrUnDef, isObject } from '/@/utils/is'; | |
17 | + import { useGlobSetting } from '/@/hooks/setting'; | |
18 | + import { ModeSwitchButton, EnumTableCardMode } from '/@/components/Widget'; | |
19 | + import { toRaw } from 'vue'; | |
20 | + import { DataActionModeEnum } from '/@/enums/toolEnum'; | |
21 | + import { ReadAndWriteEnum, TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum'; | |
22 | + import { ObjectModelCommandDeliveryModal } from './ObjectModelCommandDeliveryModal'; | |
23 | + import { ModalParamsType } from '/#/utils'; | |
24 | + import { AreaChartOutlined } from '@ant-design/icons-vue'; | |
25 | + import { SvgIcon, Icon } from '/@/components/Icon'; | |
26 | + import { DataTypeEnum } from '/@/enums/objectModelEnum'; | |
27 | + import { | |
28 | + buildTableDataSourceByObjectModel, | |
29 | + SocketInfoDataSourceItemType, | |
30 | + StructValueItemType, | |
31 | + } from './ModelOfMatter.config'; | |
32 | + import { useJsonParse } from '/@/hooks/business/useJsonParse'; | |
33 | + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
34 | + import { DeviceModelOfMatterAttrs } from '/@/api/device/model/deviceModel'; | |
35 | + | |
36 | + interface ReceiveMessage { | |
37 | + data: { | |
38 | + [key: string]: [number, string][]; | |
39 | + }; | |
40 | + } | |
41 | + | |
42 | + const props = defineProps<{ | |
43 | + deviceDetail: EdgeDeviceItemType; | |
44 | + }>(); | |
45 | + | |
46 | + const grid = { | |
47 | + gutter: 8, | |
48 | + column: 3, | |
49 | + } as any; | |
50 | + | |
51 | + const listElRef = ref<Nullable<ComponentElRef<HTMLDivElement>>>(null); | |
52 | + | |
53 | + const token = getAuthCache(JWT_TOKEN_KEY); | |
54 | + | |
55 | + const { socketUrl } = useGlobSetting(); | |
56 | + | |
57 | + const pagination = reactive<PaginationProps>({ | |
58 | + size: 'small', | |
59 | + showSizeChanger: true, | |
60 | + showQuickJumper: true, | |
61 | + hideOnSinglePage: false, | |
62 | + showTotal: (total: number) => `共${total}条数据`, | |
63 | + onChange: handleFilterChange, | |
64 | + onShowSizeChange: handleFilterChange, | |
65 | + }); | |
66 | + | |
67 | + const socketInfo = reactive({ | |
68 | + cmdId: 2, | |
69 | + origin: `${socketUrl}${token}`, | |
70 | + attr: undefined as string | undefined, | |
71 | + dataSource: [] as SocketInfoDataSourceItemType[], | |
72 | + rawDataSource: [] as SocketInfoDataSourceItemType[], | |
73 | + message: {} as ReceiveMessage['data'], | |
74 | + attrKeys: [] as string[], | |
75 | + filterAttrKeys: [] as string[], | |
76 | + }); | |
77 | + | |
78 | + function createUnsubscribeMessage(cmdId: number) { | |
79 | + return { | |
80 | + tsSubCmds: [ | |
81 | + { | |
82 | + cmdId, | |
83 | + unsubscribe: true, | |
84 | + }, | |
85 | + ], | |
86 | + }; | |
87 | + } | |
88 | + | |
89 | + const getSendValue = computed(() => { | |
90 | + return { | |
91 | + tsSubCmds: [ | |
92 | + { | |
93 | + entityType: 'DEVICE', | |
94 | + entityId: props.deviceDetail?.id?.id, | |
95 | + scope: 'LATEST_TELEMETRY', | |
96 | + cmdId: socketInfo.cmdId, | |
97 | + type: 'TIMESERIES', | |
98 | + }, | |
99 | + ], | |
100 | + }; | |
101 | + }); | |
102 | + | |
103 | + const getFilterSendValue = computed(() => { | |
104 | + return { | |
105 | + tsSubCmds: [ | |
106 | + { | |
107 | + entityType: 'DEVICE', | |
108 | + entityId: props.deviceDetail?.id?.id, | |
109 | + scope: 'LATEST_TELEMETRY', | |
110 | + cmdId: socketInfo.cmdId, | |
111 | + type: 'TIMESERIES', | |
112 | + }, | |
113 | + ], | |
114 | + }; | |
115 | + }); | |
116 | + | |
117 | + const [registerForm, { getFieldsValue }] = useForm({ | |
118 | + schemas: [ | |
119 | + { | |
120 | + field: 'value', | |
121 | + label: '键/值', | |
122 | + component: 'Input', | |
123 | + colProps: { span: 6 }, | |
124 | + componentProps: { | |
125 | + placeholder: '请输入键/值', | |
126 | + }, | |
127 | + }, | |
128 | + ], | |
129 | + labelWidth: 100, | |
130 | + compact: true, | |
131 | + showAdvancedButton: true, | |
132 | + submitFunc: async () => { | |
133 | + try { | |
134 | + const { value } = getFieldsValue() || {}; | |
135 | + | |
136 | + pagination.current = 1; | |
137 | + | |
138 | + socketInfo.filterAttrKeys = value | |
139 | + ? unref(socketInfo.rawDataSource) | |
140 | + .filter( | |
141 | + (item) => | |
142 | + item.key?.toUpperCase().includes(value.toUpperCase()) || | |
143 | + item.name?.toUpperCase().includes(value.toUpperCase()) | |
144 | + ) | |
145 | + .map((item) => item.key) | |
146 | + : socketInfo.rawDataSource.map((item) => item.key); | |
147 | + | |
148 | + await nextTick(); | |
149 | + handleFilterChange(); | |
150 | + unref(mode) === EnumTableCardMode.TABLE && setTableModeData(); | |
151 | + } catch (error) {} | |
152 | + }, | |
153 | + resetFunc: async () => { | |
154 | + try { | |
155 | + socketInfo.filterAttrKeys = []; | |
156 | + handleFilterChange(); | |
157 | + unref(mode) === EnumTableCardMode.TABLE && setTableModeData(); | |
158 | + } catch (error) {} | |
159 | + }, | |
160 | + }); | |
161 | + | |
162 | + const [registerTable, { setTableData }] = useTable({ | |
163 | + columns: realTimeDataColumns, | |
164 | + showTableSetting: true, | |
165 | + pagination: pagination as any, | |
166 | + bordered: true, | |
167 | + resizeHeightOffset: 16, | |
168 | + showIndexColumn: false, | |
169 | + }); | |
170 | + | |
171 | + function handleFilterChange( | |
172 | + page: number = pagination.current || 1, | |
173 | + pageSize: number = pagination.pageSize || 10 | |
174 | + ) { | |
175 | + pagination.current = page; | |
176 | + pagination.pageSize = pageSize; | |
177 | + send(JSON.stringify(createUnsubscribeMessage(socketInfo.cmdId))); | |
178 | + socketInfo.cmdId = socketInfo.cmdId + 1; | |
179 | + send(JSON.stringify(unref(getFilterSendValue))); | |
180 | + } | |
181 | + | |
182 | + const [registerModal, { openModal }] = useModal(); | |
183 | + | |
184 | + const mode = ref<EnumTableCardMode>(EnumTableCardMode.CARD); | |
185 | + | |
186 | + const switchMode = async (value: EnumTableCardMode) => { | |
187 | + mode.value = value; | |
188 | + await nextTick(); | |
189 | + unref(mode) === EnumTableCardMode.TABLE && setTableModeData(); | |
190 | + socketInfo.filterAttrKeys = []; | |
191 | + }; | |
192 | + | |
193 | + const { createMessage } = useMessage(); | |
194 | + | |
195 | + const setDataSource = () => { | |
196 | + socketInfo.dataSource = socketInfo.filterAttrKeys.length | |
197 | + ? socketInfo.rawDataSource.filter((item) => socketInfo.filterAttrKeys.includes(item.key)) | |
198 | + : socketInfo.rawDataSource; | |
199 | + }; | |
200 | + | |
201 | + const { send, close, data, open } = useWebSocket(socketInfo.origin, { | |
202 | + immediate: false, | |
203 | + autoReconnect: true, | |
204 | + async onConnected() { | |
205 | + send(JSON.stringify(unref(getSendValue))); | |
206 | + }, | |
207 | + async onMessage() { | |
208 | + try { | |
209 | + const value = JSON.parse(unref(data)) as ReceiveMessage; | |
210 | + if (value) { | |
211 | + const { data } = value; | |
212 | + const keys = Object.keys(data); | |
213 | + for (const key of keys) { | |
214 | + const item = socketInfo.rawDataSource.find((item) => item.key === key); | |
215 | + if (!item) continue; | |
216 | + const { type } = item; | |
217 | + const [firstItem] = data[key] || []; | |
218 | + const [time, value] = firstItem; | |
219 | + item.rawValue = value; | |
220 | + if (type === DataTypeEnum.STRUCT) { | |
221 | + const { flag, value: structJSON } = useJsonParse(value); | |
222 | + if (!flag || !isObject(item.value)) continue; | |
223 | + const structKeys = Object.keys(structJSON); | |
224 | + structKeys.forEach((key) => { | |
225 | + if ((item.value as Record<string, StructValueItemType>)?.[key]) { | |
226 | + (item.value as Record<string, StructValueItemType>)[key].value = structJSON[key]; | |
227 | + } | |
228 | + }); | |
229 | + item.time = time; | |
230 | + continue; | |
231 | + } | |
232 | + item.value = value; | |
233 | + item.time = time; | |
234 | + } | |
235 | + setDataSource(); | |
236 | + await nextTick(); | |
237 | + unref(mode) === EnumTableCardMode.TABLE && setTableModeData(); | |
238 | + } | |
239 | + } catch (error) {} | |
240 | + }, | |
241 | + onDisconnected() { | |
242 | + // close(); | |
243 | + }, | |
244 | + onError() { | |
245 | + createMessage.error('webSocket连接超时,请联系管理员'); | |
246 | + }, | |
247 | + }); | |
248 | + | |
249 | + function setTableModeData() { | |
250 | + setTableData(socketInfo.dataSource.filter((item) => !isNull(item.value))); | |
251 | + } | |
252 | + | |
253 | + const handleShowDetail = (record: SocketInfoDataSourceItemType) => { | |
254 | + const { key } = record; | |
255 | + socketInfo.attr = key; | |
256 | + openModal(true); | |
257 | + }; | |
258 | + | |
259 | + const getIsModbusDevice = computed( | |
260 | + () => | |
261 | + props.deviceDetail.deviceData.transportConfiguration.type === TransportTypeEnum.TCP && | |
262 | + props.deviceDetail?.deviceProfile?.profileData?.transportConfiguration?.protocol === | |
263 | + TCPProtocolTypeEnum.MODBUS_RTU | |
264 | + ); | |
265 | + | |
266 | + onMounted(async () => { | |
267 | + const { deviceProfileId } = props.deviceDetail; | |
268 | + const { id } = deviceProfileId; | |
269 | + const value = await getDeviceAttrs({ deviceProfileId: id }); | |
270 | + socketInfo.attrKeys = isArray(value) ? value.map((item) => item.identifier) : []; | |
271 | + socketInfo.rawDataSource = buildTableDataSourceByObjectModel( | |
272 | + value as never as DeviceModelOfMatterAttrs[], | |
273 | + props.deviceDetail | |
274 | + ); | |
275 | + setDataSource(); | |
276 | + open(); | |
277 | + }); | |
278 | + | |
279 | + const formatValue = (item: SocketInfoDataSourceItemType) => { | |
280 | + if (isNullOrUnDef(item) || isNullOrUnDef(item.value)) return '--'; | |
281 | + if (unref(getIsModbusDevice) && item.type === DataTypeEnum.BOOL) { | |
282 | + const _result = Reflect.get(item.enum || {}, item.value as string); | |
283 | + return isNullOrUnDef(_result) ? item.value : _result; | |
284 | + } | |
285 | + switch (item.type) { | |
286 | + case DataTypeEnum.BOOL: | |
287 | + return !!Number(item.value) ? item.boolOpen : item.boolClose; | |
288 | + case DataTypeEnum.ENUM: | |
289 | + return item.enum?.[item.value as string]; | |
290 | + default: | |
291 | + return item.value || '--'; | |
292 | + } | |
293 | + }; | |
294 | + | |
295 | + const [register, { openModal: openSendCommandModal }] = useModal(); | |
296 | + const handleSendCommandModal = (data: SocketInfoDataSourceItemType) => { | |
297 | + openSendCommandModal(true, { | |
298 | + mode: DataActionModeEnum.READ, | |
299 | + record: { | |
300 | + objectModel: toRaw(unref(data.detail)), | |
301 | + deviceDetail: props.deviceDetail as EdgeDeviceItemType, | |
302 | + }, | |
303 | + } as ModalParamsType); | |
304 | + }; | |
305 | + | |
306 | + onMounted(() => { | |
307 | + const element = unref(listElRef)?.$el; | |
308 | + if (!element) return; | |
309 | + const totalHeight = document.documentElement.clientHeight; | |
310 | + const { top } = element?.getBoundingClientRect() || {}; | |
311 | + const containerEl = element.querySelector('.ant-spin-container') as HTMLDivElement; | |
312 | + containerEl.style.height = `${totalHeight - top - 20 - 50}px`; | |
313 | + containerEl.style.overflowY = 'auto'; | |
314 | + containerEl.style.overflowX = 'hidden'; | |
315 | + }); | |
316 | + | |
317 | + onUnmounted(() => close()); | |
318 | +</script> | |
319 | + | |
320 | +<template> | |
321 | + <PageWrapper | |
322 | + dense | |
323 | + content-class="flex flex-col p-4 dark:text-gray-300 dark:bg-dark-700 bg-gray-100" | |
324 | + > | |
325 | + <section | |
326 | + class="flex flex-col justify-between w-full bg-light-50 pt-3 mb-4 dark:text-gray-300 dark:bg-dark-900" | |
327 | + > | |
328 | + <div class="flex-auto"> | |
329 | + <BasicForm @register="registerForm" /> | |
330 | + </div> | |
331 | + </section> | |
332 | + <section class="bg-light-50 !dark:text-gray-300 !dark:bg-dark-900"> | |
333 | + <div | |
334 | + v-show="mode === EnumTableCardMode.CARD" | |
335 | + class="flex h-70px items-center justify-end p-2" | |
336 | + > | |
337 | + <ModeSwitchButton v-model:value="mode" @change="switchMode" /> | |
338 | + </div> | |
339 | + <List | |
340 | + v-show="mode === EnumTableCardMode.CARD" | |
341 | + ref="listElRef" | |
342 | + class="list-mode !px-2" | |
343 | + :data-source="socketInfo.dataSource" | |
344 | + :grid="grid" | |
345 | + :pagination="pagination" | |
346 | + > | |
347 | + <template #renderItem="{ item }: { item: SocketInfoDataSourceItemType }"> | |
348 | + <List.Item> | |
349 | + <Card class="shadow-md"> | |
350 | + <template #title> | |
351 | + <span class="text-base font-normal mr-2">{{ item.name }}</span> | |
352 | + </template> | |
353 | + <template #extra> | |
354 | + <Space> | |
355 | + <Tooltip | |
356 | + :title="item.expand ? '收起' : '展开'" | |
357 | + v-if="item.type === DataTypeEnum.STRUCT" | |
358 | + > | |
359 | + <Icon | |
360 | + :icon="item.expand ? 'ant-design:up-outlined' : 'ant-design:down-outlined'" | |
361 | + class="cursor-pointer svg:text-blue-500" | |
362 | + @click="item.expand = !item.expand" | |
363 | + /> | |
364 | + </Tooltip> | |
365 | + <Tooltip title="属性下发"> | |
366 | + <SvgIcon | |
367 | + name="send-command" | |
368 | + prefix="iconfont" | |
369 | + v-if="ReadAndWriteEnum.READ_AND_WRITE === item.accessMode" | |
370 | + class="cursor-pointer text-lg text-blue-500" | |
371 | + @click="handleSendCommandModal(item)" | |
372 | + /> | |
373 | + </Tooltip> | |
374 | + | |
375 | + <Tooltip title="历史趋势"> | |
376 | + <AreaChartOutlined | |
377 | + class="cursor-pointer text-lg svg:fill-blue-500" | |
378 | + @click="handleShowDetail(item)" | |
379 | + /> | |
380 | + </Tooltip> | |
381 | + </Space> | |
382 | + </template> | |
383 | + <section class="min-h-16 flex flex-col justify-between"> | |
384 | + <div | |
385 | + v-if="item.type !== DataTypeEnum.STRUCT" | |
386 | + class="flex font-bold text-lg mb-4 gap-2" | |
387 | + > | |
388 | + <Tooltip :title="formatValue(item) as any" placement="topLeft"> | |
389 | + <div class="truncate">{{ formatValue(item) }}</div> | |
390 | + </Tooltip> | |
391 | + <div class="text-xs flex items-center">{{ item.unitName }}</div> | |
392 | + </div> | |
393 | + <div | |
394 | + class="mb-4 overflow-hidden flex flex-col gap-2 relative" | |
395 | + v-if="item.type == DataTypeEnum.STRUCT && isObject(item.value)" | |
396 | + :style="{ height: item.expand ? 'fit-content' : '28px' }" | |
397 | + > | |
398 | + <div | |
399 | + v-for="key in Object.keys(item.value)" | |
400 | + :key="key" | |
401 | + class="cursor-pointer flex justify-between items-center gap-2" | |
402 | + > | |
403 | + <div class="font-medium whitespace-nowrap">{{ item.value[key].name }}</div> | |
404 | + <div class="flex-auto font-bold text-lg text-left ml-4 truncate"> | |
405 | + <Tooltip | |
406 | + :title="formatValue(item.value[key] as SocketInfoDataSourceItemType) as any" | |
407 | + placement="topLeft" | |
408 | + > | |
409 | + <span> | |
410 | + {{ formatValue(item.value[key] as SocketInfoDataSourceItemType) }} | |
411 | + </span> | |
412 | + </Tooltip> | |
413 | + <span class="text-xs font-normal ml-2"> | |
414 | + {{ item.value[key].unitName }} | |
415 | + </span> | |
416 | + </div> | |
417 | + </div> | |
418 | + <Tooltip title="点击展开"> | |
419 | + <div | |
420 | + v-show="!item.expand" | |
421 | + class="absolute top-2 right-0 text-blue-400 cursor-pointer text-xs" | |
422 | + @click="item.expand = !item.expand" | |
423 | + > | |
424 | + 更多 | |
425 | + </div> | |
426 | + </Tooltip> | |
427 | + </div> | |
428 | + <div class="text-dark-800 text-xs"> | |
429 | + {{ | |
430 | + item.time && item?.rawValue | |
431 | + ? formatToDateTime(item.time, 'YYYY-MM-DD HH:mm:ss') | |
432 | + : '--' | |
433 | + }} | |
434 | + </div> | |
435 | + </section> | |
436 | + </Card> | |
437 | + </List.Item> | |
438 | + </template> | |
439 | + </List> | |
440 | + </section> | |
441 | + <BasicTable | |
442 | + v-if="mode === EnumTableCardMode.TABLE" | |
443 | + @register="registerTable" | |
444 | + class="device-things-model-table-mode" | |
445 | + > | |
446 | + <template #toolbar> | |
447 | + <div | |
448 | + v-show="mode === EnumTableCardMode.TABLE" | |
449 | + class="flex h-70px items-center justify-end p-2" | |
450 | + > | |
451 | + <ModeSwitchButton v-model:value="mode" @change="switchMode" /> | |
452 | + </div> | |
453 | + </template> | |
454 | + </BasicTable> | |
455 | + <BasicModal | |
456 | + @register="registerModal" | |
457 | + title="历史数据" | |
458 | + width="50%" | |
459 | + destroy-on-close | |
460 | + dialogClass="history-modal" | |
461 | + > | |
462 | + <HistoryData :deviceDetail="props.deviceDetail" :attr="socketInfo.attr" /> | |
463 | + </BasicModal> | |
464 | + <ObjectModelCommandDeliveryModal | |
465 | + @register="register" | |
466 | + :deviceId="deviceDetail.id.id" | |
467 | + :device-name="deviceDetail.name" | |
468 | + /> | |
469 | + </PageWrapper> | |
470 | +</template> | |
471 | + | |
472 | +<style scoped lang="less"> | |
473 | + .list-mode:deep(.ant-card-head) { | |
474 | + border-bottom: 0; | |
475 | + } | |
476 | + | |
477 | + .list-mode:deep(.ant-card-body) { | |
478 | + padding-top: 0; | |
479 | + } | |
480 | + | |
481 | + .list-mode:deep(.ant-card-head-title) { | |
482 | + height: 64px; | |
483 | + display: flex; | |
484 | + align-items: center; | |
485 | + } | |
486 | + | |
487 | + .device-things-model-table-mode:deep(.ant-table-placeholder) { | |
488 | + height: auto; | |
489 | + } | |
490 | + | |
491 | + .model-collapse { | |
492 | + border: none; | |
493 | + | |
494 | + :deep(.ant-collapse-header) { | |
495 | + display: none; | |
496 | + } | |
497 | + | |
498 | + :deep(.ant-collapse-item) { | |
499 | + border: none; | |
500 | + } | |
501 | + | |
502 | + :deep(.ant-collapse-content) { | |
503 | + border: none; | |
504 | + } | |
505 | + | |
506 | + :deep(.ant-collapse-content-box) { | |
507 | + padding: 0; | |
508 | + } | |
509 | + } | |
510 | +</style> | |
511 | + | |
512 | +<style> | |
513 | + .history-modal .ant-input-number { | |
514 | + min-width: 0 !important; | |
515 | + width: 100% !important; | |
516 | + } | |
517 | +</style> | ... | ... |
1 | +export interface DeviceType { | |
2 | + alarmStatus: number; | |
3 | + createTime: string; | |
4 | + creator: string; | |
5 | + deviceInfo: Object; | |
6 | + deviceProfile: Object; | |
7 | + deviceState: string; | |
8 | + deviceToken: string; | |
9 | + deviceType: string; | |
10 | + enabled: boolean; | |
11 | + id: string; | |
12 | + key: string; | |
13 | + label: string; | |
14 | + name: string; | |
15 | + organizationDTO: Object; | |
16 | + organizationId: string; | |
17 | + profileId: string; | |
18 | + tenantCode: string; | |
19 | + updateTime: string; | |
20 | +} | |
21 | + | |
22 | +export interface socketDataType { | |
23 | + key: string; | |
24 | + value: string; | |
25 | + time: number; | |
26 | +} | ... | ... |
1 | +import { DescItem } from '/@/components/Description/src/typing'; | |
2 | + | |
3 | +export const descSchema = (): DescItem[] => { | |
4 | + return [ | |
5 | + { | |
6 | + field: 'name', | |
7 | + label: '设备名称', | |
8 | + }, | |
9 | + { | |
10 | + field: 'label', | |
11 | + label: '设备标签', | |
12 | + }, | |
13 | + { | |
14 | + field: 'routingKey', | |
15 | + label: '边缘键', | |
16 | + }, | |
17 | + { | |
18 | + field: 'secret', | |
19 | + label: '边缘密钥', | |
20 | + }, | |
21 | + { | |
22 | + field: 'createdTime', | |
23 | + label: '创建时间', | |
24 | + }, | |
25 | + { | |
26 | + field: 'additionalInfo.description', | |
27 | + label: '描述', | |
28 | + }, | |
29 | + ]; | |
30 | +}; | ... | ... |
1 | +export { default as EdgeDeviceTabInfo } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { Tabs } from 'ant-design-vue'; | |
3 | + import { ref } from 'vue'; | |
4 | + import type { PropType } from 'vue'; | |
5 | + import { EdgeDevicePhysicalModel } from '../EdgeDevicePhysicalModel'; | |
6 | + import { EdgeDeviceAlarm } from '../EdgeDeviceAlarm'; | |
7 | + import { EdgeDeviceEvent } from '../EdgeDeviceEvent'; | |
8 | + import { EdgeDeviceCommand } from '../EdgeDeviceCommand'; | |
9 | + import { EdgeDeviceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
10 | + | |
11 | + defineProps({ | |
12 | + recordData: { | |
13 | + type: Object as PropType<EdgeDeviceItemType>, | |
14 | + default: () => {}, | |
15 | + }, | |
16 | + }); | |
17 | + | |
18 | + enum ActiveKey { | |
19 | + PHYSICALMODEL = 'PhysicalModel', | |
20 | + ALARM = 'Alarm', | |
21 | + COMMANDISSUANCERECORD = 'CommandIssuanceRecord', | |
22 | + EVENTMANAGEMENT = 'EventManagement', | |
23 | + } | |
24 | + | |
25 | + const activeKey = ref(ActiveKey.PHYSICALMODEL); | |
26 | +</script> | |
27 | + | |
28 | +<template> | |
29 | + <section class="w-full h-full p-1"> | |
30 | + <main> | |
31 | + <Tabs | |
32 | + v-model:activeKey="activeKey" | |
33 | + type="card" | |
34 | + class="w-full h-full bg-light-50 !p-4 dark:bg-dark-900" | |
35 | + > | |
36 | + <Tabs.TabPane tab="物模型" :key="ActiveKey.PHYSICALMODEL"> | |
37 | + <EdgeDevicePhysicalModel :deviceDetail="recordData" /> | |
38 | + </Tabs.TabPane> | |
39 | + <Tabs.TabPane tab="告警" :key="ActiveKey.ALARM"> | |
40 | + <EdgeDeviceAlarm :recordData="recordData" /> | |
41 | + </Tabs.TabPane> | |
42 | + <Tabs.TabPane tab="命令下发" :key="ActiveKey.COMMANDISSUANCERECORD"> | |
43 | + <EdgeDeviceCommand :recordData="recordData" /> | |
44 | + </Tabs.TabPane> | |
45 | + <Tabs.TabPane tab="事件管理" :key="ActiveKey.EVENTMANAGEMENT"> | |
46 | + <EdgeDeviceEvent :recordData="recordData" /> | |
47 | + </Tabs.TabPane> | |
48 | + </Tabs> | |
49 | + </main> | |
50 | + </section> | |
51 | +</template> | |
52 | + | |
53 | +<style lang="less" scoped></style> | ... | ... |
1 | +import { Tag } from 'ant-design-vue'; | |
2 | +import { BasicColumn } from '/@/components/Table'; | |
3 | +import { formatToDateTime } from '/@/utils/dateUtil'; | |
4 | +import { h } from 'vue'; | |
5 | + | |
6 | +// 表格配置 | |
7 | +export const columns: BasicColumn[] = [ | |
8 | + { | |
9 | + title: '事件时间', | |
10 | + dataIndex: 'createdTime', | |
11 | + format(text) { | |
12 | + return formatToDateTime(text, 'YYYY-MM-DD HH:mm:ss'); | |
13 | + }, | |
14 | + }, | |
15 | + { | |
16 | + title: '服务器', | |
17 | + dataIndex: 'body.server', | |
18 | + }, | |
19 | + { | |
20 | + title: '事件', | |
21 | + dataIndex: 'body.event', | |
22 | + }, | |
23 | + { | |
24 | + title: '状态', | |
25 | + dataIndex: 'body.success', | |
26 | + customRender: ({ record }) => { | |
27 | + const color = record.body.success ? 'success' : 'error'; | |
28 | + const text = record.body.success ? '成功' : '失败'; | |
29 | + return h(Tag, { color: color }, () => text); | |
30 | + }, | |
31 | + }, | |
32 | + { | |
33 | + title: '错误', | |
34 | + dataIndex: 'error', | |
35 | + slots: { customRender: 'errorDetail' }, | |
36 | + }, | |
37 | +]; | ... | ... |
1 | +export { default as EdgeEvents } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { BasicTable, useTable } from '/@/components/Table'; | |
3 | + import { columns } from './config'; | |
4 | + import { edgeEventPage } from '/@/api/edgeManage/edgeInstance'; | |
5 | + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
6 | + import { BasicModal, useModal } from '/@/components/Modal'; | |
7 | + import { Input, Button } from 'ant-design-vue'; | |
8 | + import { ref } from 'vue'; | |
9 | + | |
10 | + const props = defineProps({ | |
11 | + recordData: { | |
12 | + type: Object as PropType<EdgeInstanceItemType>, | |
13 | + default: () => {}, | |
14 | + }, | |
15 | + }); | |
16 | + | |
17 | + const outputData = ref<string>(); | |
18 | + | |
19 | + const [registerTable] = useTable({ | |
20 | + columns, | |
21 | + api: async ({ page, pageSize }) => { | |
22 | + const res = await edgeEventPage( | |
23 | + { | |
24 | + page: page - 1 < 0 ? 0 : page - 1, | |
25 | + pageSize, | |
26 | + tenantId: props?.recordData?.tenantId?.id, | |
27 | + sortOrder: 'DESC', | |
28 | + sortProperty: 'ts', | |
29 | + }, | |
30 | + props?.recordData?.id?.id | |
31 | + ); | |
32 | + return { | |
33 | + total: res?.totalElements, | |
34 | + items: res?.data, | |
35 | + }; | |
36 | + }, | |
37 | + showIndexColumn: false, | |
38 | + clickToRowSelect: false, | |
39 | + showTableSetting: true, | |
40 | + bordered: true, | |
41 | + }); | |
42 | + | |
43 | + const [registerModal, { openModal, closeModal }] = useModal(); | |
44 | + | |
45 | + const handleEventIsDetail = (record: Recordable) => { | |
46 | + outputData.value = record?.body?.error; | |
47 | + openModal(true); | |
48 | + }; | |
49 | +</script> | |
50 | + | |
51 | +<template> | |
52 | + <div> | |
53 | + <BasicTable :clickToRowSelect="false" @register="registerTable"> | |
54 | + <template #errorDetail="{ record }"> | |
55 | + <Button | |
56 | + v-if="record?.body?.error" | |
57 | + type="link" | |
58 | + class="ml-2" | |
59 | + @click="handleEventIsDetail(record)" | |
60 | + > | |
61 | + 查看 | |
62 | + </Button> | |
63 | + <Button v-else type="link" class="ml-2"> - </Button> | |
64 | + </template> | |
65 | + </BasicTable> | |
66 | + <BasicModal title="详情" @register="registerModal" @ok="closeModal"> | |
67 | + <Input.TextArea v-model:value="outputData" :autosize="true" /> | |
68 | + </BasicModal> | |
69 | + </div> | |
70 | +</template> | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { nextTick, ref, unref, computed } from 'vue'; | |
3 | + import { BasicForm, useForm } from '/@/components/Form'; | |
4 | + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer'; | |
5 | + import { FormFieldsEnum, formSchema } from './config'; | |
6 | + import { HandleOperationEnum } from '../../config'; | |
7 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
8 | + import { createOrEditEdgeInstance } from '/@/api/edgeManage/edgeInstance'; | |
9 | + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
10 | + import { buildUUID, randomString } from '/@/utils/uuid'; | |
11 | + import { handeleCopy } from '/@/views/device/profiles/step/topic'; | |
12 | + | |
13 | + const emits = defineEmits(['success', 'register']); | |
14 | + | |
15 | + const { createMessage } = useMessage(); | |
16 | + | |
17 | + const isUpdate = ref<Boolean>(); | |
18 | + | |
19 | + const isUpdateText = ref<String>(); | |
20 | + | |
21 | + const recordData = ref<EdgeInstanceItemType>(); | |
22 | + | |
23 | + const [registerForm, { resetFields, validate, setFieldsValue }] = useForm({ | |
24 | + labelWidth: 120, | |
25 | + schemas: formSchema, | |
26 | + showActionButtonGroup: false, | |
27 | + }); | |
28 | + | |
29 | + const cacheTitle = computed(() => (!unref(isUpdate) ? '新增实例' : '编辑实例')); | |
30 | + | |
31 | + const handleCopyRoutingKey = (text: string) => handeleCopy(text); | |
32 | + | |
33 | + const handleCopySecret = (text: string) => handeleCopy(text); | |
34 | + | |
35 | + const setDefaultValue = (event: HandleOperationEnum) => { | |
36 | + if (event === HandleOperationEnum.CREATE) { | |
37 | + setFieldsValue({ | |
38 | + [FormFieldsEnum.ROUTINGKEY]: buildUUID(), | |
39 | + [FormFieldsEnum.SECRET]: randomString(), | |
40 | + }); | |
41 | + } | |
42 | + }; | |
43 | + | |
44 | + const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => { | |
45 | + setDrawerProps({ loading: true }); | |
46 | + await resetFields(); | |
47 | + setDefaultValue(data.event); | |
48 | + isUpdate.value = data.isUpdate; | |
49 | + isUpdateText.value = data.isUpdateText; | |
50 | + recordData.value = data.record; | |
51 | + try { | |
52 | + await nextTick(); | |
53 | + setFieldsValue(data.record); | |
54 | + setFieldsValue({ | |
55 | + [FormFieldsEnum.DESCRIPTION]: data.record?.additionalInfo[FormFieldsEnum.DESCRIPTION], | |
56 | + }); | |
57 | + } finally { | |
58 | + setDrawerProps({ loading: false }); | |
59 | + } | |
60 | + }); | |
61 | + | |
62 | + const getValue = async () => { | |
63 | + const values = await validate(); | |
64 | + if (!values) return; | |
65 | + const additionalInfo = { | |
66 | + description: values[FormFieldsEnum.DESCRIPTION], | |
67 | + }; | |
68 | + const mergeValues = { | |
69 | + ...recordData.value, | |
70 | + ...values, | |
71 | + ...{ additionalInfo }, | |
72 | + }; | |
73 | + Reflect.deleteProperty(mergeValues, FormFieldsEnum.DESCRIPTION); | |
74 | + await createOrEditEdgeInstance(mergeValues); | |
75 | + createMessage.success(`${isUpdateText.value}成功`); | |
76 | + closeDrawer(); | |
77 | + setTimeout(() => { | |
78 | + emits('success'); | |
79 | + }, 500); | |
80 | + }; | |
81 | + | |
82 | + const handleSubmit = () => getValue(); | |
83 | +</script> | |
84 | + | |
85 | +<template> | |
86 | + <div> | |
87 | + <BasicDrawer | |
88 | + destroyOnClose | |
89 | + v-bind="$attrs" | |
90 | + showFooter | |
91 | + :title="cacheTitle" | |
92 | + width="30%" | |
93 | + :maskClosable="true" | |
94 | + @register="registerDrawer" | |
95 | + @ok="handleSubmit" | |
96 | + > | |
97 | + <BasicForm @register="registerForm"> | |
98 | + <template #routingKey="{ model, field }"> | |
99 | + <div class="!flex justify-between items-center gap-5"> | |
100 | + <a-input disabled v-model:value="model[field]" /> | |
101 | + <a-button type="link" @click="handleCopyRoutingKey(model[field])" class="cursor-pointer" | |
102 | + >复制</a-button | |
103 | + > | |
104 | + </div> | |
105 | + </template> | |
106 | + <template #secret="{ model, field }"> | |
107 | + <div class="!flex justify-between items-center gap-5"> | |
108 | + <a-input disabled v-model:value="model[field]" /> | |
109 | + <a-button type="link" @click="handleCopySecret(model[field])" class="cursor-pointer" | |
110 | + >复制</a-button | |
111 | + > | |
112 | + </div> | |
113 | + </template> | |
114 | + </BasicForm> | |
115 | + </BasicDrawer> | |
116 | + </div> | |
117 | +</template> | |
118 | + | |
119 | +<style lang="less" scoped></style> | ... | ... |
1 | +import { FormSchema } from '/@/components/Form'; | |
2 | + | |
3 | +export enum FormFieldsEnum { | |
4 | + NAME = 'name', | |
5 | + LABEL = 'label', | |
6 | + ROUTINGKEY = 'routingKey', | |
7 | + SECRET = 'secret', | |
8 | + TYPE = 'type', | |
9 | + DESCRIPTION = 'description', | |
10 | + TEXTSEARCH = 'textSearch', | |
11 | +} | |
12 | + | |
13 | +enum FormFieldsNameEnum { | |
14 | + NAME = '名称', | |
15 | + LABEL = '标签', | |
16 | + ROUTINGKEY = '边缘Key', | |
17 | + SECRET = '边缘密钥', | |
18 | + TYPE = '边缘类型', | |
19 | + DESCRIPTION = '描述', | |
20 | + TEXTSEARCH = '实例名称', | |
21 | +} | |
22 | + | |
23 | +export const formSchema: FormSchema[] = [ | |
24 | + { | |
25 | + field: FormFieldsEnum.NAME, | |
26 | + label: FormFieldsNameEnum.NAME, | |
27 | + component: 'Input', | |
28 | + required: true, | |
29 | + componentProps: { | |
30 | + maxLength: 255, | |
31 | + placeholder: '请输入实例名称', | |
32 | + }, | |
33 | + }, | |
34 | + { | |
35 | + field: FormFieldsEnum.TYPE, | |
36 | + label: FormFieldsNameEnum.TYPE, | |
37 | + component: 'Input', | |
38 | + required: true, | |
39 | + defaultValue: 'default', | |
40 | + componentProps: { | |
41 | + disabled: true, | |
42 | + }, | |
43 | + }, | |
44 | + { | |
45 | + field: FormFieldsEnum.ROUTINGKEY, | |
46 | + label: FormFieldsNameEnum.ROUTINGKEY, | |
47 | + slot: 'routingKey', | |
48 | + required: true, | |
49 | + component: 'Input', | |
50 | + }, | |
51 | + { | |
52 | + field: FormFieldsEnum.SECRET, | |
53 | + label: FormFieldsNameEnum.SECRET, | |
54 | + slot: 'secret', | |
55 | + required: true, | |
56 | + component: 'Input', | |
57 | + }, | |
58 | + { | |
59 | + field: FormFieldsEnum.LABEL, | |
60 | + label: FormFieldsNameEnum.LABEL, | |
61 | + component: 'Input', | |
62 | + componentProps: { | |
63 | + maxLength: 255, | |
64 | + placeholder: '请输入标签', | |
65 | + }, | |
66 | + }, | |
67 | + { | |
68 | + field: FormFieldsEnum.DESCRIPTION, | |
69 | + label: FormFieldsNameEnum.DESCRIPTION, | |
70 | + component: 'InputTextArea', | |
71 | + componentProps: { | |
72 | + maxLength: 255, | |
73 | + placeholder: '请输入描述', | |
74 | + }, | |
75 | + }, | |
76 | +]; | |
77 | + | |
78 | +export const searchFormSchema: FormSchema[] = [ | |
79 | + { | |
80 | + field: FormFieldsEnum.TEXTSEARCH, | |
81 | + label: FormFieldsNameEnum.TEXTSEARCH, | |
82 | + component: 'Input', | |
83 | + colProps: { span: 6 }, | |
84 | + componentProps: { | |
85 | + maxLength: 255, | |
86 | + placeholder: '请输入实例名称', | |
87 | + }, | |
88 | + }, | |
89 | +]; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ref } from 'vue'; | |
3 | + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
4 | + import { infoEdgeInstance } from '/@/api/edgeManage/edgeInstance'; | |
5 | + import { EdgeInstanceBasicInfo } from '../EdgeInstanceBasicInfo'; | |
6 | + import { EdgeInstanceTabInfo } from '../EdgeInstanceTabInfo'; | |
7 | + import { useRoute } from 'vue-router'; | |
8 | + import { PageWrapper } from '/@/components/Page'; | |
9 | + import { useGo } from '/@/hooks/web/usePage'; | |
10 | + | |
11 | + defineEmits(['register']); | |
12 | + | |
13 | + defineOptions({ name: 'EdgeDetail' }); | |
14 | + | |
15 | + const route = useRoute(); | |
16 | + | |
17 | + const go = useGo(); | |
18 | + | |
19 | + const edgeId = ref(route.params?.id); | |
20 | + | |
21 | + const recordData = ref<EdgeInstanceItemType>(); | |
22 | + | |
23 | + infoEdgeInstance(edgeId.value as string).then((res) => { | |
24 | + recordData.value = res; | |
25 | + }); | |
26 | + | |
27 | + function goBack() { | |
28 | + go('/edge/instance'); | |
29 | + } | |
30 | +</script> | |
31 | + | |
32 | +<template> | |
33 | + <PageWrapper :title="`边缘详情`" contentBackground @back="goBack"> | |
34 | + <!-- 基础信息 --> | |
35 | + <div class="m-4"> | |
36 | + <EdgeInstanceBasicInfo v-if="recordData" :recordData="recordData" /> | |
37 | + </div> | |
38 | + <!-- Tab信息 --> | |
39 | + <div> | |
40 | + <EdgeInstanceTabInfo v-if="recordData" :recordData="recordData" /> | |
41 | + </div> | |
42 | + </PageWrapper> | |
43 | +</template> | |
44 | + | |
45 | +<style lang="less" scoped></style> | ... | ... |
1 | +import { Tag } from 'ant-design-vue'; | |
2 | +import { h } from 'vue'; | |
3 | +import { DescItem } from '/@/components/Description/src/typing'; | |
4 | +import { handeleCopy } from '/@/views/device/profiles/step/topic'; | |
5 | +import { formatToDateTime } from '/@/utils/dateUtil'; | |
6 | + | |
7 | +export const descSchema = (): DescItem[] => { | |
8 | + return [ | |
9 | + { | |
10 | + field: 'name', | |
11 | + label: '边缘名称', | |
12 | + }, | |
13 | + { | |
14 | + field: 'label', | |
15 | + label: '设备标签', | |
16 | + render: (text) => { | |
17 | + return text | |
18 | + ? h( | |
19 | + Tag, | |
20 | + { | |
21 | + color: '#00B42A', | |
22 | + }, | |
23 | + text | |
24 | + ) | |
25 | + : ''; | |
26 | + }, | |
27 | + }, | |
28 | + { | |
29 | + field: 'active', | |
30 | + label: '状态', | |
31 | + render: (text) => { | |
32 | + const color = text ? 'success' : 'error'; | |
33 | + const textStr = text ? ' 在线' : '离线'; | |
34 | + return h( | |
35 | + Tag, | |
36 | + { | |
37 | + color, | |
38 | + }, | |
39 | + textStr | |
40 | + ); | |
41 | + }, | |
42 | + }, | |
43 | + { | |
44 | + field: 'routingKey', | |
45 | + label: '边缘键', | |
46 | + render: (text) => { | |
47 | + return h( | |
48 | + 'span', | |
49 | + { | |
50 | + style: { cursor: 'pointer', color: '#165DFF' }, | |
51 | + onClick: () => { | |
52 | + handeleCopy(text); | |
53 | + }, | |
54 | + }, | |
55 | + text | |
56 | + ); | |
57 | + }, | |
58 | + }, | |
59 | + { | |
60 | + field: 'secret', | |
61 | + label: '边缘密钥', | |
62 | + render: (text) => { | |
63 | + return h( | |
64 | + 'span', | |
65 | + { | |
66 | + style: { cursor: 'pointer', color: '#165DFF' }, | |
67 | + onClick: () => { | |
68 | + handeleCopy(text); | |
69 | + }, | |
70 | + }, | |
71 | + text | |
72 | + ); | |
73 | + }, | |
74 | + }, | |
75 | + { | |
76 | + field: 'createdTime', | |
77 | + label: '创建时间', | |
78 | + render: (_, data) => { | |
79 | + return formatToDateTime(data.createdTime, 'YYYY-MM-DD HH:mm:ss'); | |
80 | + }, | |
81 | + }, | |
82 | + { | |
83 | + field: 'additionalInfo.description', | |
84 | + label: '描述', | |
85 | + }, | |
86 | + ]; | |
87 | +}; | ... | ... |
1 | +export { default as EdgeInstanceBasicInfo } 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 { ref } from 'vue'; | |
6 | + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
7 | + import edgeInstancePng from '/@/assets/images/edgeInstance.png'; | |
8 | + import { Image } from 'ant-design-vue'; | |
9 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
10 | + import { syncEdge } from '/@/api/edgeManage/edgeInstance'; | |
11 | + // import { useGo } from '/@/hooks/web/usePage'; | |
12 | + | |
13 | + const props = defineProps({ | |
14 | + recordData: { | |
15 | + type: Object as PropType<EdgeInstanceItemType>, | |
16 | + default: () => {}, | |
17 | + }, | |
18 | + }); | |
19 | + | |
20 | + defineEmits(['register']); | |
21 | + | |
22 | + const CS = { | |
23 | + 'max-width': '600px', | |
24 | + 'word-break': 'break-all', | |
25 | + overflow: 'hidden', | |
26 | + display: '-webkit-box', | |
27 | + '-webkit-line-clamp': 2, | |
28 | + '-webkit-box-orient': 'vertical', | |
29 | + }; | |
30 | + | |
31 | + const { createMessage } = useMessage(); | |
32 | + | |
33 | + // const go = useGo(); | |
34 | + | |
35 | + const loadStatus = ref(false); | |
36 | + | |
37 | + const [register] = useDescription({ | |
38 | + layout: 'vertical', | |
39 | + schema: descSchema(), | |
40 | + column: 5, | |
41 | + }); | |
42 | + | |
43 | + const handleEventIsSyncEdge = async () => { | |
44 | + loadStatus.value = true; | |
45 | + try { | |
46 | + if (!props.recordData?.id?.id) return; | |
47 | + await syncEdge(props.recordData?.id?.id); | |
48 | + createMessage.success('同步边缘处理成功'); | |
49 | + } finally { | |
50 | + loadStatus.value = false; | |
51 | + } | |
52 | + }; | |
53 | + | |
54 | + // const handleEventIsOpenEdgeDevice = () => { | |
55 | + // go('/edge/edge_device/' + props.recordData?.id?.id); | |
56 | + // }; | |
57 | +</script> | |
58 | + | |
59 | +<template> | |
60 | + <a-row :gutter="{ xs: 8, sm: 16, md: 24, lg: 32 }"> | |
61 | + <a-col class="gutter-row" :span="3"> | |
62 | + <div class="!flex flex-col justify-between items-center"> | |
63 | + <div><Image :src="edgeInstancePng" :width="180" /></div> | |
64 | + <div class="!flex flex-col mt-3 justify-between"> | |
65 | + <span style="color: #1d2129" class="truncate font-bold">{{ recordData?.name }}</span> | |
66 | + <span style="color: #3d3d3d">边缘详情</span> | |
67 | + </div> | |
68 | + </div> | |
69 | + </a-col> | |
70 | + <a-col class="gutter-row" :span="21"> | |
71 | + <div class="!flex flex-col justify-between"> | |
72 | + <Description v-if="recordData" @register="register" :data="recordData" :contentStyle="CS" /> | |
73 | + <div class="!flex mt-3"> | |
74 | + <a-button | |
75 | + :disabled="!recordData.active" | |
76 | + :loading="loadStatus" | |
77 | + type="primary" | |
78 | + @click="handleEventIsSyncEdge" | |
79 | + >同步边缘</a-button | |
80 | + > | |
81 | + <!-- <a-button class="ml-4" type="primary" @click="handleEventIsOpenEdgeDevice" | |
82 | + >边缘设备</a-button | |
83 | + > --> | |
84 | + </div> | |
85 | + </div> | |
86 | + </a-col> | |
87 | + </a-row> | |
88 | +</template> | |
89 | + | |
90 | +<style lang="less" scoped> | |
91 | + :deep(.ant-image-img) { | |
92 | + height: 157px; | |
93 | + } | |
94 | +</style> | ... | ... |
1 | +import { DescItem } from '/@/components/Description/src/typing'; | |
2 | + | |
3 | +export const descSchema = (): DescItem[] => { | |
4 | + return [ | |
5 | + { | |
6 | + field: 'name', | |
7 | + label: '设备名称', | |
8 | + }, | |
9 | + { | |
10 | + field: 'label', | |
11 | + label: '设备标签', | |
12 | + }, | |
13 | + { | |
14 | + field: 'routingKey', | |
15 | + label: '边缘键', | |
16 | + }, | |
17 | + { | |
18 | + field: 'secret', | |
19 | + label: '边缘密钥', | |
20 | + }, | |
21 | + { | |
22 | + field: 'createdTime', | |
23 | + label: '创建时间', | |
24 | + }, | |
25 | + { | |
26 | + field: 'additionalInfo.description', | |
27 | + label: '描述', | |
28 | + }, | |
29 | + ]; | |
30 | +}; | ... | ... |
1 | +export { default as EdgeInstanceTabInfo } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { Tabs } from 'ant-design-vue'; | |
3 | + import { ref } from 'vue'; | |
4 | + import { EdgeMonitoring } from '../EdgeMonitoring'; | |
5 | + import { EdgeEvents } from '../EdgeEvents'; | |
6 | + import { EdgeDevice } from '../EdgeDevice'; | |
7 | + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
8 | + import type { PropType } from 'vue'; | |
9 | + | |
10 | + defineProps({ | |
11 | + recordData: { | |
12 | + type: Object as PropType<EdgeInstanceItemType>, | |
13 | + default: () => {}, | |
14 | + }, | |
15 | + }); | |
16 | + | |
17 | + enum ActiveKey { | |
18 | + EDGE_DEVICE = 'EdgeDevice', | |
19 | + EDGEMONITORING = 'EdgeMonitoring', | |
20 | + EDGEEVENTS = 'EdgeEvents', | |
21 | + } | |
22 | + | |
23 | + const activeKey = ref(ActiveKey.EDGE_DEVICE); | |
24 | +</script> | |
25 | + | |
26 | +<template> | |
27 | + <section class="w-full h-full p-1"> | |
28 | + <main> | |
29 | + <Tabs | |
30 | + v-model:activeKey="activeKey" | |
31 | + type="card" | |
32 | + class="w-full h-full bg-light-50 !p-4 dark:bg-dark-900" | |
33 | + > | |
34 | + <Tabs.TabPane tab="边缘设备" :key="ActiveKey.EDGE_DEVICE"> | |
35 | + <EdgeDevice | |
36 | + v-if="recordData" | |
37 | + :recordData="recordData" | |
38 | + class="p-4 bg-neutral-100 dark:bg-dark-700" | |
39 | + /> | |
40 | + </Tabs.TabPane> | |
41 | + <Tabs.TabPane tab="边缘监控" :key="ActiveKey.EDGEMONITORING"> | |
42 | + <EdgeMonitoring | |
43 | + v-if="recordData" | |
44 | + :recordData="recordData" | |
45 | + class="p-4 bg-fff dark:bg-dark-700" | |
46 | + /> | |
47 | + </Tabs.TabPane> | |
48 | + <Tabs.TabPane tab="边缘事件" :key="ActiveKey.EDGEEVENTS"> | |
49 | + <EdgeEvents :recordData="recordData" class="p-4 bg-neutral-100 dark:bg-dark-700" /> | |
50 | + </Tabs.TabPane> | |
51 | + </Tabs> | |
52 | + </main> | |
53 | + </section> | |
54 | +</template> | |
55 | + | |
56 | +<style lang="less" scoped></style> | ... | ... |
1 | +export { default as EdgeMonitoring } from './index.vue'; | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { | |
3 | + ref, | |
4 | + onMounted, | |
5 | + unref, | |
6 | + shallowReactive, | |
7 | + nextTick, | |
8 | + reactive, | |
9 | + computed, | |
10 | + onUnmounted, | |
11 | + } from 'vue'; | |
12 | + import * as echarts from 'echarts'; | |
13 | + import { Empty } from 'ant-design-vue'; | |
14 | + import { useMessage } from '/@/hooks/web/useMessage'; | |
15 | + import { useWebSocket } from '@vueuse/core'; | |
16 | + import { getAuthCache } from '/@/utils/auth'; | |
17 | + import { JWT_TOKEN_KEY } from '/@/enums/cacheEnum'; | |
18 | + import { useGlobSetting } from '/@/hooks/setting'; | |
19 | + import moment from 'moment'; | |
20 | + import { EdgeInstanceItemType } from '/@/api/edgeManage/model/edgeInstance'; | |
21 | + import { isArray, isNumber } from '/@/utils/is'; | |
22 | + import { formatSizeUnits } from '/@/utils'; | |
23 | + import cpuSvg from '/@/assets/icons/cpu.svg'; | |
24 | + import diskSvg from '/@/assets/icons/disk.svg'; | |
25 | + import memorySvg from '/@/assets/icons/memory.svg'; | |
26 | + | |
27 | + interface ChartInstance { | |
28 | + name: string; | |
29 | + currentValue: number; | |
30 | + totalKey: string; | |
31 | + totalCount: Recordable; | |
32 | + type: string; | |
33 | + text: string; | |
34 | + xAxisData: string[]; | |
35 | + seriesData: number[]; | |
36 | + icon: any; | |
37 | + } | |
38 | + | |
39 | + enum DeviceInfoOfEdge { | |
40 | + CPU_USAGE_OF_EDGE = 'cpuUsageOfEdge', | |
41 | + DISC_USAGE_OF_EDGE = 'discUsageOfEdge', | |
42 | + MEMORY_USAGE_OF_EDGE = 'memoryUsageOfEdge', | |
43 | + CPU_COUNT_OF_EDGE = 'cpuCountOfEdge', | |
44 | + TOTAL_MEMORY_OF_EDGE = 'totalMemoryOfEdge', | |
45 | + TOTAL_DISC_SPACE_OF_EDGE = 'totalDiscSpaceOfEdge', | |
46 | + } | |
47 | + | |
48 | + const props = defineProps({ | |
49 | + recordData: { | |
50 | + type: Object as PropType<EdgeInstanceItemType>, | |
51 | + default: () => {}, | |
52 | + }, | |
53 | + }); | |
54 | + | |
55 | + const token = getAuthCache(JWT_TOKEN_KEY); | |
56 | + | |
57 | + const { socketUrl } = useGlobSetting(); | |
58 | + | |
59 | + const socketInfo = reactive({ | |
60 | + entityId: '', | |
61 | + origin: `${socketUrl}${token}`, | |
62 | + }); | |
63 | + | |
64 | + const { createMessage } = useMessage(); | |
65 | + | |
66 | + const getSendValue = computed(() => { | |
67 | + return { | |
68 | + tsSubCmds: [ | |
69 | + { | |
70 | + entityId: '', | |
71 | + cmdId: 11, | |
72 | + entityType: 'EDGE', | |
73 | + keys: `${DeviceInfoOfEdge.CPU_USAGE_OF_EDGE},${DeviceInfoOfEdge.DISC_USAGE_OF_EDGE},${DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE},${DeviceInfoOfEdge.CPU_COUNT_OF_EDGE},${DeviceInfoOfEdge.TOTAL_MEMORY_OF_EDGE},${DeviceInfoOfEdge.TOTAL_DISC_SPACE_OF_EDGE}`, | |
74 | + startTs: moment(moment().subtract(1, 'minute')).valueOf(), | |
75 | + timeWindow: 3600000, | |
76 | + interval: 10000, | |
77 | + intervalType: 'MILLISECONDS', | |
78 | + // limit: 360, | |
79 | + timeZoneId: 'Asia/Shanghai', | |
80 | + agg: 'AVG', | |
81 | + unsubscribe: false, | |
82 | + }, | |
83 | + ], | |
84 | + }; | |
85 | + }); | |
86 | + | |
87 | + const chartInstance = ref<ChartInstance[]>([ | |
88 | + { | |
89 | + name: 'CPU', | |
90 | + text: 'CPU使用率', | |
91 | + currentValue: 0, | |
92 | + totalKey: DeviceInfoOfEdge.CPU_COUNT_OF_EDGE, | |
93 | + totalCount: {}, | |
94 | + type: DeviceInfoOfEdge.CPU_USAGE_OF_EDGE, | |
95 | + xAxisData: [], | |
96 | + seriesData: [], | |
97 | + icon: cpuSvg, | |
98 | + }, | |
99 | + { | |
100 | + name: '内存', | |
101 | + text: '内存使用率', | |
102 | + currentValue: 0, | |
103 | + totalKey: DeviceInfoOfEdge.TOTAL_MEMORY_OF_EDGE, | |
104 | + totalCount: {}, | |
105 | + type: DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE, | |
106 | + xAxisData: [], | |
107 | + seriesData: [], | |
108 | + icon: memorySvg, | |
109 | + }, | |
110 | + { | |
111 | + name: '磁盘', | |
112 | + text: '磁盘使用率', | |
113 | + currentValue: 0, | |
114 | + totalKey: DeviceInfoOfEdge.TOTAL_DISC_SPACE_OF_EDGE, | |
115 | + totalCount: {}, | |
116 | + type: DeviceInfoOfEdge.DISC_USAGE_OF_EDGE, | |
117 | + xAxisData: [], | |
118 | + seriesData: [], | |
119 | + icon: diskSvg, | |
120 | + }, | |
121 | + ]); | |
122 | + | |
123 | + const chartsInstance = shallowReactive<{ [key: string]: echarts.ECharts }>({}); | |
124 | + | |
125 | + // const cacheUsedSpace = (total, percentage) => { | |
126 | + // if (!total) return; | |
127 | + // const takeValue = total.split('.')[0]; | |
128 | + // const takeUnit = total.split('.')[1]; | |
129 | + // return `${(takeValue * (percentage / 100)).toFixed(1)}${takeUnit}`; | |
130 | + // }; | |
131 | + | |
132 | + const { send, close, data, open } = useWebSocket(socketInfo.origin, { | |
133 | + immediate: false, | |
134 | + autoReconnect: true, | |
135 | + async onConnected() { | |
136 | + getSendValue.value.tsSubCmds[0].entityId = socketInfo.entityId; | |
137 | + send(JSON.stringify(unref(getSendValue))); | |
138 | + }, | |
139 | + async onMessage() { | |
140 | + try { | |
141 | + const value = JSON.parse(unref(data)) as any; | |
142 | + if (value) { | |
143 | + const { data } = value; | |
144 | + const keys = Object.keys(data); | |
145 | + for (const key of keys) { | |
146 | + chartInstance.value.forEach((chartItem: ChartInstance) => { | |
147 | + if (chartItem.type === key) { | |
148 | + chartItem.xAxisData.push(moment(data[key][0][0]).format('HH:mm')); | |
149 | + // chartItem.seriesData = []; | |
150 | + chartItem.seriesData = data[key][0][1] ? [data[key][0][1]] : []; | |
151 | + chartItem.currentValue = Number(data[key][0][1]); | |
152 | + } | |
153 | + if (chartItem.totalKey === key) { | |
154 | + chartItem.totalCount.cpuTotal = | |
155 | + key === DeviceInfoOfEdge.CPU_COUNT_OF_EDGE ? data[key][0][1] : 0; | |
156 | + chartItem.totalCount.totalDisc = | |
157 | + key === DeviceInfoOfEdge.TOTAL_DISC_SPACE_OF_EDGE | |
158 | + ? formatSizeUnits(Number(data[key][0][1])) | |
159 | + : 0; | |
160 | + chartItem.totalCount.totalMemory = | |
161 | + key === DeviceInfoOfEdge.TOTAL_MEMORY_OF_EDGE | |
162 | + ? formatSizeUnits(Number(data[key][0][1])) | |
163 | + : 0; | |
164 | + } | |
165 | + }); | |
166 | + } | |
167 | + await nextTick(); | |
168 | + handleRenderChartInstance(chartInstance.value); | |
169 | + } | |
170 | + } catch (error) {} | |
171 | + }, | |
172 | + onDisconnected() {}, | |
173 | + onError() { | |
174 | + createMessage.error('webSocket连接超时,请联系管理员'); | |
175 | + }, | |
176 | + }); | |
177 | + | |
178 | + function onResize(type) { | |
179 | + if (!chartsInstance[type]) return; | |
180 | + chartsInstance[type]?.resize(); | |
181 | + } | |
182 | + | |
183 | + const chartOption = { | |
184 | + series: [ | |
185 | + { | |
186 | + data: [], | |
187 | + detail: { | |
188 | + formatter: `暂无数据`, | |
189 | + }, | |
190 | + type: 'gauge', | |
191 | + axisLine: { | |
192 | + lineStyle: { | |
193 | + width: 10, | |
194 | + color: [ | |
195 | + [0.2, '#739ded'], | |
196 | + [0.8, '#5f89d8'], | |
197 | + [1, '#377dff'], | |
198 | + ], | |
199 | + }, | |
200 | + }, | |
201 | + pointer: { | |
202 | + itemStyle: { | |
203 | + color: 'auto', | |
204 | + }, | |
205 | + }, | |
206 | + axisTick: { | |
207 | + distance: 0, | |
208 | + length: 8, | |
209 | + lineStyle: { | |
210 | + color: 'auto', | |
211 | + }, | |
212 | + }, | |
213 | + splitLine: { | |
214 | + distance: 0, | |
215 | + length: 10, | |
216 | + lineStyle: { | |
217 | + color: 'auto', | |
218 | + width: 3, | |
219 | + }, | |
220 | + }, | |
221 | + axisLabel: { | |
222 | + color: 'inherit', | |
223 | + distance: 15, | |
224 | + fontSize: 8, | |
225 | + }, | |
226 | + }, | |
227 | + ], | |
228 | + }; | |
229 | + const pieOption = { | |
230 | + grid: { | |
231 | + top: 0, | |
232 | + bottom: 0, | |
233 | + left: 0, | |
234 | + right: 0, | |
235 | + }, | |
236 | + tooltip: { | |
237 | + trigger: 'item', | |
238 | + formatter: '{b}<br/>{d}%', | |
239 | + textStyle: { | |
240 | + fontSize: 12, | |
241 | + }, | |
242 | + }, | |
243 | + title: { | |
244 | + text: '磁盘容量', | |
245 | + subtext: '暂无数据', | |
246 | + textStyle: { | |
247 | + fontSize: 12, | |
248 | + color: '#72767c', | |
249 | + }, | |
250 | + subtextStyle: { | |
251 | + fontSize: 14, | |
252 | + color: '#000000', | |
253 | + fontWeight: 500, | |
254 | + }, | |
255 | + textAlign: 'center', | |
256 | + left: '48.5%', | |
257 | + top: '44%', | |
258 | + }, | |
259 | + legend: { | |
260 | + show: true, | |
261 | + icon: 'circle', | |
262 | + | |
263 | + bottom: '0%', | |
264 | + data: [{ name: '磁盘已使用' }, { name: '磁盘剩余空间' }].map((item) => ({ | |
265 | + name: item.name, | |
266 | + })), | |
267 | + formatter(name: string) { | |
268 | + return `{b_style|${name}} `; | |
269 | + }, | |
270 | + textStyle: { | |
271 | + color: '#000', | |
272 | + rich: { | |
273 | + b_style: { | |
274 | + color: '#8d8ea0', | |
275 | + fontSize: 12, | |
276 | + padding: [0, 5, 0, 0], | |
277 | + }, | |
278 | + }, | |
279 | + }, | |
280 | + }, | |
281 | + series: [ | |
282 | + { | |
283 | + type: 'pie', | |
284 | + // center: ['35%', '50%'], | |
285 | + radius: ['40%', '67%'], | |
286 | + startAngle: 30, | |
287 | + emphasis: { | |
288 | + scale: false, | |
289 | + }, | |
290 | + label: { | |
291 | + position: 'outside', | |
292 | + alignTo: 'labelLine', | |
293 | + height: 0, | |
294 | + width: 0, | |
295 | + lineHeight: 0, | |
296 | + distanceToLabelLine: 75, | |
297 | + borderRadius: 3, | |
298 | + borderWidth: 1, | |
299 | + borderColor: 'none', | |
300 | + padding: [20, -15, 0, -10], | |
301 | + rich: { | |
302 | + a: { | |
303 | + padding: [0, -80, 55, -80], | |
304 | + fontSize: '12px', | |
305 | + color: '#000000', | |
306 | + }, | |
307 | + b: { | |
308 | + padding: [20, -80, 40, -80], | |
309 | + fontSize: '12px', | |
310 | + color: '#72767c', | |
311 | + }, | |
312 | + }, | |
313 | + // formatter: (params: any) => { | |
314 | + // const { data } = params; | |
315 | + // const { value } = data || {}; | |
316 | + | |
317 | + // const total = Number(totalCount.totalDisc?.split('.')[0] || 0); | |
318 | + // const totalUnit = totalCount.totalDisc?.split('.')[1] || 0; | |
319 | + | |
320 | + // return `{a|${value && total ? value : ''}${value && total ? '%' : ''}} \n {b|${ | |
321 | + // total ? (total * (value / 100)).toFixed(1) : '' | |
322 | + // }${total ? totalUnit : ''}}`; | |
323 | + // }, | |
324 | + }, | |
325 | + labelLine: { | |
326 | + show: false, | |
327 | + length: 5, | |
328 | + // align: 'bottom', | |
329 | + lineStyle: { | |
330 | + width: 1, | |
331 | + }, | |
332 | + }, | |
333 | + data: [ | |
334 | + { name: '磁盘已使用', value: 0, itemStyle: { color: '#90b2f1' } }, | |
335 | + { name: '磁盘剩余空间', value: 100, itemStyle: { color: '#377dff' } }, | |
336 | + ], | |
337 | + }, | |
338 | + ], | |
339 | + }; | |
340 | + | |
341 | + const handleRenderChartInstance = async (chartInstance: ChartInstance[]) => { | |
342 | + await nextTick(); | |
343 | + if (!chartInstance) return; | |
344 | + if (isArray(chartInstance) && chartInstance.length <= 0) return; | |
345 | + for (const item of unref(chartInstance)) { | |
346 | + const { type, seriesData, currentValue, totalCount } = item; | |
347 | + // chartsInstance[type] = echarts.init(document.getElementById(`chart-${type}`) as HTMLElement); | |
348 | + if (type !== DeviceInfoOfEdge.DISC_USAGE_OF_EDGE) { | |
349 | + chartsInstance[type].setOption({ | |
350 | + series: [ | |
351 | + { | |
352 | + data: seriesData, | |
353 | + detail: { | |
354 | + formatter: `${isNumber(currentValue) ? '{value} %' : '暂无数据'}`, | |
355 | + }, | |
356 | + }, | |
357 | + ], | |
358 | + }); | |
359 | + } else { | |
360 | + chartsInstance[type].setOption({ | |
361 | + title: { | |
362 | + subtext: totalCount.totalDisc, | |
363 | + }, | |
364 | + series: [ | |
365 | + { | |
366 | + label: { | |
367 | + formatter: (params: any) => { | |
368 | + const { data } = params; | |
369 | + const { value } = data || {}; | |
370 | + | |
371 | + const total = Number(totalCount.totalDisc?.split('.')[0] || 0); | |
372 | + const totalUnit = totalCount.totalDisc?.split('.')[1] || 0; | |
373 | + | |
374 | + return `{a|${value && total ? value : ''}${value && total ? '%' : ''}} \n {b|${ | |
375 | + total ? (total * (value / 100)).toFixed(1) : '' | |
376 | + }${total ? totalUnit : ''}}`; | |
377 | + }, | |
378 | + }, | |
379 | + data: [ | |
380 | + { name: '磁盘已使用', value: currentValue, itemStyle: { color: '#90b2f1' } }, | |
381 | + { | |
382 | + name: '磁盘剩余空间', | |
383 | + value: 100 - currentValue, | |
384 | + itemStyle: { color: '#377dff' }, | |
385 | + }, | |
386 | + ], | |
387 | + }, | |
388 | + ], | |
389 | + }); | |
390 | + } | |
391 | + | |
392 | + window.addEventListener('resize', () => onResize(type)); | |
393 | + } | |
394 | + }; | |
395 | + | |
396 | + onUnmounted(() => { | |
397 | + window.removeEventListener('resize', onResize); | |
398 | + }); | |
399 | + | |
400 | + onMounted(() => { | |
401 | + socketInfo.entityId = props.recordData?.id?.id as string; | |
402 | + if (socketInfo.entityId) { | |
403 | + open(); | |
404 | + [ | |
405 | + DeviceInfoOfEdge.CPU_USAGE_OF_EDGE, | |
406 | + DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE, | |
407 | + DeviceInfoOfEdge.DISC_USAGE_OF_EDGE, | |
408 | + ].forEach((item) => { | |
409 | + chartsInstance[item] = echarts.init( | |
410 | + document.getElementById(`chart-${item}`) as HTMLElement | |
411 | + ); | |
412 | + if (item !== DeviceInfoOfEdge.DISC_USAGE_OF_EDGE) { | |
413 | + chartsInstance[item]?.setOption(chartOption); | |
414 | + } else { | |
415 | + chartsInstance[item]?.setOption(pieOption); | |
416 | + } | |
417 | + }); | |
418 | + } | |
419 | + }); | |
420 | + | |
421 | + onUnmounted(() => close()); | |
422 | +</script> | |
423 | + | |
424 | +<template> | |
425 | + <div> | |
426 | + <a-row justify="space-around" align="middle" :gutter="{ xs: 8, sm: 16, md: 24, lg: 32 }"> | |
427 | + <a-col | |
428 | + class="gutter-row" | |
429 | + style="background: #f5f5f5; border-radius: 20px" | |
430 | + :span="7" | |
431 | + v-for="(item, index) in chartInstance" | |
432 | + :key="index" | |
433 | + > | |
434 | + <a-row align="middle"> | |
435 | + <div class="!flex justify-between items-center font-bold fill-dark-900 p-2.5 mt-4"> | |
436 | + <img :src="item.icon" /> | |
437 | + <span class="ml-1">{{ item.text }}</span> | |
438 | + <!-- <span>{{ item.currentValue }}%</span> | |
439 | + <span v-if="item.type !== DeviceInfoOfEdge.DISC_USAGE_OF_EDGE" | |
440 | + >{{ | |
441 | + item.type === DeviceInfoOfEdge.CPU_USAGE_OF_EDGE | |
442 | + ? item.totalCount.cpuTotal | |
443 | + : item.type === DeviceInfoOfEdge.MEMORY_USAGE_OF_EDGE | |
444 | + ? item.totalCount.totalMemory | |
445 | + : 0 | |
446 | + }}{{ item.type === DeviceInfoOfEdge.CPU_USAGE_OF_EDGE ? 'cores' : '' }}</span | |
447 | + > | |
448 | + <span v-if="item.type === DeviceInfoOfEdge.DISC_USAGE_OF_EDGE" style="color: #d46b08" | |
449 | + >已用{{ cacheUsedSpace(item.totalCount.totalDisc, item.currentValue) }}</span | |
450 | + > | |
451 | + <span v-if="item.type === DeviceInfoOfEdge.DISC_USAGE_OF_EDGE" style="color: #1677ff" | |
452 | + >可用{{ item.totalCount.totalDisc }}</span | |
453 | + > --> | |
454 | + </div> | |
455 | + </a-row> | |
456 | + <a-row class="mt-8" justify="space-around" align="middle"> | |
457 | + <div class="flex w-full justify-center relative"> | |
458 | + <Empty | |
459 | + description="暂无数据" | |
460 | + class="text-dark-50 m-4 absolute" | |
461 | + :style="{ display: item.seriesData.length == 0 ? 'block' : 'none' }" | |
462 | + /> | |
463 | + <div | |
464 | + :id="`chart-${item.type}`" | |
465 | + class="m-4 w-9/10 h-300px" | |
466 | + :style="{ opacity: item.seriesData.length > 0 ? 1 : 0 }" | |
467 | + ></div> | |
468 | + </div> | |
469 | + </a-row> | |
470 | + </a-col> | |
471 | + </a-row> | |
472 | + </div> | |
473 | +</template> | ... | ... |
src/views/edge/instance/config/index.ts
0 → 100644
1 | +export enum PermissionEnum { | |
2 | + CREATE = 'api:yt:task_center:add:post', | |
3 | + UPDATE = 'api:yt:task_center:update:update', | |
4 | + START_TASK = 'api:yt:task_center:update:state', | |
5 | + DELETE = 'api:yt:task_center:delete', | |
6 | + ALLOW = 'api:yt:task_center:cancel:allow', | |
7 | + EXECUTE = 'api:yt:task_center:immediate:execute', | |
8 | + DETAIL = 'api:yt:task_center:get', | |
9 | +} | |
10 | + | |
11 | +export enum HandleOperationEnum { | |
12 | + CREATE = 'create', | |
13 | + VIEW = 'view', | |
14 | + UPDATE = 'update', | |
15 | + BATCH_DELETE = 'batchDelete', | |
16 | + DELETE = 'singleDelete', | |
17 | +} | |
18 | + | |
19 | +export enum HandleOperationNameEnum { | |
20 | + CREATE = '新增', | |
21 | + VIEW = '查看', | |
22 | + UPDATE = '编辑', | |
23 | + BATCH_DELETE = '批量删除', | |
24 | + DELETE = '删除', | |
25 | +} | ... | ... |
src/views/edge/instance/index.vue
0 → 100644
... | ... | @@ -3,7 +3,12 @@ |
3 | 3 | import { BasicForm, useForm } from '/@/components/Form'; |
4 | 4 | import { ALG, formSchema, PackageField } from '../config/packageDetail.config'; |
5 | 5 | import { ref } from 'vue'; |
6 | - import { createOtaPackage, uploadOtaPackages, deleteOtaPackage } from '/@/api/ota'; | |
6 | + import { | |
7 | + createOtaPackage, | |
8 | + uploadOtaPackages, | |
9 | + deleteOtaPackage, | |
10 | + getDeviceProfileInfo, | |
11 | + } from '/@/api/ota'; | |
7 | 12 | import { CreateOtaPackageParams } from '/@/api/ota/model'; |
8 | 13 | |
9 | 14 | interface FieldsValue extends CreateOtaPackageParams { |
... | ... | @@ -14,6 +19,8 @@ |
14 | 19 | |
15 | 20 | const emit = defineEmits(['register', 'update:list']); |
16 | 21 | |
22 | + const defaultDeviceProfile = 'default'; | |
23 | + | |
17 | 24 | const loading = ref(false); |
18 | 25 | |
19 | 26 | const [registerModal, { changeLoading, closeModal }] = useModalInner(); |
... | ... | @@ -72,6 +79,10 @@ |
72 | 79 | try { |
73 | 80 | await validate(); |
74 | 81 | const value = getFieldsValue(); |
82 | + if (value[PackageField.DEVICE_PROFILE_INFO] === defaultDeviceProfile) { | |
83 | + const data = await getDeviceProfileInfo(defaultDeviceProfile); | |
84 | + value[PackageField.DEVICE_PROFILE_INFO] = data.id.id; | |
85 | + } | |
75 | 86 | value[PackageField.DEVICE_PROFILE_INFO] = { |
76 | 87 | id: value[PackageField.DEVICE_PROFILE_INFO], |
77 | 88 | entityType: 'DEVICE_PROFILE', | ... | ... |
1 | -import { getDeviceProfileInfo, getDeviceProfileInfos } from '/@/api/ota'; | |
1 | +import { getDeviceProfileInfos } from '/@/api/ota'; | |
2 | 2 | |
3 | 3 | import { FormSchema } from '/@/components/Form'; |
4 | 4 | import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; |
5 | +import { createPickerSearch } from '/@/utils/pickerSearch'; | |
5 | 6 | |
6 | 7 | export enum PackageField { |
7 | 8 | TITLE = 'title', |
... | ... | @@ -107,39 +108,25 @@ export const formSchema: FormSchema[] = [ |
107 | 108 | { |
108 | 109 | field: PackageField.DEVICE_PROFILE_INFO, |
109 | 110 | label: '所属产品', |
110 | - component: 'ApiSearchSelect', | |
111 | + component: 'ApiSelectScrollLoad', | |
111 | 112 | helpMessage: ['上传的包仅适用于具有所选配置文件的设备'], |
112 | 113 | rules: [{ required: true, message: '所属产品为必填项' }], |
113 | 114 | defaultValue: 'default', |
114 | - componentProps: ({ formModel, formActionType }) => { | |
115 | + componentProps: () => { | |
115 | 116 | return { |
116 | 117 | placeholder: '请选择所属产品', |
117 | - showSearch: true, | |
118 | 118 | labelField: 'name', |
119 | 119 | valueField: 'id', |
120 | + pagination: { page: 0, pageSize: 10 }, | |
120 | 121 | api: async (params: Recordable) => { |
121 | - const data = await getDeviceProfileInfos(params); | |
122 | - return data.data.map((item) => ({ | |
122 | + const { totalElements, data } = await getDeviceProfileInfos(params); | |
123 | + const dataList = data.map((item) => ({ | |
123 | 124 | ...item, |
124 | 125 | id: item.id.id, |
125 | 126 | })); |
127 | + return { items: dataList, total: totalElements }; | |
126 | 128 | }, |
127 | - params: (textSearch: string) => { | |
128 | - return { | |
129 | - page: 0, | |
130 | - pageSize: 10, | |
131 | - textSearch, | |
132 | - }; | |
133 | - }, | |
134 | - queryApi: async (id = 'default') => { | |
135 | - const data = await getDeviceProfileInfo(id); | |
136 | - | |
137 | - if (formModel[PackageField.DEVICE_PROFILE_INFO] === 'default' && id === 'default') { | |
138 | - formActionType.setFieldsValue({ [PackageField.DEVICE_PROFILE_INFO]: data.id.id }); | |
139 | - } | |
140 | - | |
141 | - return { ...data, id: data.id.id }; | |
142 | - }, | |
129 | + ...createPickerSearch(), | |
143 | 130 | }; |
144 | 131 | }, |
145 | 132 | }, | ... | ... |
... | ... | @@ -13,7 +13,7 @@ |
13 | 13 | import { BasicTable, useTable } from '/@/components/Table'; |
14 | 14 | import { FETCH_SETTING } from '/@/components/Table/src/const'; |
15 | 15 | import { useDesign } from '/@/hooks/web/useDesign'; |
16 | - import { isFunction } from '/@/utils/is'; | |
16 | + import { isArray, isFunction } from '/@/utils/is'; | |
17 | 17 | |
18 | 18 | interface DeviceModel extends RawDeviceModal { |
19 | 19 | disabled?: boolean; |
... | ... | @@ -225,7 +225,7 @@ |
225 | 225 | return; |
226 | 226 | } |
227 | 227 | const { items } = await devicePage({ page: 1, pageSize: 10, ...params, selected: true }); |
228 | - selectedTotalList.value = items; | |
228 | + selectedTotalList.value = isArray(props.value) && props.value.length === 0 ? [] : items; | |
229 | 229 | }); |
230 | 230 | </script> |
231 | 231 | ... | ... |
1 | -import { h } from 'vue'; | |
1 | +import { h, unref } from 'vue'; | |
2 | 2 | import { BasicColumn } from '/@/components/Table'; |
3 | 3 | import { dateUtil } from '/@/utils/dateUtil'; |
4 | 4 | import Icon from '/@/components/Icon'; |
5 | 5 | import { Modal } from 'ant-design-vue'; |
6 | 6 | import { JsonPreview } from '/@/components/CodeEditor'; |
7 | +import { CopyOutlined } from '@ant-design/icons-vue'; | |
8 | +import { useClipboard } from '@vueuse/core'; | |
9 | +import { useMessage } from '/@/hooks/web/useMessage'; | |
7 | 10 | |
8 | 11 | const handleOpenJsonPreviewModal = (title: string, content: string) => { |
9 | 12 | Modal.info({ |
... | ... | @@ -13,6 +16,15 @@ const handleOpenJsonPreviewModal = (title: string, content: string) => { |
13 | 16 | }); |
14 | 17 | }; |
15 | 18 | |
19 | +const { copy, copied } = useClipboard({ legacy: true }); | |
20 | +const { createMessage } = useMessage(); | |
21 | + | |
22 | +async function copyText(text: string) { | |
23 | + await copy(text); | |
24 | + | |
25 | + unref(copied) ? createMessage.success('复制成功') : createMessage.error('复制失败'); | |
26 | +} | |
27 | + | |
16 | 28 | export const columns: BasicColumn[] = [ |
17 | 29 | { |
18 | 30 | title: '事件时间', |
... | ... | @@ -34,13 +46,30 @@ export const columns: BasicColumn[] = [ |
34 | 46 | }, |
35 | 47 | { |
36 | 48 | title: '实体类型', |
37 | - dataIndex: 'body.entityName', | |
49 | + dataIndex: 'body.entityType', | |
38 | 50 | ellipsis: true, |
39 | 51 | }, |
40 | 52 | { |
41 | - title: '消息ID', | |
53 | + title: '实体ID', | |
42 | 54 | dataIndex: 'body.entityId', |
43 | 55 | ellipsis: true, |
56 | + customRender: ({ text }: { text: string }) => { | |
57 | + return h('span', { class: 'flex items-center' }, [ | |
58 | + h('span', { class: 'truncate' }, text), | |
59 | + h(CopyOutlined, { class: 'flex-shrink cursor-pointer', onClick: () => copyText(text) }), | |
60 | + ]); | |
61 | + }, | |
62 | + }, | |
63 | + { | |
64 | + title: '消息ID', | |
65 | + dataIndex: 'body.msgId', | |
66 | + ellipsis: true, | |
67 | + customRender: ({ text }: { text: string }) => { | |
68 | + return h('span', { class: 'flex items-center' }, [ | |
69 | + h('span', { class: 'truncate' }, text), | |
70 | + h(CopyOutlined, { class: 'flex-shrink cursor-pointer', onClick: () => copyText(text) }), | |
71 | + ]); | |
72 | + }, | |
44 | 73 | }, |
45 | 74 | { |
46 | 75 | title: '消息类型', | ... | ... |
... | ... | @@ -217,7 +217,7 @@ export const getFormSchemas = (hasAlarmNotify: Ref<boolean>): FormSchema[] => { |
217 | 217 | const { setFieldsValue } = formActionType; |
218 | 218 | return { |
219 | 219 | api: async () => { |
220 | - return await getDeviceProfile(deviceType); | |
220 | + return await getDeviceProfile(deviceType, true); | |
221 | 221 | }, |
222 | 222 | labelField: 'name', |
223 | 223 | valueField: 'id', |
... | ... | @@ -283,7 +283,10 @@ export const getFormSchemas = (hasAlarmNotify: Ref<boolean>): FormSchema[] => { |
283 | 283 | return { |
284 | 284 | api: async (params: Record<'organizationId' | 'deviceProfileId', string>) => { |
285 | 285 | if (params.deviceProfileId && params.organizationId) { |
286 | - const result = await byOrganizationIdGetMasterDevice(params); | |
286 | + const result = await byOrganizationIdGetMasterDevice({ | |
287 | + ...params, | |
288 | + isSceneLinkage: true, | |
289 | + }); | |
287 | 290 | if (result) { |
288 | 291 | return result.map((item) => ({ |
289 | 292 | ...item, | ... | ... |
... | ... | @@ -147,7 +147,7 @@ export const getFormSchemas = ( |
147 | 147 | const { setFieldsValue } = formActionType; |
148 | 148 | return { |
149 | 149 | api: async () => { |
150 | - return await getDeviceProfile(deviceType); | |
150 | + return await getDeviceProfile(deviceType, true); | |
151 | 151 | }, |
152 | 152 | labelField: 'name', |
153 | 153 | valueField: 'id', |
... | ... | @@ -251,7 +251,10 @@ export const getFormSchemas = ( |
251 | 251 | return { |
252 | 252 | api: async (params: Record<'organizationId' | 'deviceProfileId', string>) => { |
253 | 253 | if (params.deviceProfileId && params.organizationId) { |
254 | - const result = await byOrganizationIdGetMasterDevice(params); | |
254 | + const result = await byOrganizationIdGetMasterDevice({ | |
255 | + ...params, | |
256 | + isSceneLinkage: true, | |
257 | + }); | |
255 | 258 | if (result) { |
256 | 259 | return result.map((item) => ({ |
257 | 260 | ...item, | ... | ... |
... | ... | @@ -143,7 +143,12 @@ |
143 | 143 | </div> |
144 | 144 | <Tooltip title="删除"> |
145 | 145 | <Icon |
146 | - v-if="!disabledDrawer && flipFlopListElRef.length > 1" | |
146 | + v-if=" | |
147 | + !disabledDrawer && | |
148 | + (type === FlipFlopComponentTypeEnum.FLIP_FLOP | |
149 | + ? flipFlopListElRef.length > 1 | |
150 | + : true) | |
151 | + " | |
147 | 152 | class="ml-2 cursor-pointer" |
148 | 153 | icon="fluent:delete-off-20-regular" |
149 | 154 | size="20" | ... | ... |
1 | +import { FileItem } from '../types'; | |
2 | +import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter'; | |
1 | 3 | import type { FormSchema } from '/@/components/Form/index'; |
4 | +import { createImgPreview } from '/@/components/Preview'; | |
2 | 5 | export const schemas: FormSchema[] = [ |
3 | 6 | { |
4 | 7 | field: 'name', |
... | ... | @@ -27,21 +30,99 @@ export const schemas: FormSchema[] = [ |
27 | 30 | }, |
28 | 31 | { |
29 | 32 | field: 'logo', |
30 | - component: 'Upload', | |
31 | 33 | label: 'APP Logo', |
32 | - colProps: { | |
33 | - span: 24, | |
34 | + // component: 'Upload', | |
35 | + // colProps: { | |
36 | + // span: 24, | |
37 | + // }, | |
38 | + // slot: 'logoUpload', | |
39 | + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为32*32px,大小不超过5M '], | |
40 | + component: 'ApiUpload', | |
41 | + changeEvent: 'update:fileList', | |
42 | + valueField: 'fileList', | |
43 | + componentProps: ({ formModel }) => { | |
44 | + return { | |
45 | + listType: 'picture-card', | |
46 | + maxFileLimit: 1, | |
47 | + accept: '.png,.jpg,.jpeg,.gif', | |
48 | + api: async (file: File) => { | |
49 | + try { | |
50 | + const formData = new FormData(); | |
51 | + formData.set('file', file); | |
52 | + const { fileStaticUri, fileName } = await uploadThumbnail(formData); | |
53 | + return { | |
54 | + uid: fileStaticUri, | |
55 | + name: fileName, | |
56 | + url: fileStaticUri, | |
57 | + } as FileItem; | |
58 | + } catch (error) { | |
59 | + return {}; | |
60 | + } | |
61 | + }, | |
62 | + // showUploadList: true, | |
63 | + onDownload() {}, | |
64 | + onPreview: (fileList: FileItem) => { | |
65 | + createImgPreview({ imageList: [fileList.url!] }); | |
66 | + }, | |
67 | + onDelete(url: string) { | |
68 | + formModel.deleteLogoUrl = url!; | |
69 | + }, | |
70 | + }; | |
34 | 71 | }, |
35 | - slot: 'logoUpload', | |
36 | 72 | }, |
37 | 73 | { |
38 | - field: 'background', | |
74 | + field: 'deleteLogoUrl', | |
75 | + label: '', | |
39 | 76 | component: 'Input', |
77 | + show: false, | |
78 | + }, | |
79 | + { | |
80 | + field: 'background', | |
40 | 81 | label: '登录页背景图片', |
41 | - colProps: { | |
42 | - span: 24, | |
82 | + // component: 'Input', | |
83 | + // colProps: { | |
84 | + // span: 24, | |
85 | + // }, | |
86 | + // slot: 'bgUpload', | |
87 | + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为1920*1080px,大小不超过5M'], | |
88 | + component: 'ApiUpload', | |
89 | + changeEvent: 'update:fileList', | |
90 | + valueField: 'fileList', | |
91 | + componentProps: ({ formModel }) => { | |
92 | + return { | |
93 | + listType: 'picture-card', | |
94 | + maxFileLimit: 1, | |
95 | + accept: '.png,.jpg,.jpeg,.gif,', | |
96 | + api: async (file: File) => { | |
97 | + try { | |
98 | + const formData = new FormData(); | |
99 | + formData.set('file', file); | |
100 | + const { fileStaticUri, fileName } = await uploadThumbnail(formData); | |
101 | + return { | |
102 | + uid: fileStaticUri, | |
103 | + name: fileName, | |
104 | + url: fileStaticUri, | |
105 | + } as FileItem; | |
106 | + } catch (error) { | |
107 | + return {}; | |
108 | + } | |
109 | + }, | |
110 | + // showUploadList: true, | |
111 | + onDownload() {}, | |
112 | + onPreview: (fileList: FileItem) => { | |
113 | + createImgPreview({ imageList: [fileList.url!] }); | |
114 | + }, | |
115 | + onDelete(url: string) { | |
116 | + formModel.deleteBackgroundUrl = url!; | |
117 | + }, | |
118 | + }; | |
43 | 119 | }, |
44 | - slot: 'bgUpload', | |
120 | + }, | |
121 | + { | |
122 | + field: 'deleteBackgroundUrl', | |
123 | + label: '', | |
124 | + component: 'Input', | |
125 | + show: false, | |
45 | 126 | }, |
46 | 127 | { |
47 | 128 | field: 'backgroundColor', | ... | ... |
1 | +import { FileItem } from '../types'; | |
2 | +import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter'; | |
1 | 3 | import type { FormSchema } from '/@/components/Form/index'; |
4 | +import { createImgPreview } from '/@/components/Preview'; | |
2 | 5 | |
3 | 6 | export const schemas: FormSchema[] = [ |
4 | 7 | { |
... | ... | @@ -28,30 +31,148 @@ export const schemas: FormSchema[] = [ |
28 | 31 | }, |
29 | 32 | { |
30 | 33 | field: 'logo', |
31 | - component: 'Upload', | |
32 | 34 | label: '平台Logo', |
33 | - colProps: { | |
34 | - span: 24, | |
35 | + // component: 'Upload', | |
36 | + // colProps: { | |
37 | + // span: 24, | |
38 | + // }, | |
39 | + // slot: 'logoUpload', | |
40 | + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为32*32px,大小不超过5M '], | |
41 | + component: 'ApiUpload', | |
42 | + changeEvent: 'update:fileList', | |
43 | + valueField: 'fileList', | |
44 | + componentProps: ({ formModel }) => { | |
45 | + return { | |
46 | + listType: 'picture-card', | |
47 | + maxFileLimit: 1, | |
48 | + accept: '.png,.jpg,.jpeg,.gif', | |
49 | + api: async (file: File) => { | |
50 | + try { | |
51 | + const formData = new FormData(); | |
52 | + formData.set('file', file); | |
53 | + const { fileStaticUri, fileName } = await uploadThumbnail(formData); | |
54 | + return { | |
55 | + uid: fileStaticUri, | |
56 | + name: fileName, | |
57 | + url: fileStaticUri, | |
58 | + } as FileItem; | |
59 | + } catch (error) { | |
60 | + return {}; | |
61 | + } | |
62 | + }, | |
63 | + // showUploadList: true, | |
64 | + onDownload() {}, | |
65 | + onPreview: (fileList: FileItem) => { | |
66 | + createImgPreview({ imageList: [fileList.url!] }); | |
67 | + }, | |
68 | + onDelete(url: string) { | |
69 | + formModel.deleteLogoUrl = url!; | |
70 | + }, | |
71 | + }; | |
35 | 72 | }, |
36 | - slot: 'logoUpload', | |
73 | + }, | |
74 | + { | |
75 | + field: 'deleteLogoUrl', | |
76 | + label: '', | |
77 | + component: 'Input', | |
78 | + show: false, | |
37 | 79 | }, |
38 | 80 | { |
39 | 81 | field: 'icon', |
40 | - component: 'Upload', | |
41 | 82 | label: '浏览器ico图标', |
42 | - colProps: { | |
43 | - span: 24, | |
83 | + // component: 'Upload', | |
84 | + // colProps: { | |
85 | + // span: 24, | |
86 | + // }, | |
87 | + // slot: 'iconUpload', | |
88 | + helpMessage: ['支持.ICO格式,建议尺寸为16*16px '], | |
89 | + component: 'ApiUpload', | |
90 | + changeEvent: 'update:fileList', | |
91 | + valueField: 'fileList', | |
92 | + componentProps: ({ formModel }) => { | |
93 | + return { | |
94 | + listType: 'picture-card', | |
95 | + maxFileLimit: 1, | |
96 | + maxSize: 500 * 1024, | |
97 | + accept: '.icon,.ico', | |
98 | + api: async (file: File) => { | |
99 | + try { | |
100 | + const formData = new FormData(); | |
101 | + formData.set('file', file); | |
102 | + const { fileStaticUri, fileName } = await uploadThumbnail(formData); | |
103 | + return { | |
104 | + uid: fileStaticUri, | |
105 | + name: fileName, | |
106 | + url: fileStaticUri, | |
107 | + } as FileItem; | |
108 | + } catch (error) { | |
109 | + return {}; | |
110 | + } | |
111 | + }, | |
112 | + // showUploadList: true, | |
113 | + onDownload() {}, | |
114 | + onPreview: (fileList: FileItem) => { | |
115 | + createImgPreview({ imageList: [fileList.url!] }); | |
116 | + }, | |
117 | + onDelete(url: string) { | |
118 | + formModel.deleteIconUrl = url!; | |
119 | + }, | |
120 | + }; | |
44 | 121 | }, |
45 | - slot: 'iconUpload', | |
46 | 122 | }, |
47 | 123 | { |
48 | - field: 'background', | |
124 | + field: 'deleteIconUrl', | |
125 | + label: '', | |
49 | 126 | component: 'Input', |
127 | + show: false, | |
128 | + }, | |
129 | + { | |
130 | + field: 'background', | |
50 | 131 | label: '登录页背景图片', |
51 | - colProps: { | |
52 | - span: 24, | |
132 | + // component: 'Input', | |
133 | + // colProps: { | |
134 | + // span: 24, | |
135 | + // }, | |
136 | + // slot: 'bgUpload', | |
137 | + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为1920*1080px以上,大小不超过5M'], | |
138 | + component: 'ApiUpload', | |
139 | + changeEvent: 'update:fileList', | |
140 | + valueField: 'fileList', | |
141 | + componentProps: ({ formModel }) => { | |
142 | + return { | |
143 | + listType: 'picture-card', | |
144 | + maxFileLimit: 1, | |
145 | + accept: '.png,.jpg,.jpeg,.gif,.jfif', | |
146 | + api: async (file: File) => { | |
147 | + try { | |
148 | + const formData = new FormData(); | |
149 | + formData.set('file', file); | |
150 | + const { fileStaticUri, fileName } = await uploadThumbnail(formData); | |
151 | + return { | |
152 | + uid: fileStaticUri, | |
153 | + name: fileName, | |
154 | + url: fileStaticUri, | |
155 | + } as FileItem; | |
156 | + } catch (error) { | |
157 | + return {}; | |
158 | + } | |
159 | + }, | |
160 | + // showUploadList: true, | |
161 | + onDownload() {}, | |
162 | + onPreview: (fileList: FileItem) => { | |
163 | + createImgPreview({ imageList: [fileList.url!] }); | |
164 | + }, | |
165 | + onDelete(url: string) { | |
166 | + formModel.deleteBackgroundUrl = url!; | |
167 | + }, | |
168 | + }; | |
53 | 169 | }, |
54 | - slot: 'bgUpload', | |
170 | + }, | |
171 | + { | |
172 | + field: 'deleteBackgroundUrl', | |
173 | + label: '', | |
174 | + component: 'Input', | |
175 | + show: false, | |
55 | 176 | }, |
56 | 177 | { |
57 | 178 | field: 'backgroundColor', | ... | ... |
1 | 1 | import type { FormSchema } from '/@/components/Form/index'; |
2 | 2 | import { getAreaList } from '/@/api/oem/index'; |
3 | 3 | import { emailRule, phoneRule } from '/@/utils/rules'; |
4 | +import { uploadThumbnail } from '/@/api/configuration/center/configurationCenter'; | |
5 | +import { FileItem } from '../types'; | |
6 | +import { createImgPreview } from '/@/components/Preview'; | |
4 | 7 | |
5 | 8 | export enum Level { |
6 | 9 | PROVINCE = 'PROVINCE', |
... | ... | @@ -179,14 +182,53 @@ export const schemas: FormSchema[] = [ |
179 | 182 | rules: phoneRule, |
180 | 183 | }, |
181 | 184 | { |
182 | - field: 'qrcode', | |
185 | + field: 'qrCode', | |
183 | 186 | label: '联系我们', |
184 | 187 | required: true, |
185 | - component: 'Select', | |
188 | + // component: 'Select', | |
189 | + // slot: 'qrcode', | |
190 | + component: 'ApiUpload', | |
191 | + changeEvent: 'update:fileList', | |
192 | + valueField: 'fileList', | |
186 | 193 | colProps: { |
187 | 194 | span: 24, |
188 | 195 | }, |
189 | - slot: 'qrcode', | |
196 | + helpMessage: ['支持.PNG、.JPG格式,建议尺寸为300*300px,大小不超过5M '], | |
197 | + componentProps: ({ formModel }) => { | |
198 | + return { | |
199 | + listType: 'picture-card', | |
200 | + maxFileLimit: 1, | |
201 | + accept: '.png,.jpg,.jpeg,.gif', | |
202 | + api: async (file: File) => { | |
203 | + try { | |
204 | + const formData = new FormData(); | |
205 | + formData.set('file', file); | |
206 | + const { fileStaticUri, fileName } = await uploadThumbnail(formData); | |
207 | + return { | |
208 | + uid: fileStaticUri, | |
209 | + name: fileName, | |
210 | + url: fileStaticUri, | |
211 | + } as FileItem; | |
212 | + } catch (error) { | |
213 | + return {}; | |
214 | + } | |
215 | + }, | |
216 | + // showUploadList: true, | |
217 | + onDownload() {}, | |
218 | + onPreview: (fileList: FileItem) => { | |
219 | + createImgPreview({ imageList: [fileList.url!] }); | |
220 | + }, | |
221 | + onDelete(url: string) { | |
222 | + formModel.deleteUrl = url!; | |
223 | + }, | |
224 | + }; | |
225 | + }, | |
226 | + }, | |
227 | + { | |
228 | + field: 'deleteUrl', | |
229 | + label: '', | |
230 | + component: 'Input', | |
231 | + show: false, | |
190 | 232 | }, |
191 | 233 | { |
192 | 234 | field: 'id', | ... | ... |
... | ... | @@ -3,7 +3,7 @@ |
3 | 3 | <Card :bordered="false" class="card"> |
4 | 4 | <div style="margin-left: -40px"> |
5 | 5 | <BasicForm @register="registerForm"> |
6 | - <template #logoUpload> | |
6 | + <!-- <template #logoUpload> | |
7 | 7 | <ContentUploadText> |
8 | 8 | <template #uploadImg> |
9 | 9 | <CustomUploadComp :imgUrl="logoPic" @setImg="handleSetLogoImgUrl" /> |
... | ... | @@ -24,7 +24,7 @@ |
24 | 24 | </div> |
25 | 25 | </template> |
26 | 26 | </ContentUploadText> |
27 | - </template> | |
27 | + </template> --> | |
28 | 28 | <template #colorInput="{ model, field }" |
29 | 29 | ><Input disabled v-model:value="model[field]"> |
30 | 30 | <template #prefix> <input type="color" v-model="model[field]" /> </template |
... | ... | @@ -41,6 +41,7 @@ |
41 | 41 | :customRequest="customUploadHomeSwiper" |
42 | 42 | :before-upload="beforeUploadHomeSwiper" |
43 | 43 | @change="handleChange" |
44 | + :remove="handleRemove" | |
44 | 45 | > |
45 | 46 | <div v-if="fileList.length < 5"> |
46 | 47 | <div> |
... | ... | @@ -74,7 +75,7 @@ |
74 | 75 | </template> |
75 | 76 | |
76 | 77 | <script lang="ts"> |
77 | - import { defineComponent, ref, unref, onMounted } from 'vue'; | |
78 | + import { defineComponent, ref, onMounted, unref } from 'vue'; | |
78 | 79 | import { BasicForm, useForm } from '/@/components/Form/index'; |
79 | 80 | import { Loading } from '/@/components/Loading/index'; |
80 | 81 | import { Card, Upload, Input, Modal } from 'ant-design-vue'; |
... | ... | @@ -86,7 +87,9 @@ |
86 | 87 | import { getAppDesign, updateAppDesign } from '/@/api/oem/index'; |
87 | 88 | import { Authority } from '/@/components/Authority'; |
88 | 89 | import ContentUploadText from './ContentUploadText.vue'; |
89 | - import { CustomUploadComp } from './customUplaod/index'; | |
90 | + // import { CustomUploadComp } from './customUplaod/index'; | |
91 | + import { buildUUID } from '/@/utils/uuid'; | |
92 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
90 | 93 | |
91 | 94 | export default defineComponent({ |
92 | 95 | components: { |
... | ... | @@ -99,7 +102,7 @@ |
99 | 102 | Modal, |
100 | 103 | Authority, |
101 | 104 | ContentUploadText, |
102 | - CustomUploadComp, | |
105 | + // CustomUploadComp, | |
103 | 106 | }, |
104 | 107 | setup() { |
105 | 108 | const loading = ref(false); |
... | ... | @@ -193,19 +196,42 @@ |
193 | 196 | } |
194 | 197 | }; |
195 | 198 | |
199 | + const deleteHomeUrl = ref<any[]>([]); | |
200 | + const handleRemove = (file: FileItem) => { | |
201 | + deleteHomeUrl.value.push(file.url); | |
202 | + }; | |
203 | + | |
196 | 204 | const handleUpdateInfo = async () => { |
197 | 205 | try { |
198 | 206 | const fieldValue = getFieldsValue(); |
207 | + if (Reflect.has(fieldValue, 'logo')) { | |
208 | + const file = (fieldValue.logo || []).at(0) || {}; | |
209 | + fieldValue.logo = file.url || ''; | |
210 | + } | |
211 | + if (Reflect.has(fieldValue, 'background')) { | |
212 | + const file = (fieldValue.background || []).at(0) || {}; | |
213 | + fieldValue.background = file.url || ''; | |
214 | + } | |
199 | 215 | // 做换字段 |
200 | 216 | const homeSwiper = fileList.value.map((item) => item.url); |
201 | 217 | const rotation = homeSwiper.join(','); |
202 | 218 | |
203 | 219 | compState.value.loading = true; |
220 | + if (Reflect.has(fieldValue, 'deleteLogoUrl') && fieldValue.deleteLogoUrl) { | |
221 | + await deleteFilePath(fieldValue?.deleteLogoUrl); | |
222 | + Reflect.deleteProperty(fieldValue, 'deleteLogoUrl'); | |
223 | + } | |
224 | + if (Reflect.has(fieldValue, 'deleteBackgroundUrl') && fieldValue.deleteBackgroundUrl) { | |
225 | + await deleteFilePath(fieldValue?.deleteBackgroundUrl); | |
226 | + Reflect.deleteProperty(fieldValue, 'deleteBackgroundUrl'); | |
227 | + } | |
228 | + if (unref(deleteHomeUrl)?.length) { | |
229 | + await Promise.all(unref(deleteHomeUrl).map((item) => deleteFilePath(item))); | |
230 | + } | |
204 | 231 | await updateAppDesign({ |
205 | 232 | ...fieldValue, |
206 | - background: unref(bgPic), | |
207 | - icon: unref(bgPic), | |
208 | - logo: unref(logoPic), | |
233 | + background: fieldValue.background || '', | |
234 | + logo: fieldValue.logo || '', | |
209 | 235 | rotation, |
210 | 236 | }); |
211 | 237 | compState.value.loading = false; |
... | ... | @@ -227,9 +253,15 @@ |
227 | 253 | status: 'done', |
228 | 254 | }); |
229 | 255 | } |
256 | + if (res.logo) { | |
257 | + res.logo = [{ uid: buildUUID(), name: 'name', url: res.logo } as FileItem]; | |
258 | + } | |
259 | + if (res.background) { | |
260 | + res.background = [{ uid: buildUUID(), name: 'name', url: res.background } as FileItem]; | |
261 | + } | |
230 | 262 | setFieldsValue(res); |
231 | - logoPic.value = res.logo; | |
232 | - bgPic.value = res.background; | |
263 | + // logoPic.value = res.logo; | |
264 | + // bgPic.value = res.background; | |
233 | 265 | if (arr[0]?.url === '') return; |
234 | 266 | fileList.value = arr; |
235 | 267 | }); |
... | ... | @@ -252,8 +284,8 @@ |
252 | 284 | }); |
253 | 285 | } |
254 | 286 | setFieldsValue(res); |
255 | - logoPic.value = res.logo; | |
256 | - bgPic.value = res.background; | |
287 | + // logoPic.value = res.logo; | |
288 | + // bgPic.value = res.background; | |
257 | 289 | if (arr[0]?.url === '') return; |
258 | 290 | fileList.value = arr; |
259 | 291 | } catch (e) { |
... | ... | @@ -271,8 +303,8 @@ |
271 | 303 | customUploadHomeSwiper, |
272 | 304 | beforeUploadHomeSwiper, |
273 | 305 | handleChange, |
274 | - logoPic, | |
275 | - bgPic, | |
306 | + // logoPic, | |
307 | + // bgPic, | |
276 | 308 | previewVisible, |
277 | 309 | previewImage, |
278 | 310 | loading, |
... | ... | @@ -280,6 +312,7 @@ |
280 | 312 | handleResetInfo, |
281 | 313 | handleSetBgImgUrl, |
282 | 314 | handleSetLogoImgUrl, |
315 | + handleRemove, | |
283 | 316 | }; |
284 | 317 | }, |
285 | 318 | }); | ... | ... |
... | ... | @@ -74,13 +74,13 @@ |
74 | 74 | </template> |
75 | 75 | |
76 | 76 | <script lang="ts"> |
77 | - import { defineComponent, ref, onMounted, unref } from 'vue'; | |
77 | + import { defineComponent, ref, onMounted } from 'vue'; | |
78 | 78 | import { Card, Upload, Input, Spin } from 'ant-design-vue'; |
79 | 79 | import { BasicForm, useForm } from '/@/components/Form/index'; |
80 | 80 | import { schemas } from '../config/CVIDraw.config'; |
81 | 81 | import { Loading } from '/@/components/Loading/index'; |
82 | 82 | import { useMessage } from '/@/hooks/web/useMessage'; |
83 | - import type { FileItem } from '/@/components/Upload/src/typing'; | |
83 | + // import type { FileItem } from '/@/components/Upload/src/typing'; | |
84 | 84 | import { iconUpload, getPlatForm, updatePlatForm, resetPlateInfo } from '/@/api/oem/index'; |
85 | 85 | import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'; |
86 | 86 | import { useUserStore } from '/@/store/modules/user'; |
... | ... | @@ -88,6 +88,9 @@ |
88 | 88 | import { Authority } from '/@/components/Authority'; |
89 | 89 | import ContentUploadText from './ContentUploadText.vue'; |
90 | 90 | import { CustomUploadComp } from './customUplaod/index'; |
91 | + import { buildUUID } from '/@/utils/uuid'; | |
92 | + import { FileItem } from '../types'; | |
93 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
91 | 94 | |
92 | 95 | export default defineComponent({ |
93 | 96 | components: { |
... | ... | @@ -166,18 +169,42 @@ |
166 | 169 | const handleUpdateInfo = async () => { |
167 | 170 | try { |
168 | 171 | const fieldValue = getFieldsValue(); |
172 | + if (Reflect.has(fieldValue, 'logo')) { | |
173 | + const file = (fieldValue.logo || []).at(0) || {}; | |
174 | + fieldValue.logo = file.url || null; | |
175 | + } | |
176 | + if (Reflect.has(fieldValue, 'icon')) { | |
177 | + const file = (fieldValue.icon || []).at(0) || {}; | |
178 | + fieldValue.icon = file.url || null; | |
179 | + } | |
180 | + if (Reflect.has(fieldValue, 'background')) { | |
181 | + const file = (fieldValue.background || []).at(0) || {}; | |
182 | + fieldValue.background = file.url || null; | |
183 | + } | |
169 | 184 | compState.value.loading = true; |
170 | - const newFieldValue = { | |
185 | + if (Reflect.has(fieldValue, 'deleteLogoUrl') && fieldValue.deleteLogoUrl) { | |
186 | + await deleteFilePath(fieldValue?.deleteLogoUrl); | |
187 | + Reflect.deleteProperty(fieldValue, 'deleteLogoUrl'); | |
188 | + } | |
189 | + if (Reflect.has(fieldValue, 'deleteIconUrl') && fieldValue.deleteIconUrl) { | |
190 | + await deleteFilePath(fieldValue?.deleteIconUrl); | |
191 | + Reflect.deleteProperty(fieldValue, 'deleteIconUrl'); | |
192 | + } | |
193 | + if (Reflect.has(fieldValue, 'deleteBackgroundUrl') && fieldValue.deleteBackgroundUrl) { | |
194 | + await deleteFilePath(fieldValue?.deleteBackgroundUrl); | |
195 | + Reflect.deleteProperty(fieldValue, 'deleteBackgroundUrl'); | |
196 | + } | |
197 | + | |
198 | + await updatePlatForm({ | |
171 | 199 | ...fieldValue, |
172 | - logo: unref(logoPic), | |
173 | - icon: unref(iconPic), | |
174 | - background: unref(bgPic), | |
175 | - }; | |
176 | - await updatePlatForm(newFieldValue); | |
200 | + logo: fieldValue.logo || '', | |
201 | + icon: fieldValue.icon || '', | |
202 | + background: fieldValue.background || '', | |
203 | + }); | |
177 | 204 | compState.value.loading = false; |
178 | 205 | createMessage.success('保存信息成功'); |
179 | 206 | |
180 | - setPlatFormInfo(newFieldValue); | |
207 | + setPlatFormInfo(fieldValue); | |
181 | 208 | } catch (e) { |
182 | 209 | createMessage.error('保存信息失败'); |
183 | 210 | } |
... | ... | @@ -197,6 +224,15 @@ |
197 | 224 | |
198 | 225 | onMounted(async () => { |
199 | 226 | const res = await getPlatForm(); |
227 | + if (res.logo) { | |
228 | + res.logo = [{ uid: buildUUID(), name: 'name', url: res.logo }] as any; | |
229 | + } | |
230 | + if (res.icon) { | |
231 | + res.icon = [{ uid: buildUUID(), name: 'name', url: res.icon }] as any; | |
232 | + } | |
233 | + if (res.background) { | |
234 | + res.background = [{ uid: buildUUID(), name: 'name', url: res.background }] as any; | |
235 | + } | |
200 | 236 | setFieldsValue(res); |
201 | 237 | logoPic.value = res.logo; |
202 | 238 | iconPic.value = res.icon; | ... | ... |
... | ... | @@ -2,7 +2,7 @@ |
2 | 2 | <div class="card"> |
3 | 3 | <Card :bordered="false" class="card"> |
4 | 4 | <BasicForm @register="registerForm"> |
5 | - <template #qrcode> | |
5 | + <!-- <template #qrcode> | |
6 | 6 | <ContentUploadText> |
7 | 7 | <template #uploadImg> |
8 | 8 | <CustomUploadComp :imgUrl="qrcodePic" @setImg="handleSetCodeImgUrl" /> |
... | ... | @@ -11,7 +11,7 @@ |
11 | 11 | <div class="box-outline"> 支持.PNG、.JPG格式,建议尺寸为300*300px,大小不超过5M </div> |
12 | 12 | </template> |
13 | 13 | </ContentUploadText> |
14 | - </template> | |
14 | + </template> --> | |
15 | 15 | <template #customProv> |
16 | 16 | <BasicForm @register="registerCustomForm" /> |
17 | 17 | </template> |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | </template> |
32 | 32 | |
33 | 33 | <script lang="ts"> |
34 | - import { defineComponent, onMounted, ref, computed, watch } from 'vue'; | |
34 | + import { defineComponent, onMounted, ref, computed } from 'vue'; | |
35 | 35 | import { Card } from 'ant-design-vue'; |
36 | 36 | import { BasicForm, useForm } from '/@/components/Form/index'; |
37 | 37 | import { schemas, provSchemas } from '../config/enterPriseInfo.config'; |
... | ... | @@ -43,8 +43,11 @@ |
43 | 43 | import { Authority } from '/@/components/Authority'; |
44 | 44 | import { USER_INFO_KEY } from '/@/enums/cacheEnum'; |
45 | 45 | import { getAuthCache } from '/@/utils/auth'; |
46 | - import ContentUploadText from './ContentUploadText.vue'; | |
47 | - import { CustomUploadComp } from './customUplaod/index'; | |
46 | + // import ContentUploadText from './ContentUploadText.vue'; | |
47 | + // import { CustomUploadComp } from './customUplaod/index'; | |
48 | + import { buildUUID } from '/@/utils/uuid'; | |
49 | + import { FileItem } from '../types'; | |
50 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
48 | 51 | |
49 | 52 | export default defineComponent({ |
50 | 53 | components: { |
... | ... | @@ -52,8 +55,8 @@ |
52 | 55 | BasicForm, |
53 | 56 | Loading, |
54 | 57 | Authority, |
55 | - ContentUploadText, | |
56 | - CustomUploadComp, | |
58 | + // ContentUploadText, | |
59 | + // CustomUploadComp, | |
57 | 60 | }, |
58 | 61 | setup() { |
59 | 62 | const userInfo: any = getAuthCache(USER_INFO_KEY); |
... | ... | @@ -73,11 +76,8 @@ |
73 | 76 | loading: false, |
74 | 77 | tip: '拼命加载中...', |
75 | 78 | }); |
76 | - const [ | |
77 | - registerForm, | |
78 | - { getFieldsValue, setFieldsValue, validate, clearValidate, validateFields }, | |
79 | - ] = useForm({ | |
80 | - labelWidth: 80, | |
79 | + const [registerForm, { getFieldsValue, setFieldsValue, validate, clearValidate }] = useForm({ | |
80 | + labelWidth: 90, | |
81 | 81 | schemas, |
82 | 82 | showResetButton: false, |
83 | 83 | showSubmitButton: false, |
... | ... | @@ -100,26 +100,30 @@ |
100 | 100 | |
101 | 101 | const { createMessage } = useMessage(); |
102 | 102 | |
103 | - const qrcodePic = ref(); | |
104 | - const handleSetCodeImgUrl = (d) => { | |
105 | - qrcodePic.value = d; | |
106 | - }; | |
107 | - watch( | |
108 | - () => qrcodePic.value, | |
109 | - (newValue) => { | |
110 | - if (!newValue) validateFields(['qrcode']); | |
111 | - else clearValidate('qrcode'); | |
112 | - } | |
113 | - ); | |
103 | + // const qrcodePic = ref(); | |
104 | + // const handleSetCodeImgUrl = (d) => { | |
105 | + // qrcodePic.value = d; | |
106 | + // }; | |
107 | + // watch( | |
108 | + // () => qrcodePic.value, | |
109 | + // (newValue) => { | |
110 | + // if (!newValue) validateFields(['qrcode']); | |
111 | + // else clearValidate('qrcode'); | |
112 | + // } | |
113 | + // ); | |
114 | 114 | // 更新 |
115 | 115 | const handleUpdateInfo = async () => { |
116 | 116 | try { |
117 | 117 | const fieldsValue = getFieldsValue(); |
118 | 118 | const { nameTown } = getNameTown(); |
119 | + if (Reflect.has(fieldsValue, 'qrCode')) { | |
120 | + const file = (fieldsValue.qrCode || []).at(0) || {}; | |
121 | + fieldsValue.qrCode = file.url || null; | |
122 | + } | |
119 | 123 | const newFieldValue: any = { |
120 | 124 | ...fieldsValue, |
121 | 125 | codeTown: nameTown, |
122 | - qrCode: qrcodePic.value, | |
126 | + // qrCode: qrcodePic.value, | |
123 | 127 | }; |
124 | 128 | delete newFieldValue.nameProv; |
125 | 129 | delete newFieldValue.nameCity; |
... | ... | @@ -139,9 +143,9 @@ |
139 | 143 | 'id', |
140 | 144 | ]; |
141 | 145 | if (newFieldValue.qrCode == undefined || newFieldValue.qrCode == '') { |
142 | - validateArray.push('qrcode'); | |
146 | + validateArray.push('qrCode'); | |
143 | 147 | } else { |
144 | - const findExistIndex = validateArray.findIndex((o) => o == 'qrcode'); | |
148 | + const findExistIndex = validateArray.findIndex((o) => o == 'qrCode'); | |
145 | 149 | if (findExistIndex !== -1) { |
146 | 150 | validateArray.splice(findExistIndex, 1); |
147 | 151 | } |
... | ... | @@ -158,6 +162,10 @@ |
158 | 162 | const values = await validate(validateArray); |
159 | 163 | if (!values) return; |
160 | 164 | compState.value.loading = true; |
165 | + if (Reflect.has(newFieldValue, 'deleteUrl') && newFieldValue.deleteUrl) { | |
166 | + await deleteFilePath(newFieldValue?.deleteUrl); | |
167 | + Reflect.deleteProperty(newFieldValue, 'deleteUrl'); | |
168 | + } | |
161 | 169 | await updateEnterPriseDetail(newFieldValue); |
162 | 170 | createMessage.success('更新信息成功'); |
163 | 171 | setEnterPriseInfo(newFieldValue); |
... | ... | @@ -189,19 +197,22 @@ |
189 | 197 | }); |
190 | 198 | setFieldsValue({ nameCountry: codeCountry }); |
191 | 199 | } |
192 | - setFieldsValue(res); | |
193 | - qrcodePic.value = res.qrCode; | |
200 | + if (res.qrCode) { | |
201 | + res.qrCode = [{ uid: buildUUID(), name: 'name', url: res.qrCode } as FileItem]; | |
202 | + } | |
203 | + setFieldsValue({ ...res, qrcode: res.qrCode }); | |
204 | + // qrcodePic.value = res.qrCode; | |
194 | 205 | }); |
195 | 206 | |
196 | 207 | return { |
197 | 208 | registerForm, |
198 | 209 | compState, |
199 | - qrcodePic, | |
200 | 210 | handleUpdateInfo, |
201 | 211 | registerCustomForm, |
202 | 212 | loading, |
203 | 213 | isWhereAdmin, |
204 | - handleSetCodeImgUrl, | |
214 | + // qrcodePic, | |
215 | + // handleSetCodeImgUrl, | |
205 | 216 | }; |
206 | 217 | }, |
207 | 218 | }); | ... | ... |
... | ... | @@ -65,8 +65,22 @@ export const formSchema: FormSchema[] = [ |
65 | 65 | label: t('routes.common.common.sort'), //排序 |
66 | 66 | component: 'InputNumber', |
67 | 67 | required: true, |
68 | + dynamicRules: () => { | |
69 | + return [ | |
70 | + { | |
71 | + required: true, | |
72 | + validator: (_, value) => { | |
73 | + if (Number(value) < -2147483648 || Number(value) > 2147483647) { | |
74 | + return Promise.reject('取值范围是-2147483648到2147483647'); | |
75 | + } | |
76 | + return Promise.resolve(); | |
77 | + }, | |
78 | + }, | |
79 | + ]; | |
80 | + }, | |
68 | 81 | componentProps: { |
69 | - maxLength: 32, | |
82 | + min: -2147483648, | |
83 | + max: 2147483647, | |
70 | 84 | }, |
71 | 85 | }, |
72 | 86 | { | ... | ... |
... | ... | @@ -9,7 +9,7 @@ import { |
9 | 9 | import { PollCommandInput, ModeEnum } from '../PollCommandInput'; |
10 | 10 | import { DeviceProfileModel } from '/@/api/device/model/deviceModel'; |
11 | 11 | import { JSONEditorValidator } from '/@/components/CodeEditor/src/JSONEditor'; |
12 | -import { BooleanStringEnum, TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum'; | |
12 | +import { TimeUnitEnum, TimeUnitNameEnum } from '/@/enums/toolEnum'; | |
13 | 13 | import { dateUtil } from '/@/utils/dateUtil'; |
14 | 14 | import { ProductPicker, validateProductPicker } from '../ProductPicker'; |
15 | 15 | import { useGlobSetting } from '/@/hooks/setting'; |
... | ... | @@ -386,16 +386,15 @@ export const formSchemas: FormSchema[] = [ |
386 | 386 | component: 'RadioGroup', |
387 | 387 | label: '时间单位', |
388 | 388 | ifShow: ({ model }) => model[FormFieldsEnum.EXECUTE_TIME_TYPE] === ExecuteTimeTypeEnum.POLL, |
389 | - defaultValue: | |
390 | - disabledTaskCenterExecuteIntervalUnitSecond === BooleanStringEnum.TRUE | |
391 | - ? TimeUnitEnum.MINUTE | |
392 | - : TimeUnitEnum.SECOND, | |
389 | + defaultValue: disabledTaskCenterExecuteIntervalUnitSecond | |
390 | + ? TimeUnitEnum.MINUTE | |
391 | + : TimeUnitEnum.SECOND, | |
393 | 392 | componentProps: () => { |
394 | 393 | const options = [ |
395 | 394 | { label: TimeUnitNameEnum.MINUTE, value: TimeUnitEnum.MINUTE }, |
396 | 395 | { label: TimeUnitNameEnum.HOUR, value: TimeUnitEnum.HOUR }, |
397 | 396 | ]; |
398 | - if (disabledTaskCenterExecuteIntervalUnitSecond === BooleanStringEnum.FALSE) { | |
397 | + if (!disabledTaskCenterExecuteIntervalUnitSecond) { | |
399 | 398 | options.unshift({ label: TimeUnitNameEnum.SECOND, value: TimeUnitEnum.SECOND }); |
400 | 399 | } |
401 | 400 | return { | ... | ... |
... | ... | @@ -65,7 +65,7 @@ |
65 | 65 | unref(getRecord).id, |
66 | 66 | !unref(getRecord).state ? StateEnum.ENABLE : StateEnum.CLOSE |
67 | 67 | ); |
68 | - createMessage.success('更新状态成功'); | |
68 | + createMessage.success(`${unref(getRecord).state ? '禁用' : '启用'}成功`); | |
69 | 69 | props.reload?.(); |
70 | 70 | } catch (error) { |
71 | 71 | throw error; | ... | ... |
... | ... | @@ -423,7 +423,7 @@ export const speedSchema: FormSchema[] = [ |
423 | 423 | label: '传输租户信息', |
424 | 424 | colProps: { span: 12 }, |
425 | 425 | component: 'CreateSpeed', |
426 | - defaultValue: '0', | |
426 | + // defaultValue: '0', | |
427 | 427 | changeEvent: 'update:value', |
428 | 428 | componentProps: { |
429 | 429 | placeholder: '请输入(请输入数字)', |
... | ... | @@ -432,7 +432,6 @@ export const speedSchema: FormSchema[] = [ |
432 | 432 | }, |
433 | 433 | { |
434 | 434 | field: 'transportDeviceMsgRateLimit', |
435 | - defaultValue: '0', | |
436 | 435 | label: '传输设备信息', |
437 | 436 | colProps: { span: 12 }, |
438 | 437 | component: 'CreateSpeed', |
... | ... | @@ -443,7 +442,6 @@ export const speedSchema: FormSchema[] = [ |
443 | 442 | }, |
444 | 443 | { |
445 | 444 | field: 'transportTenantTelemetryMsgRateLimit', |
446 | - defaultValue: '0', | |
447 | 445 | label: '传输租户遥测消息', |
448 | 446 | colProps: { span: 12 }, |
449 | 447 | component: 'CreateSpeed', |
... | ... | @@ -454,7 +452,6 @@ export const speedSchema: FormSchema[] = [ |
454 | 452 | }, |
455 | 453 | { |
456 | 454 | field: 'transportDeviceTelemetryMsgRateLimit', |
457 | - defaultValue: '0', | |
458 | 455 | label: '传输设备遥测消息', |
459 | 456 | colProps: { span: 12 }, |
460 | 457 | component: 'CreateSpeed', |
... | ... | @@ -492,6 +489,7 @@ export const speedSchema: FormSchema[] = [ |
492 | 489 | placeholder: '请输入(请输入数字)', |
493 | 490 | title: '租户REST请求', |
494 | 491 | }, |
492 | + defaultValue: '', | |
495 | 493 | }, |
496 | 494 | { |
497 | 495 | field: 'customerServerRestLimitsConfiguration', |
... | ... | @@ -502,6 +500,7 @@ export const speedSchema: FormSchema[] = [ |
502 | 500 | placeholder: '请输入(请输入数字)', |
503 | 501 | title: '客户REST请求', |
504 | 502 | }, |
503 | + defaultValue: '', | |
505 | 504 | }, |
506 | 505 | { |
507 | 506 | field: 'tenantEntityExportRateLimit', |
... | ... | @@ -532,6 +531,7 @@ export const speedSchema: FormSchema[] = [ |
532 | 531 | placeholder: '请输入(请输入数字)', |
533 | 532 | title: '会话WS更新', |
534 | 533 | }, |
534 | + defaultValue: '', | |
535 | 535 | }, |
536 | 536 | { |
537 | 537 | field: 'cassandraQueryTenantRateLimitsConfiguration', |
... | ... | @@ -542,6 +542,7 @@ export const speedSchema: FormSchema[] = [ |
542 | 542 | placeholder: '请输入(请输入数字)', |
543 | 543 | title: '租户Cassandra查询', |
544 | 544 | }, |
545 | + defaultValue: '', | |
545 | 546 | }, |
546 | 547 | { |
547 | 548 | field: 'tenantNotificationRequestsRateLimit', | ... | ... |
... | ... | @@ -182,9 +182,9 @@ |
182 | 182 | createdTime: isUpdate ? unref(createTime) : Date.now(), |
183 | 183 | }; |
184 | 184 | |
185 | - const defaultInfo = { | |
186 | - default: unref(isUpdate) ? isDefault.value : false, | |
187 | - }; | |
185 | + // const defaultInfo = { | |
186 | + // default: unref(isUpdate) ? isDefault.value : false, | |
187 | + // }; | |
188 | 188 | Object.assign( |
189 | 189 | postAllData, |
190 | 190 | { |
... | ... | @@ -192,8 +192,8 @@ |
192 | 192 | }, |
193 | 193 | getValuesFormData, |
194 | 194 | id, |
195 | - createTime1, | |
196 | - defaultInfo | |
195 | + createTime1 | |
196 | + // defaultInfo | |
197 | 197 | ); |
198 | 198 | if (!unref(isUpdate)) { |
199 | 199 | delete postAllData.id; |
... | ... | @@ -206,14 +206,14 @@ |
206 | 206 | if (!unref(isUpdate)) { |
207 | 207 | await getAllFieldsFunc(); |
208 | 208 | // return; |
209 | - await saveTenantProfileApi(postAllData); | |
209 | + await saveTenantProfileApi({ ...postAllData, isolatedTbRuleEngine: null }); | |
210 | 210 | createMessage.success('租户配置新增成功'); |
211 | 211 | closeDrawer(); |
212 | 212 | emit('success'); |
213 | 213 | resetFields(); |
214 | 214 | } else { |
215 | 215 | await getAllFieldsFunc(true); |
216 | - await saveTenantProfileApi(postAllData); | |
216 | + await saveTenantProfileApi({ ...postAllData, isolatedTbRuleEngine: null }); | |
217 | 217 | createMessage.success('租户配置编辑成功'); |
218 | 218 | closeDrawer(); |
219 | 219 | emit('success'); | ... | ... |
... | ... | @@ -20,6 +20,7 @@ |
20 | 20 | import { useMessage } from '/@/hooks/web/useMessage'; |
21 | 21 | import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; |
22 | 22 | import { buildUUID } from '/@/utils/uuid'; |
23 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
23 | 24 | |
24 | 25 | export default defineComponent({ |
25 | 26 | name: 'TenantDrawer', |
... | ... | @@ -102,6 +103,10 @@ |
102 | 103 | entityType: 'TENANT_PROFILE', |
103 | 104 | }, |
104 | 105 | }; |
106 | + if (Reflect.has(values, 'deleteUrl') && values.deleteUrl) { | |
107 | + await deleteFilePath(values?.deleteUrl); | |
108 | + Reflect.deleteProperty(values, 'deleteUrl'); | |
109 | + } | |
105 | 110 | await updateOrCreateTenant(req); |
106 | 111 | emit('success'); |
107 | 112 | createMessage.success(`${unref(isUpdate) ? '编辑' : '新增'}成功`); | ... | ... |
... | ... | @@ -83,7 +83,7 @@ export const tenantFormSchema: FormSchema[] = [ |
83 | 83 | component: 'ApiUpload', |
84 | 84 | changeEvent: 'update:fileList', |
85 | 85 | valueField: 'fileList', |
86 | - componentProps: () => { | |
86 | + componentProps: ({ formModel }) => { | |
87 | 87 | return { |
88 | 88 | listType: 'picture-card', |
89 | 89 | maxFileLimit: 1, |
... | ... | @@ -105,10 +105,19 @@ export const tenantFormSchema: FormSchema[] = [ |
105 | 105 | onPreview: (fileList: FileItem) => { |
106 | 106 | createImgPreview({ imageList: [fileList.url!] }); |
107 | 107 | }, |
108 | + onDelete(url: string) { | |
109 | + formModel.deleteUrl = url!; | |
110 | + }, | |
108 | 111 | }; |
109 | 112 | }, |
110 | 113 | }, |
111 | 114 | { |
115 | + field: 'deleteUrl', | |
116 | + label: '', | |
117 | + component: 'Input', | |
118 | + show: false, | |
119 | + }, | |
120 | + { | |
112 | 121 | field: 'name', |
113 | 122 | label: '租户名称', |
114 | 123 | required: true, | ... | ... |
... | ... | @@ -56,7 +56,7 @@ |
56 | 56 | </script> |
57 | 57 | |
58 | 58 | <template> |
59 | - <BasicModal @register="register" title="组件设置" @ok="handleOk"> | |
59 | + <BasicModal @register="register" title="组件设置" @ok="handleOk" :width="700"> | |
60 | 60 | <!-- --> |
61 | 61 | <component ref="settingFormEl" :is="getSettingComponent" /> |
62 | 62 | </BasicModal> | ... | ... |
... | ... | @@ -29,6 +29,8 @@ |
29 | 29 | import { MessageAlert } from './components/MessageAlert'; |
30 | 30 | import { createSelectWidgetKeysContext, createSelectWidgetModeContext } from './useContext'; |
31 | 31 | import { useGetCategoryByComponentKey } from '../packages/hook/useGetCategoryByComponentKey'; |
32 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
33 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
32 | 34 | |
33 | 35 | const props = defineProps<{ |
34 | 36 | layout: Layout[]; |
... | ... | @@ -227,6 +229,20 @@ |
227 | 229 | return `${category} / ${componentConfig.title}`; |
228 | 230 | }); |
229 | 231 | |
232 | + const countElementOccurrences = (arr) => { | |
233 | + const countMap = {}; | |
234 | + | |
235 | + arr.forEach((element) => { | |
236 | + if (countMap[element]) { | |
237 | + countMap[element]++; | |
238 | + } else { | |
239 | + countMap[element] = 1; | |
240 | + } | |
241 | + }); | |
242 | + | |
243 | + return countMap; | |
244 | + }; | |
245 | + | |
230 | 246 | const handleSubmit = async () => { |
231 | 247 | const validateResult = await validate(); |
232 | 248 | if (validateResult && !validateResult.flag) { |
... | ... | @@ -241,6 +257,59 @@ |
241 | 257 | } |
242 | 258 | } |
243 | 259 | const value = getFormValues(); |
260 | + const { record } = value || {}; | |
261 | + try { | |
262 | + const currentRecordIconUrl = ref<any>([]); | |
263 | + // 判断当前自定义组件表单以前的自定义图片呢url | |
264 | + currentRecord.value?.dataSource.forEach((item) => { | |
265 | + if (item.componentInfo?.customIcon) { | |
266 | + item.componentInfo?.customIcon?.forEach((icon: FileItem) => { | |
267 | + currentRecordIconUrl.value.push(icon.url); | |
268 | + }); | |
269 | + } | |
270 | + }); | |
271 | + // 取当前修改过后的自定义图片url | |
272 | + const dataSourceUrl = record.dataSource?.map( | |
273 | + (item) => item.componentInfo.customIcon?.[0].url | |
274 | + ); | |
275 | + | |
276 | + // 当前自定义组件取出要进行删除的图标url | |
277 | + const dataSourceDeleteUrl = unref(currentRecordIconUrl).filter( | |
278 | + (item) => !dataSourceUrl?.includes(item) | |
279 | + ); | |
280 | + | |
281 | + //查询外部所有组件的自定义图标的url | |
282 | + const oldDataSource = props.layout; | |
283 | + const customIconUrls = ref<any>([]); | |
284 | + oldDataSource?.forEach((item: any) => { | |
285 | + item.dataSource?.forEach((dataSource) => { | |
286 | + if (dataSource.componentInfo?.customIcon) { | |
287 | + dataSource.componentInfo?.customIcon?.forEach((icon: FileItem) => { | |
288 | + customIconUrls.value.push(icon.url); | |
289 | + }); | |
290 | + } | |
291 | + }); | |
292 | + }); | |
293 | + // const dataSourceDeleteUrl = record.dataSource?.map((item) => item.componentInfo.deleteUrl); | |
294 | + | |
295 | + if (unref(customIconUrls) && unref(customIconUrls).length && dataSourceDeleteUrl?.length) { | |
296 | + // 判断外部所有组件是否有dataSourceDeleteUrl使用中的url | |
297 | + const deletePromise = unref(customIconUrls)?.filter((item) => | |
298 | + dataSourceDeleteUrl?.includes(item) | |
299 | + ); | |
300 | + const deleteUrlInfo = countElementOccurrences(deletePromise); | |
301 | + const deleteUrl = deletePromise?.filter((item) => deleteUrlInfo?.[item] == 1); | |
302 | + Promise.all( | |
303 | + deleteUrl.map((item) => { | |
304 | + deleteFilePath(item); | |
305 | + }) | |
306 | + ); | |
307 | + } | |
308 | + } catch (err) { | |
309 | + // eslint-disable-next-line no-console | |
310 | + console.log(err); | |
311 | + } | |
312 | + | |
244 | 313 | try { |
245 | 314 | loading.value = true; |
246 | 315 | unref(currentMode) === DataActionModeEnum.UPDATE | ... | ... |
... | ... | @@ -3,15 +3,68 @@ |
3 | 3 | import { useForm, BasicForm } from '/@/components/Form'; |
4 | 4 | import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; |
5 | 5 | import { option } from './config'; |
6 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
7 | + import { createImgPreview } from '/@/components/Preview'; | |
8 | + import { upload } from '/@/api/oss/ossFileUploader'; | |
6 | 9 | |
7 | 10 | const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ |
8 | 11 | schemas: [ |
9 | 12 | { |
13 | + field: ComponentConfigFieldEnum.FONT_SIZE, | |
14 | + label: '文本字体大小', | |
15 | + component: 'InputNumber', | |
16 | + defaultValue: 14, | |
17 | + componentProps: { | |
18 | + min: 0, | |
19 | + max: 100, | |
20 | + formatter: (e) => { | |
21 | + const value = e?.toString().replace(/^0/g, ''); | |
22 | + if (value) { | |
23 | + return value.replace(/^0/g, ''); | |
24 | + } else { | |
25 | + return 0; | |
26 | + } | |
27 | + }, | |
28 | + }, | |
29 | + }, | |
30 | + { | |
31 | + field: ComponentConfigFieldEnum.PASS_WORD, | |
32 | + label: '操作密码', | |
33 | + component: 'InputPassword', | |
34 | + defaultValue: '', | |
35 | + }, | |
36 | + { | |
37 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
38 | + label: '显示设备名称', | |
39 | + component: 'Checkbox', | |
40 | + defaultValue: option.showDeviceName, | |
41 | + }, | |
42 | + { | |
43 | + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM, | |
44 | + label: '图标类型', | |
45 | + component: 'RadioGroup', | |
46 | + defaultValue: 'default', | |
47 | + componentProps: ({ formModel }) => { | |
48 | + return { | |
49 | + options: [ | |
50 | + { label: '系统默认', value: 'default' }, | |
51 | + { label: '自定义', value: 'custom' }, | |
52 | + ], | |
53 | + onChange() { | |
54 | + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = []; | |
55 | + }, | |
56 | + }; | |
57 | + }, | |
58 | + }, | |
59 | + { | |
10 | 60 | field: ComponentConfigFieldEnum.ICON_COLOR, |
11 | 61 | label: '图标颜色', |
12 | 62 | component: 'ColorPicker', |
13 | 63 | changeEvent: 'update:value', |
14 | 64 | defaultValue: option.iconColor, |
65 | + ifShow: ({ model }) => { | |
66 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
67 | + }, | |
15 | 68 | }, |
16 | 69 | { |
17 | 70 | field: ComponentConfigFieldEnum.ICON, |
... | ... | @@ -19,6 +72,9 @@ |
19 | 72 | component: 'IconDrawer', |
20 | 73 | changeEvent: 'update:value', |
21 | 74 | defaultValue: option.icon, |
75 | + ifShow: ({ model }) => { | |
76 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
77 | + }, | |
22 | 78 | componentProps({ formModel }) { |
23 | 79 | const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; |
24 | 80 | return { |
... | ... | @@ -27,34 +83,51 @@ |
27 | 83 | }, |
28 | 84 | }, |
29 | 85 | { |
30 | - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
31 | - label: '显示设备名称', | |
32 | - component: 'Checkbox', | |
33 | - defaultValue: option.showDeviceName, | |
34 | - }, | |
35 | - { | |
36 | - field: ComponentConfigFieldEnum.FONT_SIZE, | |
37 | - label: '文本字体大小', | |
38 | - component: 'InputNumber', | |
39 | - defaultValue: 14, | |
40 | - componentProps: { | |
41 | - min: 0, | |
42 | - max: 100, | |
43 | - formatter: (e) => { | |
44 | - const value = e?.toString().replace(/^0/g, ''); | |
45 | - if (value) { | |
46 | - return value.replace(/^0/g, ''); | |
47 | - } else { | |
48 | - return 0; | |
49 | - } | |
50 | - }, | |
86 | + field: ComponentConfigFieldEnum.CUSTOM_ICON, | |
87 | + label: '图标', | |
88 | + component: 'ApiUpload', | |
89 | + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom', | |
90 | + changeEvent: 'update:fileList', | |
91 | + valueField: 'fileList', | |
92 | + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '], | |
93 | + componentProps: ({ formModel }) => { | |
94 | + return { | |
95 | + listType: 'picture-card', | |
96 | + maxFileLimit: 1, | |
97 | + maxSize: 50 * 1024, | |
98 | + accept: '.svg', | |
99 | + api: async (file: File) => { | |
100 | + try { | |
101 | + const formData = new FormData(); | |
102 | + const { name } = file; | |
103 | + formData.set('file', file); | |
104 | + const { fileStaticUri, fileName } = await upload(formData); | |
105 | + return { | |
106 | + uid: fileStaticUri, | |
107 | + name: name || fileName, | |
108 | + url: fileStaticUri, | |
109 | + } as FileItem; | |
110 | + } catch (error) { | |
111 | + return {}; | |
112 | + } | |
113 | + }, | |
114 | + // showUploadList: true, | |
115 | + onDownload() {}, | |
116 | + onPreview: (fileList: FileItem) => { | |
117 | + createImgPreview({ imageList: [fileList.url!] }); | |
118 | + }, | |
119 | + | |
120 | + onDelete(url: string) { | |
121 | + formModel.deleteUrl = url!; | |
122 | + }, | |
123 | + }; | |
51 | 124 | }, |
52 | 125 | }, |
53 | 126 | { |
54 | - field: ComponentConfigFieldEnum.PASS_WORD, | |
55 | - label: '操作密码', | |
56 | - component: 'InputPassword', | |
57 | - defaultValue: '', | |
127 | + field: 'deleteUrl', | |
128 | + label: '', | |
129 | + component: 'Input', | |
130 | + show: false, | |
58 | 131 | }, |
59 | 132 | ], |
60 | 133 | showActionButtonGroup: false, | ... | ... |
... | ... | @@ -31,7 +31,7 @@ |
31 | 31 | fontSize: persetFontSize, |
32 | 32 | password: persetPassword, |
33 | 33 | } = persetOption || {}; |
34 | - const { icon, iconColor, fontSize, password } = componentInfo || {}; | |
34 | + const { icon, iconColor, fontSize, password, customIcon, defaultCustom } = componentInfo || {}; | |
35 | 35 | |
36 | 36 | const tsl = getDeviceProfileTslByIdWithIdentifier?.(deviceProfileId, attribute); |
37 | 37 | return { |
... | ... | @@ -41,6 +41,8 @@ |
41 | 41 | fontSize: fontSize || persetFontSize || 14, |
42 | 42 | password: password || persetPassword, |
43 | 43 | commandType, |
44 | + defaultCustom: defaultCustom || 'default', | |
45 | + customIcon: customIcon || [], | |
44 | 46 | }; |
45 | 47 | }); |
46 | 48 | |
... | ... | @@ -81,10 +83,17 @@ |
81 | 83 | <main class="w-full h-full flex justify-around items-center" :style="getScale"> |
82 | 84 | <div class="flex flex-col justify-center items-center"> |
83 | 85 | <SvgIcon |
84 | - :name="getDesign.icon" | |
86 | + v-if="getDesign.defaultCustom !== 'custom'" | |
87 | + :name="getDesign.icon!" | |
85 | 88 | prefix="iconfont" |
86 | - :style="{ color: getDesign.iconColor }" | |
87 | 89 | :size="getRatio ? getRatio * 60 : 60" |
90 | + :style="{ color: getDesign.iconColor }" | |
91 | + /> | |
92 | + <img | |
93 | + v-else | |
94 | + :src="getDesign.customIcon[0]?.url" | |
95 | + :style="{ width: getRatio ? getRatio * 60 + 'px' : '60px' }" | |
96 | + :alt="getDesign.customIcon[0]?.name" | |
88 | 97 | /> |
89 | 98 | <span |
90 | 99 | class="mt-3 truncate text-gray-500 text-center" | ... | ... |
... | ... | @@ -62,29 +62,31 @@ |
62 | 62 | }; |
63 | 63 | }); |
64 | 64 | |
65 | - const sendValue = ref(0); | |
66 | - const handleChange = async (value: number) => { | |
67 | - sendValue.value = value; | |
68 | - }; | |
65 | + // const sendValue = ref(0); | |
66 | + const isChangeValue = ref<number | string>(); //保留以前的值 报错的时候要取以前的值 | |
67 | + const handleChange = async () => {}; | |
69 | 68 | |
70 | 69 | const { loading, doControlSendCommand } = useControlComand(); |
71 | 70 | |
72 | - const handleAfterChange = () => { | |
71 | + const handleAfterChange = (value) => { | |
73 | 72 | if (unref(getDesign).password) { |
74 | - openModal(true, { password: unref(getDesign).password }); | |
73 | + openModal(true, { password: unref(getDesign).password, value: value }); | |
75 | 74 | unref(sliderElRef)?.blur(); |
76 | 75 | return; |
77 | 76 | } |
78 | - handleSendCommand(); | |
77 | + handleSendCommand(value); | |
79 | 78 | }; |
80 | 79 | |
81 | - const handleSendCommand = async () => { | |
80 | + const handleSendCommand = async (value) => { | |
82 | 81 | if (props.config.option.mode === ComponentMode.SELECT_PREVIEW) return; |
83 | - const value = unref(sendValue); | |
84 | 82 | const { option } = props.config || {}; |
85 | 83 | const result = await doControlSendCommand(option, value); |
86 | 84 | unref(sliderElRef)?.blur(); |
87 | - sliderValue.value = result ? value : unref(sliderValue); | |
85 | + sliderValue.value = result ? value : unref(isChangeValue); | |
86 | + | |
87 | + if (result) { | |
88 | + isChangeValue.value = sliderValue.value; | |
89 | + } | |
88 | 90 | }; |
89 | 91 | |
90 | 92 | const { getNumberValue } = useReceiveValue(); |
... | ... | @@ -94,6 +96,7 @@ |
94 | 96 | const [name, value] = latest; |
95 | 97 | if (!name && !value) return; |
96 | 98 | sliderValue.value = getNumberValue(value); |
99 | + isChangeValue.value = getNumberValue(value); | |
97 | 100 | }; |
98 | 101 | |
99 | 102 | useDataFetch(props, updateFn); |
... | ... | @@ -125,7 +128,7 @@ |
125 | 128 | '--slider-top': -(getRatio ? getRatio * 5 : 5) + 'px', |
126 | 129 | }" |
127 | 130 | class="no-drag" |
128 | - :value="sliderValue" | |
131 | + v-model:value="sliderValue" | |
129 | 132 | :min="getDesign.minNumber" |
130 | 133 | :max="getDesign.maxNumber" |
131 | 134 | @change="handleChange" | ... | ... |
... | ... | @@ -3,30 +3,13 @@ |
3 | 3 | import { useForm, BasicForm } from '/@/components/Form'; |
4 | 4 | import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; |
5 | 5 | import { option } from './config'; |
6 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
7 | + import { createImgPreview } from '/@/components/Preview'; | |
8 | + import { upload } from '/@/api/oss/ossFileUploader'; | |
6 | 9 | |
7 | 10 | const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ |
8 | 11 | schemas: [ |
9 | 12 | { |
10 | - field: ComponentConfigFieldEnum.ICON_COLOR, | |
11 | - label: '图标颜色', | |
12 | - component: 'ColorPicker', | |
13 | - changeEvent: 'update:value', | |
14 | - defaultValue: option.iconColor, | |
15 | - }, | |
16 | - { | |
17 | - field: ComponentConfigFieldEnum.ICON, | |
18 | - label: '图标', | |
19 | - component: 'IconDrawer', | |
20 | - changeEvent: 'update:value', | |
21 | - defaultValue: option.icon, | |
22 | - componentProps({ formModel }) { | |
23 | - const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; | |
24 | - return { | |
25 | - color, | |
26 | - }; | |
27 | - }, | |
28 | - }, | |
29 | - { | |
30 | 13 | field: ComponentConfigFieldEnum.FONT_SIZE, |
31 | 14 | label: '文本字体大小', |
32 | 15 | component: 'InputNumber', |
... | ... | @@ -45,16 +28,106 @@ |
45 | 28 | }, |
46 | 29 | }, |
47 | 30 | { |
31 | + field: ComponentConfigFieldEnum.PASS_WORD, | |
32 | + label: '操作密码', | |
33 | + component: 'InputPassword', | |
34 | + defaultValue: '', | |
35 | + }, | |
36 | + { | |
48 | 37 | field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, |
49 | 38 | label: '显示设备名称', |
50 | 39 | component: 'Checkbox', |
51 | 40 | defaultValue: option.showDeviceName, |
52 | 41 | }, |
53 | 42 | { |
54 | - field: ComponentConfigFieldEnum.PASS_WORD, | |
55 | - label: '操作密码', | |
56 | - component: 'InputPassword', | |
57 | - defaultValue: '', | |
43 | + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM, | |
44 | + label: '图标类型', | |
45 | + component: 'RadioGroup', | |
46 | + defaultValue: 'default', | |
47 | + componentProps: ({ formModel }) => { | |
48 | + return { | |
49 | + options: [ | |
50 | + { label: '系统默认', value: 'default' }, | |
51 | + { label: '自定义', value: 'custom' }, | |
52 | + ], | |
53 | + onChange() { | |
54 | + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = []; | |
55 | + }, | |
56 | + }; | |
57 | + }, | |
58 | + }, | |
59 | + { | |
60 | + field: ComponentConfigFieldEnum.ICON_COLOR, | |
61 | + label: '图标颜色', | |
62 | + component: 'ColorPicker', | |
63 | + changeEvent: 'update:value', | |
64 | + defaultValue: option.iconColor, | |
65 | + ifShow: ({ model }) => { | |
66 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
67 | + }, | |
68 | + }, | |
69 | + { | |
70 | + field: ComponentConfigFieldEnum.ICON, | |
71 | + label: '图标', | |
72 | + component: 'IconDrawer', | |
73 | + changeEvent: 'update:value', | |
74 | + defaultValue: option.icon, | |
75 | + ifShow: ({ model }) => { | |
76 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
77 | + }, | |
78 | + componentProps({ formModel }) { | |
79 | + const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; | |
80 | + return { | |
81 | + color, | |
82 | + }; | |
83 | + }, | |
84 | + }, | |
85 | + { | |
86 | + field: ComponentConfigFieldEnum.CUSTOM_ICON, | |
87 | + label: '图标', | |
88 | + component: 'ApiUpload', | |
89 | + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom', | |
90 | + changeEvent: 'update:fileList', | |
91 | + valueField: 'fileList', | |
92 | + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '], | |
93 | + componentProps: ({ formModel }) => { | |
94 | + return { | |
95 | + listType: 'picture-card', | |
96 | + maxSize: 50 * 1024, | |
97 | + maxFileLimit: 1, | |
98 | + accept: '.svg', | |
99 | + api: async (file: File) => { | |
100 | + try { | |
101 | + const formData = new FormData(); | |
102 | + const { name } = file; | |
103 | + formData.set('file', file); | |
104 | + const { fileStaticUri, fileName } = await upload(formData); | |
105 | + return { | |
106 | + uid: fileStaticUri, | |
107 | + name: name || fileName, | |
108 | + url: fileStaticUri, | |
109 | + } as FileItem; | |
110 | + } catch (error) { | |
111 | + return {}; | |
112 | + } | |
113 | + }, | |
114 | + // showUploadList: true, | |
115 | + onDownload() {}, | |
116 | + onPreview: (fileList: FileItem) => { | |
117 | + createImgPreview({ imageList: [fileList.url!] }); | |
118 | + }, | |
119 | + | |
120 | + onDelete(url: string) { | |
121 | + formModel.deleteUrl = url!; | |
122 | + }, | |
123 | + }; | |
124 | + }, | |
125 | + }, | |
126 | + { | |
127 | + field: 'deleteUrl', | |
128 | + label: '', | |
129 | + component: 'Input', | |
130 | + show: false, | |
58 | 131 | }, |
59 | 132 | ], |
60 | 133 | showActionButtonGroup: false, | ... | ... |
... | ... | @@ -38,8 +38,17 @@ |
38 | 38 | } = persetOption || {}; |
39 | 39 | return { |
40 | 40 | dataSource: dataSource.map((item) => { |
41 | - const { fontColor, icon, iconColor, unit, showDeviceName, password, fontSize } = | |
42 | - item.componentInfo; | |
41 | + const { | |
42 | + fontColor, | |
43 | + icon, | |
44 | + iconColor, | |
45 | + unit, | |
46 | + showDeviceName, | |
47 | + password, | |
48 | + fontSize, | |
49 | + customIcon, | |
50 | + defaultCustom, | |
51 | + } = item.componentInfo; | |
43 | 52 | const { |
44 | 53 | attribute, |
45 | 54 | attributeRename, |
... | ... | @@ -76,6 +85,8 @@ |
76 | 85 | closeCommand, |
77 | 86 | openService, |
78 | 87 | closeService, |
88 | + defaultCustom: defaultCustom || 'default', | |
89 | + customIcon: customIcon || [], | |
79 | 90 | } as SwitchItemType; |
80 | 91 | }), |
81 | 92 | }; |
... | ... | @@ -128,11 +139,24 @@ |
128 | 139 | :key="item.id" |
129 | 140 | class="flex justify-between items-center w-full px-4" |
130 | 141 | > |
131 | - <SvgIcon | |
142 | + <!-- <SvgIcon | |
132 | 143 | :name="item.icon!" |
133 | 144 | prefix="iconfont" |
134 | 145 | :size="getRatio ? 30 * getRatio : 30" |
135 | 146 | :style="{ color: item.iconColor }" |
147 | + /> --> | |
148 | + <SvgIcon | |
149 | + v-if="item.defaultCustom !== 'custom'" | |
150 | + :name="item.icon!" | |
151 | + prefix="iconfont" | |
152 | + :size="getRatio ? getRatio * 30 : 30" | |
153 | + :style="{ color: item.iconColor }" | |
154 | + /> | |
155 | + <img | |
156 | + v-else | |
157 | + :src="item.customIcon[0]?.url" | |
158 | + :style="{ width: getRatio ? getRatio * 30 + 'px' : '30px' }" | |
159 | + :alt="item.customIcon[0]?.name" | |
136 | 160 | /> |
137 | 161 | <div |
138 | 162 | class="text-gray-500 truncate mx-2" | ... | ... |
... | ... | @@ -30,16 +30,17 @@ |
30 | 30 | const { config } = props; |
31 | 31 | const { option } = config; |
32 | 32 | const { videoConfig, uuid } = option || {}; |
33 | - const { type, url } = await getPlayUrl({ | |
34 | - id: videoConfig?.id, | |
35 | - accessMode: videoConfig?.accessMode, | |
36 | - playProtocol: videoConfig?.playProtocol, | |
37 | - videoUrl: videoConfig?.videoUrl, | |
38 | - params: { | |
39 | - deviceId: videoConfig?.deviceId, | |
40 | - channelNo: videoConfig?.channelId, | |
41 | - }, | |
42 | - } as unknown as CameraRecord); | |
33 | + const { type, url } = | |
34 | + (await getPlayUrl({ | |
35 | + id: videoConfig?.id, | |
36 | + accessMode: videoConfig?.accessMode, | |
37 | + playProtocol: videoConfig?.playProtocol, | |
38 | + videoUrl: videoConfig?.videoUrl, | |
39 | + params: { | |
40 | + deviceId: videoConfig?.deviceId, | |
41 | + channelNo: videoConfig?.channelId, | |
42 | + }, | |
43 | + } as unknown as CameraRecord)) || {}; | |
43 | 44 | playType.value = type; |
44 | 45 | playUrl.value = url; |
45 | 46 | if (!uuid) return; | ... | ... |
... | ... | @@ -3,15 +3,68 @@ |
3 | 3 | import { useForm, BasicForm } from '/@/components/Form'; |
4 | 4 | import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; |
5 | 5 | import { option } from './config'; |
6 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
7 | + import { createImgPreview } from '/@/components/Preview'; | |
8 | + import { upload } from '/@/api/oss/ossFileUploader'; | |
6 | 9 | |
7 | 10 | const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ |
8 | 11 | schemas: [ |
9 | 12 | { |
13 | + field: ComponentConfigFieldEnum.FONT_SIZE, | |
14 | + label: '文本字体大小', | |
15 | + component: 'InputNumber', | |
16 | + defaultValue: 14, | |
17 | + componentProps: { | |
18 | + min: 0, | |
19 | + max: 100, | |
20 | + formatter: (e) => { | |
21 | + const value = e?.toString().replace(/^0/g, ''); | |
22 | + if (value) { | |
23 | + return value.replace(/^0/g, ''); | |
24 | + } else { | |
25 | + return 0; | |
26 | + } | |
27 | + }, | |
28 | + }, | |
29 | + }, | |
30 | + { | |
31 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
32 | + label: '显示设备名称', | |
33 | + component: 'Checkbox', | |
34 | + defaultValue: option.showDeviceName, | |
35 | + }, | |
36 | + { | |
37 | + field: ComponentConfigFieldEnum.SHOW_TIME, | |
38 | + label: '显示时间', | |
39 | + component: 'Checkbox', | |
40 | + defaultValue: option.showTime, | |
41 | + }, | |
42 | + { | |
43 | + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM, | |
44 | + label: '图标类型', | |
45 | + component: 'RadioGroup', | |
46 | + defaultValue: 'default', | |
47 | + componentProps: ({ formModel }) => { | |
48 | + return { | |
49 | + options: [ | |
50 | + { label: '系统默认', value: 'default' }, | |
51 | + { label: '自定义', value: 'custom' }, | |
52 | + ], | |
53 | + onChange() { | |
54 | + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = []; | |
55 | + }, | |
56 | + }; | |
57 | + }, | |
58 | + }, | |
59 | + { | |
10 | 60 | field: ComponentConfigFieldEnum.ICON, |
11 | 61 | label: '开启状态图标', |
12 | 62 | component: 'IconDrawer', |
13 | 63 | changeEvent: 'update:value', |
14 | 64 | defaultValue: option.icon, |
65 | + ifShow: ({ model }) => { | |
66 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
67 | + }, | |
15 | 68 | componentProps({ formModel }) { |
16 | 69 | const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; |
17 | 70 | return { |
... | ... | @@ -24,6 +77,9 @@ |
24 | 77 | label: '开启图标颜色', |
25 | 78 | component: 'ColorPicker', |
26 | 79 | changeEvent: 'update:value', |
80 | + ifShow: ({ model }) => { | |
81 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
82 | + }, | |
27 | 83 | defaultValue: option.iconColor, |
28 | 84 | }, |
29 | 85 | { |
... | ... | @@ -32,6 +88,9 @@ |
32 | 88 | component: 'IconDrawer', |
33 | 89 | changeEvent: 'update:value', |
34 | 90 | defaultValue: option.iconClose, |
91 | + ifShow: ({ model }) => { | |
92 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
93 | + }, | |
35 | 94 | componentProps({ formModel }) { |
36 | 95 | const color = formModel[ComponentConfigFieldEnum.ICON_COLOR_CLOSE]; |
37 | 96 | return { |
... | ... | @@ -44,37 +103,84 @@ |
44 | 103 | label: '关闭图标颜色', |
45 | 104 | component: 'ColorPicker', |
46 | 105 | changeEvent: 'update:value', |
106 | + ifShow: ({ model }) => { | |
107 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
108 | + }, | |
47 | 109 | defaultValue: option.iconColorClose, |
48 | 110 | }, |
49 | 111 | { |
50 | - field: ComponentConfigFieldEnum.FONT_SIZE, | |
51 | - label: '文本字体大小', | |
52 | - component: 'InputNumber', | |
53 | - defaultValue: 14, | |
54 | - componentProps: { | |
55 | - min: 0, | |
56 | - max: 100, | |
57 | - formatter: (e) => { | |
58 | - const value = e?.toString().replace(/^0/g, ''); | |
59 | - if (value) { | |
60 | - return value.replace(/^0/g, ''); | |
61 | - } else { | |
62 | - return 0; | |
63 | - } | |
64 | - }, | |
112 | + field: ComponentConfigFieldEnum.CUSTOM_ICON, | |
113 | + label: '开启状态图标', | |
114 | + component: 'ApiUpload', | |
115 | + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom', | |
116 | + changeEvent: 'update:fileList', | |
117 | + valueField: 'fileList', | |
118 | + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '], | |
119 | + componentProps: ({}) => { | |
120 | + return { | |
121 | + listType: 'picture-card', | |
122 | + maxSize: 50 * 1024, | |
123 | + maxFileLimit: 1, | |
124 | + accept: '.svg', | |
125 | + api: async (file: File) => { | |
126 | + try { | |
127 | + const formData = new FormData(); | |
128 | + const { name } = file; | |
129 | + formData.set('file', file); | |
130 | + const { fileStaticUri, fileName } = await upload(formData); | |
131 | + return { | |
132 | + uid: fileStaticUri, | |
133 | + name: name || fileName, | |
134 | + url: fileStaticUri, | |
135 | + } as FileItem; | |
136 | + } catch (error) { | |
137 | + return {}; | |
138 | + } | |
139 | + }, | |
140 | + // showUploadList: true, | |
141 | + onDownload() {}, | |
142 | + onPreview: (fileList: FileItem) => { | |
143 | + createImgPreview({ imageList: [fileList.url!] }); | |
144 | + }, | |
145 | + }; | |
65 | 146 | }, |
66 | 147 | }, |
67 | 148 | { |
68 | - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
69 | - label: '显示设备名称', | |
70 | - component: 'Checkbox', | |
71 | - defaultValue: option.showDeviceName, | |
72 | - }, | |
73 | - { | |
74 | - field: ComponentConfigFieldEnum.SHOW_TIME, | |
75 | - label: '显示时间', | |
76 | - component: 'Checkbox', | |
77 | - defaultValue: option.showTime, | |
149 | + field: ComponentConfigFieldEnum.CUSTOM_ICON_CLOSE, | |
150 | + label: '关闭状态图标', | |
151 | + component: 'ApiUpload', | |
152 | + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '], | |
153 | + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom', | |
154 | + changeEvent: 'update:fileList', | |
155 | + valueField: 'fileList', | |
156 | + componentProps: ({}) => { | |
157 | + return { | |
158 | + listType: 'picture-card', | |
159 | + maxSize: 0 * 1024, | |
160 | + maxFileLimit: 1, | |
161 | + accept: '.svg', | |
162 | + api: async (file: File) => { | |
163 | + try { | |
164 | + const formData = new FormData(); | |
165 | + const { name } = file; | |
166 | + formData.set('file', file); | |
167 | + const { fileStaticUri, fileName } = await upload(formData); | |
168 | + return { | |
169 | + uid: fileStaticUri, | |
170 | + name: name || fileName, | |
171 | + url: fileStaticUri, | |
172 | + } as FileItem; | |
173 | + } catch (error) { | |
174 | + return {}; | |
175 | + } | |
176 | + }, | |
177 | + // showUploadList: true, | |
178 | + onDownload() {}, | |
179 | + onPreview: (fileList: FileItem) => { | |
180 | + createImgPreview({ imageList: [fileList.url!] }); | |
181 | + }, | |
182 | + }; | |
183 | + }, | |
78 | 184 | }, |
79 | 185 | ], |
80 | 186 | showActionButtonGroup: false, | ... | ... |
... | ... | @@ -32,8 +32,19 @@ |
32 | 32 | |
33 | 33 | const { componentInfo, attributeName, attributeRename } = option; |
34 | 34 | |
35 | - const { icon, iconColor, fontColor, unit, iconClose, iconColorClose, showTime, fontSize } = | |
36 | - componentInfo || {}; | |
35 | + const { | |
36 | + icon, | |
37 | + iconColor, | |
38 | + fontColor, | |
39 | + unit, | |
40 | + iconClose, | |
41 | + iconColorClose, | |
42 | + showTime, | |
43 | + fontSize, | |
44 | + customIcon, | |
45 | + customIconClose, | |
46 | + defaultCustom, | |
47 | + } = componentInfo || {}; | |
37 | 48 | return { |
38 | 49 | iconColor: iconColor || persetIconColor, |
39 | 50 | unit: unit ?? perseUnit, |
... | ... | @@ -44,6 +55,9 @@ |
44 | 55 | iconColorClose: iconColorClose || persetIconColorClose, |
45 | 56 | showTime: showTime ?? persetShowTime, |
46 | 57 | fontSize: fontSize || persetFontSize || 14, |
58 | + defaultCustom: defaultCustom || 'default', | |
59 | + customIcon: customIcon || [], | |
60 | + customIconClose: customIconClose || [], | |
47 | 61 | }; |
48 | 62 | }); |
49 | 63 | |
... | ... | @@ -68,11 +82,18 @@ |
68 | 82 | <DeviceName :config="config" /> |
69 | 83 | <div class="flex flex-1 flex-col justify-center items-center"> |
70 | 84 | <SvgIcon |
71 | - :name="isOpenClose ? getDesign.icon : getDesign.iconClose" | |
85 | + v-if="getDesign.defaultCustom !== 'custom'" | |
86 | + :name="getDesign.iconClose" | |
72 | 87 | prefix="iconfont" |
73 | 88 | :size="getRatio ? getRatio * 70 : 70" |
74 | 89 | :style="{ color: isOpenClose ? getDesign.iconColor : getDesign.iconColorClose }" |
75 | 90 | /> |
91 | + <img | |
92 | + v-else | |
93 | + :src="isOpenClose ? getDesign.customIcon[0]?.url : getDesign.customIconClose[0]?.url" | |
94 | + :style="{ width: getRatio ? getRatio * 70 + 'px' : '70px' }" | |
95 | + :alt="getDesign.customIcon[0]?.name" | |
96 | + /> | |
76 | 97 | <div |
77 | 98 | class="text-gray-500 truncate m-2" |
78 | 99 | :style="{ | ... | ... |
... | ... | @@ -3,6 +3,9 @@ |
3 | 3 | import { useForm, BasicForm } from '/@/components/Form'; |
4 | 4 | import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; |
5 | 5 | import { option } from './config'; |
6 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
7 | + import { createImgPreview } from '/@/components/Preview'; | |
8 | + import { upload } from '/@/api/oss/ossFileUploader'; | |
6 | 9 | |
7 | 10 | const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ |
8 | 11 | schemas: [ |
... | ... | @@ -61,11 +64,37 @@ |
61 | 64 | }, |
62 | 65 | }, |
63 | 66 | { |
67 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
68 | + label: '显示设备名称', | |
69 | + component: 'Checkbox', | |
70 | + defaultValue: option.showDeviceName, | |
71 | + }, | |
72 | + { | |
73 | + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM, | |
74 | + label: '图标类型', | |
75 | + component: 'RadioGroup', | |
76 | + defaultValue: 'default', | |
77 | + componentProps: ({ formModel }) => { | |
78 | + return { | |
79 | + options: [ | |
80 | + { label: '系统默认', value: 'default' }, | |
81 | + { label: '自定义', value: 'custom' }, | |
82 | + ], | |
83 | + onChange() { | |
84 | + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = []; | |
85 | + }, | |
86 | + }; | |
87 | + }, | |
88 | + }, | |
89 | + { | |
64 | 90 | field: ComponentConfigFieldEnum.ICON_COLOR, |
65 | 91 | label: '图标颜色', |
66 | 92 | component: 'ColorPicker', |
67 | 93 | changeEvent: 'update:value', |
68 | 94 | defaultValue: option.iconColor, |
95 | + ifShow: ({ model }) => { | |
96 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
97 | + }, | |
69 | 98 | }, |
70 | 99 | { |
71 | 100 | field: ComponentConfigFieldEnum.ICON, |
... | ... | @@ -73,6 +102,9 @@ |
73 | 102 | component: 'IconDrawer', |
74 | 103 | changeEvent: 'update:value', |
75 | 104 | defaultValue: option.icon, |
105 | + ifShow: ({ model }) => { | |
106 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
107 | + }, | |
76 | 108 | componentProps({ formModel }) { |
77 | 109 | const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; |
78 | 110 | return { |
... | ... | @@ -81,10 +113,50 @@ |
81 | 113 | }, |
82 | 114 | }, |
83 | 115 | { |
84 | - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
85 | - label: '显示设备名称', | |
86 | - component: 'Checkbox', | |
87 | - defaultValue: option.showDeviceName, | |
116 | + field: ComponentConfigFieldEnum.CUSTOM_ICON, | |
117 | + label: '图标', | |
118 | + component: 'ApiUpload', | |
119 | + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom', | |
120 | + changeEvent: 'update:fileList', | |
121 | + valueField: 'fileList', | |
122 | + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '], | |
123 | + componentProps: ({ formModel }) => { | |
124 | + return { | |
125 | + maxSize: 50 * 1024, | |
126 | + listType: 'picture-card', | |
127 | + maxFileLimit: 1, | |
128 | + accept: '.svg', | |
129 | + api: async (file: File) => { | |
130 | + try { | |
131 | + const formData = new FormData(); | |
132 | + const { name } = file; | |
133 | + formData.set('file', file); | |
134 | + const { fileStaticUri, fileName } = await upload(formData); | |
135 | + return { | |
136 | + uid: fileStaticUri, | |
137 | + name: name || fileName, | |
138 | + url: fileStaticUri, | |
139 | + } as FileItem; | |
140 | + } catch (error) { | |
141 | + return {}; | |
142 | + } | |
143 | + }, | |
144 | + // showUploadList: true, | |
145 | + onDownload() {}, | |
146 | + onPreview: (fileList: FileItem) => { | |
147 | + createImgPreview({ imageList: [fileList.url!] }); | |
148 | + }, | |
149 | + onDelete(url: string) { | |
150 | + formModel.deleteUrl = url!; | |
151 | + }, | |
152 | + }; | |
153 | + }, | |
154 | + }, | |
155 | + { | |
156 | + field: 'deleteUrl', | |
157 | + label: '', | |
158 | + component: 'Input', | |
159 | + show: false, | |
88 | 160 | }, |
89 | 161 | ], |
90 | 162 | showActionButtonGroup: false, | ... | ... |
... | ... | @@ -31,7 +31,6 @@ |
31 | 31 | |
32 | 32 | const getDesign = computed(() => { |
33 | 33 | const { persetOption = {}, option } = props.config; |
34 | - | |
35 | 34 | const { |
36 | 35 | iconColor: persetIconColor, |
37 | 36 | unit: perseUnit, |
... | ... | @@ -44,7 +43,8 @@ |
44 | 43 | const { componentInfo, attributeRename } = option; |
45 | 44 | const { functionName } = unref(getThingModelTsl) || {}; |
46 | 45 | |
47 | - const { icon, iconColor, fontColor, unit, valueSize, fontSize } = componentInfo || {}; | |
46 | + const { icon, iconColor, fontColor, unit, valueSize, fontSize, customIcon, defaultCustom } = | |
47 | + componentInfo || {}; | |
48 | 48 | return { |
49 | 49 | iconColor: iconColor || persetIconColor, |
50 | 50 | unit: unit ?? perseUnit, |
... | ... | @@ -53,6 +53,8 @@ |
53 | 53 | attribute: attributeRename || functionName, |
54 | 54 | valueSize: valueSize || persetValueSize || 20, |
55 | 55 | fontSize: fontSize || persetFontSize || 14, |
56 | + defaultCustom: defaultCustom || 'default', | |
57 | + customIcon: customIcon || [], | |
56 | 58 | }; |
57 | 59 | }); |
58 | 60 | |
... | ... | @@ -78,11 +80,18 @@ |
78 | 80 | <DeviceName :config="config" /> |
79 | 81 | <div class="flex-1 flex justify-center items-center flex-col w-full"> |
80 | 82 | <SvgIcon |
83 | + v-if="getDesign.defaultCustom !== 'custom'" | |
81 | 84 | :name="getDesign.icon!" |
82 | 85 | prefix="iconfont" |
83 | 86 | :size="getRatio ? getRatio * 70 : 70" |
84 | 87 | :style="{ color: getDesign.iconColor }" |
85 | 88 | /> |
89 | + <img | |
90 | + v-else | |
91 | + :src="getDesign.customIcon[0]?.url" | |
92 | + :style="{ width: getRatio ? getRatio * 70 + 'px' : '70px' }" | |
93 | + :alt="getDesign.customIcon[0]?.name" | |
94 | + /> | |
86 | 95 | <h1 |
87 | 96 | class="font-bold m-2 truncate w-full text-center" |
88 | 97 | :style="{ | ... | ... |
... | ... | @@ -3,6 +3,9 @@ |
3 | 3 | import { useForm, BasicForm } from '/@/components/Form'; |
4 | 4 | import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; |
5 | 5 | import { option } from './config'; |
6 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
7 | + import { createImgPreview } from '/@/components/Preview'; | |
8 | + import { upload } from '/@/api/oss/ossFileUploader'; | |
6 | 9 | |
7 | 10 | const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ |
8 | 11 | schemas: [ |
... | ... | @@ -60,11 +63,37 @@ |
60 | 63 | }, |
61 | 64 | }, |
62 | 65 | { |
66 | + field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
67 | + label: '显示设备名称', | |
68 | + component: 'Checkbox', | |
69 | + defaultValue: option.showDeviceName, | |
70 | + }, | |
71 | + { | |
72 | + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM, | |
73 | + label: '图标类型', | |
74 | + component: 'RadioGroup', | |
75 | + defaultValue: 'default', | |
76 | + componentProps: ({ formModel }) => { | |
77 | + return { | |
78 | + options: [ | |
79 | + { label: '系统默认', value: 'default' }, | |
80 | + { label: '自定义', value: 'custom' }, | |
81 | + ], | |
82 | + onChange() { | |
83 | + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = []; | |
84 | + }, | |
85 | + }; | |
86 | + }, | |
87 | + }, | |
88 | + { | |
63 | 89 | field: ComponentConfigFieldEnum.ICON_COLOR, |
64 | 90 | label: '图标颜色', |
65 | 91 | component: 'ColorPicker', |
66 | 92 | changeEvent: 'update:value', |
67 | 93 | defaultValue: option.iconColor, |
94 | + ifShow: ({ model }) => { | |
95 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
96 | + }, | |
68 | 97 | }, |
69 | 98 | { |
70 | 99 | field: ComponentConfigFieldEnum.ICON, |
... | ... | @@ -72,6 +101,9 @@ |
72 | 101 | component: 'IconDrawer', |
73 | 102 | changeEvent: 'update:value', |
74 | 103 | defaultValue: option.icon, |
104 | + ifShow: ({ model }) => { | |
105 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
106 | + }, | |
75 | 107 | componentProps({ formModel }) { |
76 | 108 | const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; |
77 | 109 | return { |
... | ... | @@ -80,10 +112,50 @@ |
80 | 112 | }, |
81 | 113 | }, |
82 | 114 | { |
83 | - field: ComponentConfigFieldEnum.SHOW_DEVICE_NAME, | |
84 | - label: '显示设备名称', | |
85 | - component: 'Checkbox', | |
86 | - defaultValue: option.showDeviceName, | |
115 | + field: ComponentConfigFieldEnum.CUSTOM_ICON, | |
116 | + label: '图标', | |
117 | + component: 'ApiUpload', | |
118 | + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom', | |
119 | + changeEvent: 'update:fileList', | |
120 | + valueField: 'fileList', | |
121 | + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '], | |
122 | + componentProps: ({ formModel }) => { | |
123 | + return { | |
124 | + listType: 'picture-card', | |
125 | + maxSize: 50 * 1024, | |
126 | + maxFileLimit: 1, | |
127 | + accept: '.svg', | |
128 | + api: async (file: File) => { | |
129 | + try { | |
130 | + const formData = new FormData(); | |
131 | + const { name } = file; | |
132 | + formData.set('file', file); | |
133 | + const { fileStaticUri, fileName } = await upload(formData); | |
134 | + return { | |
135 | + uid: fileStaticUri, | |
136 | + name: name || fileName, | |
137 | + url: fileStaticUri, | |
138 | + } as FileItem; | |
139 | + } catch (error) { | |
140 | + return {}; | |
141 | + } | |
142 | + }, | |
143 | + // showUploadList: true, | |
144 | + onDownload() {}, | |
145 | + onPreview: (fileList: FileItem) => { | |
146 | + createImgPreview({ imageList: [fileList.url!] }); | |
147 | + }, | |
148 | + onDelete(url: string) { | |
149 | + formModel.deleteUrl = url!; | |
150 | + }, | |
151 | + }; | |
152 | + }, | |
153 | + }, | |
154 | + { | |
155 | + field: 'deleteUrl', | |
156 | + label: '', | |
157 | + component: 'Input', | |
158 | + show: false, | |
87 | 159 | }, |
88 | 160 | ], |
89 | 161 | showActionButtonGroup: false, | ... | ... |
... | ... | @@ -40,7 +40,8 @@ |
40 | 40 | |
41 | 41 | const { componentInfo, attribute, attributeRename } = option; |
42 | 42 | |
43 | - const { icon, iconColor, fontColor, unit, valueSize, fontSize } = componentInfo || {}; | |
43 | + const { icon, iconColor, fontColor, unit, valueSize, fontSize, customIcon, defaultCustom } = | |
44 | + componentInfo || {}; | |
44 | 45 | return { |
45 | 46 | iconColor: iconColor || persetIconColor, |
46 | 47 | unit: unit ?? perseUnit, |
... | ... | @@ -49,6 +50,8 @@ |
49 | 50 | attribute: attributeRename || unref(getThingModelTsl)?.functionName || attribute, |
50 | 51 | valueSize: valueSize || persetValueSize || 20, |
51 | 52 | fontSize: fontSize || persetFontSize || 14, |
53 | + defaultCustom: defaultCustom || 'default', | |
54 | + customIcon: customIcon || [], | |
52 | 55 | }; |
53 | 56 | }); |
54 | 57 | |
... | ... | @@ -73,11 +76,18 @@ |
73 | 76 | <DeviceName :config="config" /> |
74 | 77 | <div :style="getScale" class="flex-1 flex justify-center items-center flex-col w-full"> |
75 | 78 | <SvgIcon |
79 | + v-if="getDesign.defaultCustom !== 'custom'" | |
76 | 80 | :name="getDesign.icon!" |
77 | 81 | prefix="iconfont" |
78 | 82 | :size="getRatio ? getRatio * 70 : 70" |
79 | 83 | :style="{ color: getDesign.iconColor }" |
80 | 84 | /> |
85 | + <img | |
86 | + v-else | |
87 | + :src="getDesign.customIcon[0]?.url" | |
88 | + :style="{ width: getRatio ? getRatio * 70 + 'px' : '70px' }" | |
89 | + :alt="getDesign.customIcon[0]?.name" | |
90 | + /> | |
81 | 91 | <h1 |
82 | 92 | class="my-4 font-bold !my-2 truncate w-full text-center" |
83 | 93 | :style="{ | ... | ... |
... | ... | @@ -4,6 +4,10 @@ |
4 | 4 | import { PublicFormInstaceType } from '/@/views/visual/dataSourceBindPanel/index.type'; |
5 | 5 | import { option } from './config'; |
6 | 6 | |
7 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
8 | + import { createImgPreview } from '/@/components/Preview'; | |
9 | + import { upload } from '/@/api/oss/ossFileUploader'; | |
10 | + | |
7 | 11 | const [register, { getFieldsValue, setFieldsValue, resetFields }] = useForm({ |
8 | 12 | schemas: [ |
9 | 13 | { |
... | ... | @@ -60,11 +64,31 @@ |
60 | 64 | }, |
61 | 65 | }, |
62 | 66 | { |
67 | + field: ComponentConfigFieldEnum.DEFAULT_CUSTOM, | |
68 | + label: '图标类型', | |
69 | + component: 'RadioGroup', | |
70 | + defaultValue: 'default', | |
71 | + componentProps: ({ formModel }) => { | |
72 | + return { | |
73 | + options: [ | |
74 | + { label: '系统默认', value: 'default' }, | |
75 | + { label: '自定义', value: 'custom' }, | |
76 | + ], | |
77 | + onChange() { | |
78 | + formModel[ComponentConfigFieldEnum.CUSTOM_ICON] = []; | |
79 | + }, | |
80 | + }; | |
81 | + }, | |
82 | + }, | |
83 | + { | |
63 | 84 | field: ComponentConfigFieldEnum.ICON_COLOR, |
64 | 85 | label: '图标颜色', |
65 | 86 | component: 'ColorPicker', |
66 | 87 | changeEvent: 'update:value', |
67 | 88 | defaultValue: option.iconColor, |
89 | + ifShow: ({ model }) => { | |
90 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
91 | + }, | |
68 | 92 | }, |
69 | 93 | { |
70 | 94 | field: ComponentConfigFieldEnum.ICON, |
... | ... | @@ -73,6 +97,9 @@ |
73 | 97 | changeEvent: 'update:value', |
74 | 98 | valueField: 'value', |
75 | 99 | defaultValue: option.icon, |
100 | + ifShow: ({ model }) => { | |
101 | + return model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] !== 'custom'; | |
102 | + }, | |
76 | 103 | componentProps({ formModel }) { |
77 | 104 | const color = formModel[ComponentConfigFieldEnum.ICON_COLOR]; |
78 | 105 | return { |
... | ... | @@ -80,6 +107,51 @@ |
80 | 107 | }; |
81 | 108 | }, |
82 | 109 | }, |
110 | + { | |
111 | + field: ComponentConfigFieldEnum.CUSTOM_ICON, | |
112 | + label: '图标', | |
113 | + component: 'ApiUpload', | |
114 | + ifShow: ({ model }) => model[ComponentConfigFieldEnum.DEFAULT_CUSTOM] === 'custom', | |
115 | + changeEvent: 'update:fileList', | |
116 | + valueField: 'fileList', | |
117 | + helpMessage: ['支持.svg格式,建议尺寸为32*32px,大小不超过50kb '], | |
118 | + componentProps: ({ formModel }) => { | |
119 | + return { | |
120 | + listType: 'picture-card', | |
121 | + maxSize: 50 * 1024, | |
122 | + maxFileLimit: 1, | |
123 | + accept: '.svg', | |
124 | + api: async (file: File) => { | |
125 | + try { | |
126 | + const formData = new FormData(); | |
127 | + const { name } = file; | |
128 | + formData.set('file', file); | |
129 | + const { fileStaticUri, fileName } = await upload(formData); | |
130 | + return { | |
131 | + uid: fileStaticUri, | |
132 | + name: name || fileName, | |
133 | + url: fileStaticUri, | |
134 | + } as FileItem; | |
135 | + } catch (error) { | |
136 | + return {}; | |
137 | + } | |
138 | + }, | |
139 | + onDownload() {}, | |
140 | + onPreview: (fileList: FileItem) => { | |
141 | + createImgPreview({ imageList: [fileList.url!] }); | |
142 | + }, | |
143 | + onDelete(url: string) { | |
144 | + formModel.deleteUrl = url!; | |
145 | + }, | |
146 | + }; | |
147 | + }, | |
148 | + }, | |
149 | + { | |
150 | + field: 'deleteUrl', | |
151 | + label: '', | |
152 | + component: 'Input', | |
153 | + show: false, | |
154 | + }, | |
83 | 155 | ], |
84 | 156 | showActionButtonGroup: false, |
85 | 157 | labelWidth: 120, | ... | ... |
... | ... | @@ -35,7 +35,8 @@ |
35 | 35 | |
36 | 36 | return { |
37 | 37 | dataSource: dataSource.map((item) => { |
38 | - const { fontColor, icon, iconColor, unit, valueSize, fontSize } = item.componentInfo; | |
38 | + const { fontColor, icon, iconColor, unit, valueSize, fontSize, customIcon, defaultCustom } = | |
39 | + item.componentInfo; | |
39 | 40 | const { attribute, attributeRename, deviceName, deviceRename, deviceId, deviceProfileId } = |
40 | 41 | item; |
41 | 42 | const tsl = getDeviceProfileTslByIdWithIdentifier?.(deviceProfileId, attribute); |
... | ... | @@ -50,6 +51,8 @@ |
50 | 51 | id: deviceId, |
51 | 52 | valueSize: valueSize || persetValueSize || 20, |
52 | 53 | fontSize: fontSize || persetFontSize || 14, |
54 | + defaultCustom: defaultCustom || 'default', | |
55 | + customIcon: customIcon || [], | |
53 | 56 | }; |
54 | 57 | }), |
55 | 58 | }; |
... | ... | @@ -67,6 +70,8 @@ |
67 | 70 | fontColor: '#357CFB', |
68 | 71 | fontSize: 16, |
69 | 72 | valueSize: 16, |
73 | + defaultCustom: 'default', | |
74 | + customIcon: [], | |
70 | 75 | }, |
71 | 76 | { |
72 | 77 | id: buildUUID(), |
... | ... | @@ -79,6 +84,8 @@ |
79 | 84 | fontColor: '#FFA000', |
80 | 85 | fontSize: 16, |
81 | 86 | valueSize: 16, |
87 | + defaultCustom: 'default', | |
88 | + customIcon: [], | |
82 | 89 | }, |
83 | 90 | ]); |
84 | 91 | |
... | ... | @@ -114,11 +121,24 @@ |
114 | 121 | class="flex justify-between items-center mt-2" |
115 | 122 | > |
116 | 123 | <div class="flex items-center"> |
117 | - <SvgIcon | |
124 | + <!-- <SvgIcon | |
118 | 125 | :name="item.icon!" |
119 | 126 | prefix="iconfont" |
120 | 127 | :size="getRatio ? 30 * getRatio : 30" |
121 | 128 | :style="{ color: item.iconColor }" |
129 | + /> --> | |
130 | + <SvgIcon | |
131 | + v-if="item.defaultCustom !== 'custom'" | |
132 | + :name="item.icon!" | |
133 | + prefix="iconfont" | |
134 | + :size="getRatio ? getRatio * 30 : 30" | |
135 | + :style="{ color: item.iconColor }" | |
136 | + /> | |
137 | + <img | |
138 | + v-else | |
139 | + :src="item.customIcon[0]?.url" | |
140 | + :style="{ width: getRatio ? getRatio * 30 + 'px' : '30px' }" | |
141 | + :alt="item.customIcon[0]?.name" | |
122 | 142 | /> |
123 | 143 | <div |
124 | 144 | class="text-gray-500 ml-6" | ... | ... |
... | ... | @@ -36,4 +36,8 @@ export enum ComponentConfigFieldEnum { |
36 | 36 | MIN_NUMBER = 'minNumber', |
37 | 37 | MAX_NUMBER = 'maxNumber', |
38 | 38 | PASS_WORD = 'password', //操作密码 |
39 | + DEFAULT_CUSTOM = 'defaultCustom', | |
40 | + DEFAULT_CUSTOM_CLOSE = 'defaultCustomClose', | |
41 | + CUSTOM_ICON = 'customIcon', | |
42 | + CUSTOM_ICON_CLOSE = 'customIconClose', | |
39 | 43 | } | ... | ... |
... | ... | @@ -20,6 +20,8 @@ |
20 | 20 | import { useGetComponentConfig } from '../../../packages/hook/useGetComponetConfig'; |
21 | 21 | import { isBoolean } from '/@/utils/is'; |
22 | 22 | import { useApp } from '../../hooks/useApp'; |
23 | + import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | |
24 | + import { deleteFilePath } from '/@/api/oss/ossFileUploader'; | |
23 | 25 | |
24 | 26 | const props = defineProps<{ |
25 | 27 | sourceInfo: WidgetDataType; |
... | ... | @@ -74,8 +76,56 @@ |
74 | 76 | emit('update', toRaw(props.sourceInfo)); |
75 | 77 | } |
76 | 78 | |
79 | + const countElementOccurrences = (arr) => { | |
80 | + const countMap = {}; | |
81 | + | |
82 | + arr.forEach((element) => { | |
83 | + if (countMap[element]) { | |
84 | + countMap[element]++; | |
85 | + } else { | |
86 | + countMap[element] = 1; | |
87 | + } | |
88 | + }); | |
89 | + | |
90 | + return countMap; | |
91 | + }; | |
92 | + | |
77 | 93 | async function handleDelete() { |
78 | 94 | try { |
95 | + const { componentData: oldDataSource } = props.rawDataSource; | |
96 | + const customIconUrls = ref<any>([]); | |
97 | + oldDataSource?.forEach((item: any) => { | |
98 | + item.dataSource?.forEach((dataSource) => { | |
99 | + if (dataSource.componentInfo?.customIcon) { | |
100 | + dataSource.componentInfo?.customIcon.forEach((icon: FileItem) => { | |
101 | + customIconUrls.value.push(icon.url); | |
102 | + }); | |
103 | + } | |
104 | + }); | |
105 | + }); | |
106 | + | |
107 | + const { dataSource: deleteDataSource } = props.sourceInfo; | |
108 | + const dataSourceDeleteUrl = deleteDataSource.map( | |
109 | + (item) => item.componentInfo.customIcon?.[0].url | |
110 | + ); | |
111 | + | |
112 | + if (dataSourceDeleteUrl?.length) { | |
113 | + // 判断外部所有组件是否有dataSourceDeleteUrl使用中的url | |
114 | + const deletePromise = unref(customIconUrls)?.filter((item) => | |
115 | + dataSourceDeleteUrl?.includes(item) | |
116 | + ); | |
117 | + | |
118 | + const deleteUrlInfo = countElementOccurrences(deletePromise); | |
119 | + const deleteUrl = deletePromise?.filter((item) => deleteUrlInfo?.[item] == 1); | |
120 | + Promise.all( | |
121 | + deleteUrl.map((item) => { | |
122 | + deleteFilePath(item); | |
123 | + }) | |
124 | + ); | |
125 | + } | |
126 | + } catch (err) {} | |
127 | + | |
128 | + try { | |
79 | 129 | await deleteDataComponent({ dataBoardId: unref(boardId), ids: [props.sourceInfo.id] }); |
80 | 130 | createMessage.success('删除成功'); |
81 | 131 | emit('ok'); | ... | ... |