Commit ca118a71af2ff60c5dfe1762725b8741656b2d66
1 parent
356ba0a0
feat: ota page add permission control
Showing
10 changed files
with
324 additions
and
13 deletions
... | ... | @@ -81,7 +81,13 @@ |
81 | 81 | </script> |
82 | 82 | |
83 | 83 | <template> |
84 | - <BasicModal title="包管理" destroy-on-close @register="registerModal" @ok="handleSubmit"> | |
84 | + <BasicModal | |
85 | + title="包管理" | |
86 | + destroy-on-close | |
87 | + :ok-button-props="{ loading }" | |
88 | + @register="registerModal" | |
89 | + @ok="handleSubmit" | |
90 | + > | |
85 | 91 | <BasicForm @register="registerForm" class="package-manage-form" /> |
86 | 92 | </BasicModal> |
87 | 93 | </template> | ... | ... |
... | ... | @@ -15,6 +15,8 @@ |
15 | 15 | } from '/@/api/ota'; |
16 | 16 | import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard'; |
17 | 17 | import { useDownload } from '../hook/useDownload'; |
18 | + import { Authority } from '/@/components/Authority'; | |
19 | + import { OtaPermissionKey } from '../config/config'; | |
18 | 20 | // import DeviceDetailDrawer from '/@/views/device/list/cpns/modal/DeviceDetailDrawer.vue'; |
19 | 21 | |
20 | 22 | const emit = defineEmits(['register', 'update:list']); |
... | ... | @@ -143,7 +145,9 @@ |
143 | 145 | class="absolute right-0 bottom-0 w-full border-t bg-light-50 border-t-gray-100 py-2 px-4 text-right" |
144 | 146 | > |
145 | 147 | <Button class="mr-2" @click="closeDrawer">取消</Button> |
146 | - <Button type="primary" :loading="loading" @click="handleSubmit">保存</Button> | |
148 | + <Authority :value="OtaPermissionKey.UPDATE"> | |
149 | + <Button type="primary" :loading="loading" @click="handleSubmit">保存</Button> | |
150 | + </Authority> | |
147 | 151 | </div> |
148 | 152 | </template> |
149 | 153 | <!-- <DeviceDetailDrawer @register="registerTBDrawer" /> --> | ... | ... |
... | ... | @@ -8,6 +8,13 @@ export interface ModalPassRecord { |
8 | 8 | record?: Recordable; |
9 | 9 | } |
10 | 10 | |
11 | +export enum OtaPermissionKey { | |
12 | + CREATE = 'api:operation:ota:post', | |
13 | + UPDATE = 'api:operation:ota:update', | |
14 | + DELETE = 'api:operation:ota:delete', | |
15 | + DOWNLOAD = 'api:operation:ota:download', | |
16 | +} | |
17 | + | |
11 | 18 | export const columns: BasicColumn[] = [ |
12 | 19 | { |
13 | 20 | title: '创建时间', | ... | ... |
... | ... | @@ -45,14 +45,29 @@ export enum ALG { |
45 | 45 | MURMUR3_128 = 'MURMUR3128', |
46 | 46 | } |
47 | 47 | |
48 | +const getVersionTag = (title: string, version: string) => { | |
49 | + return `${title ?? ''} ${version ?? ''}`; | |
50 | +}; | |
51 | + | |
48 | 52 | export const formSchema: FormSchema[] = [ |
49 | 53 | { |
50 | 54 | field: PackageField.TITLE, |
51 | 55 | label: '标题', |
52 | 56 | component: 'Input', |
53 | 57 | rules: [{ required: true, message: '标题为必填项' }], |
54 | - componentProps: { | |
55 | - placeholder: '请输入标题', | |
58 | + componentProps: ({ formActionType, formModel }) => { | |
59 | + const { setFieldsValue } = formActionType; | |
60 | + return { | |
61 | + placeholder: '请输入标题', | |
62 | + onChange: (value: Event) => { | |
63 | + setFieldsValue({ | |
64 | + [PackageField.VERSION_TAG]: getVersionTag( | |
65 | + (value.target as HTMLInputElement).value, | |
66 | + formModel[PackageField.VERSION] | |
67 | + ), | |
68 | + }); | |
69 | + }, | |
70 | + }; | |
56 | 71 | }, |
57 | 72 | }, |
58 | 73 | { |
... | ... | @@ -60,8 +75,19 @@ export const formSchema: FormSchema[] = [ |
60 | 75 | label: '版本', |
61 | 76 | component: 'Input', |
62 | 77 | rules: [{ required: true, message: '版本为必填项' }], |
63 | - componentProps: { | |
64 | - placeholder: '请输入版本', | |
78 | + componentProps: ({ formActionType, formModel }) => { | |
79 | + const { setFieldsValue } = formActionType; | |
80 | + return { | |
81 | + placeholder: '请输入版本', | |
82 | + onChange: (value: Event) => { | |
83 | + setFieldsValue({ | |
84 | + [PackageField.VERSION_TAG]: getVersionTag( | |
85 | + formModel[PackageField.TITLE], | |
86 | + (value.target as HTMLInputElement).value | |
87 | + ), | |
88 | + }); | |
89 | + }, | |
90 | + }; | |
65 | 91 | }, |
66 | 92 | }, |
67 | 93 | { | ... | ... |
1 | 1 | <script lang="ts" setup> |
2 | 2 | import { Button } from 'ant-design-vue'; |
3 | - import { columns, ModalPassRecord, searchFormSchema } from './config/config'; | |
3 | + import { columns, ModalPassRecord, OtaPermissionKey, searchFormSchema } from './config/config'; | |
4 | 4 | import { PageWrapper } from '/@/components/Page'; |
5 | 5 | import { BasicTable, useTable, TableAction } from '/@/components/Table'; |
6 | 6 | import PackageDetailModal from './components/PackageDetailModal.vue'; |
... | ... | @@ -13,6 +13,7 @@ |
13 | 13 | import { useDownload } from './hook/useDownload'; |
14 | 14 | import { computed } from 'vue'; |
15 | 15 | import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm'; |
16 | + import { Authority } from '/@/components/Authority'; | |
16 | 17 | |
17 | 18 | const [register, { reload, getSelectRowKeys, getRowSelection, setSelectedRowKeys }] = useTable({ |
18 | 19 | columns, |
... | ... | @@ -102,10 +103,14 @@ |
102 | 103 | <PageWrapper dense contentFullHeight contentClass="flex flex-col"> |
103 | 104 | <BasicTable @register="register" @row-click="handleOpenDetailDrawer" class="ota-list"> |
104 | 105 | <template #toolbar> |
105 | - <Button @click="handleCreatePackage" type="primary">新增包</Button> | |
106 | - <Button @click="handleBatchDelete" :disabled="canDelete" type="primary" danger> | |
107 | - 批量删除 | |
108 | - </Button> | |
106 | + <Authority :value="OtaPermissionKey.CREATE"> | |
107 | + <Button @click="handleCreatePackage" type="primary">新增包</Button> | |
108 | + </Authority> | |
109 | + <Authority :value="OtaPermissionKey.DELETE"> | |
110 | + <Button @click="handleBatchDelete" :disabled="canDelete" type="primary" danger> | |
111 | + 批量删除 | |
112 | + </Button> | |
113 | + </Authority> | |
109 | 114 | </template> |
110 | 115 | <template #action="{ record }"> |
111 | 116 | <TableAction |
... | ... | @@ -114,12 +119,14 @@ |
114 | 119 | { |
115 | 120 | label: '下载', |
116 | 121 | icon: 'ant-design:download-outlined', |
122 | + auth: OtaPermissionKey.DOWNLOAD, | |
117 | 123 | onClick: downloadFile.bind(null, record), |
118 | 124 | }, |
119 | 125 | { |
120 | 126 | label: '删除', |
121 | 127 | icon: 'ant-design:delete-outlined', |
122 | 128 | color: 'error', |
129 | + auth: OtaPermissionKey.DELETE, | |
123 | 130 | popConfirm: { |
124 | 131 | title: '是否确认删除', |
125 | 132 | confirm: deletePackage.bind(null, record), | ... | ... |
1 | +<script lang="ts" setup> | |
2 | + import { ref, unref } from 'vue'; | |
3 | + import { BasicForm, FormActionType } from '/@/components/Form'; | |
4 | + import { mapFormSchema } from '../../config/basicConfiguration'; | |
5 | + | |
6 | + const formEl = ref<Nullable<FormActionType>>(); | |
7 | + | |
8 | + const setFormEl = (el: any) => { | |
9 | + formEl.value = el; | |
10 | + }; | |
11 | + | |
12 | + const getFieldsValue = () => { | |
13 | + return unref(formEl)!.getFieldsValue(); | |
14 | + }; | |
15 | + | |
16 | + const validate = async () => { | |
17 | + await unref(formEl)!.validate(); | |
18 | + }; | |
19 | + | |
20 | + const setFieldsValue = async (record: Recordable) => { | |
21 | + await unref(formEl)!.setFieldsValue(record); | |
22 | + }; | |
23 | + | |
24 | + const clearValidate = async (name?: string | string[]) => { | |
25 | + await unref(formEl)!.clearValidate(name); | |
26 | + }; | |
27 | + defineExpose({ | |
28 | + formActionType: { getFieldsValue, validate, setFieldsValue, clearValidate }, | |
29 | + }); | |
30 | +</script> | |
31 | + | |
32 | +<template> | |
33 | + <div class="w-full flex-1"> | |
34 | + <BasicForm | |
35 | + :ref="(el) => setFormEl(el)" | |
36 | + :schemas="mapFormSchema" | |
37 | + class="w-full flex-1 data-source-form" | |
38 | + :show-action-button-group="false" | |
39 | + :row-props="{ | |
40 | + gutter: 10, | |
41 | + }" | |
42 | + layout="horizontal" | |
43 | + :label-col="{ span: 0 }" | |
44 | + /> | |
45 | + </div> | |
46 | +</template> | ... | ... |
... | ... | @@ -2,10 +2,12 @@ import { Component } from 'vue'; |
2 | 2 | import { FrontComponent } from '../../../const/const'; |
3 | 3 | import BasicDataSourceForm from './BasicDataSourceForm.vue'; |
4 | 4 | import ControlDataSourceForm from './ControlDataSourceForm.vue'; |
5 | +import MapDataSourceForm from './MapDataSourceForm.vue'; | |
5 | 6 | |
6 | 7 | const dataSourceComponentMap = new Map<FrontComponent, Component>(); |
7 | 8 | |
8 | 9 | dataSourceComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, ControlDataSourceForm); |
10 | +dataSourceComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK, MapDataSourceForm); | |
9 | 11 | |
10 | 12 | export const getDataSourceComponent = (frontId: FrontComponent) => { |
11 | 13 | if (dataSourceComponentMap.has(frontId)) return dataSourceComponentMap.get(frontId)!; | ... | ... |
... | ... | @@ -25,6 +25,8 @@ export enum DataSourceField { |
25 | 25 | ATTRIBUTE_RENAME = 'attributeRename', |
26 | 26 | DEVICE_NAME = 'deviceName', |
27 | 27 | DEVICE_RENAME = 'deviceRename', |
28 | + LONGITUDE_ATTRIBUTE = 'longitudeAttribute', | |
29 | + LATITUDE_ATTRIBUTE = 'latitudeAttribute', | |
28 | 30 | } |
29 | 31 | |
30 | 32 | export const basicSchema: FormSchema[] = [ |
... | ... | @@ -238,3 +240,214 @@ export const controlFormSchema: FormSchema[] = [ |
238 | 240 | }, |
239 | 241 | }, |
240 | 242 | ]; |
243 | + | |
244 | +export const mapFormSchema: FormSchema[] = [ | |
245 | + { | |
246 | + field: DataSourceField.IS_GATEWAY_DEVICE, | |
247 | + component: 'Switch', | |
248 | + label: '是否是网关设备', | |
249 | + show: false, | |
250 | + }, | |
251 | + { | |
252 | + field: DataSourceField.DEVICE_NAME, | |
253 | + component: 'Input', | |
254 | + label: '设备名', | |
255 | + show: false, | |
256 | + }, | |
257 | + { | |
258 | + field: DataSourceField.ORIGINATION_ID, | |
259 | + component: 'ApiTreeSelect', | |
260 | + label: '组织', | |
261 | + colProps: { span: 8 }, | |
262 | + rules: [{ required: true, message: '组织为必填项' }], | |
263 | + componentProps({ formActionType }) { | |
264 | + const { setFieldsValue } = formActionType; | |
265 | + return { | |
266 | + placeholder: '请选择组织', | |
267 | + api: async () => { | |
268 | + const data = await getOrganizationList(); | |
269 | + copyTransFun(data as any as any[]); | |
270 | + return data; | |
271 | + }, | |
272 | + onChange() { | |
273 | + setFieldsValue({ | |
274 | + [DataSourceField.DEVICE_ID]: null, | |
275 | + [DataSourceField.LATITUDE_ATTRIBUTE]: null, | |
276 | + [DataSourceField.LONGITUDE_ATTRIBUTE]: null, | |
277 | + [DataSourceField.SLAVE_DEVICE_ID]: null, | |
278 | + [DataSourceField.IS_GATEWAY_DEVICE]: false, | |
279 | + }); | |
280 | + }, | |
281 | + getPopupContainer: () => document.body, | |
282 | + }; | |
283 | + }, | |
284 | + }, | |
285 | + { | |
286 | + field: DataSourceField.DEVICE_ID, | |
287 | + component: 'ApiSelect', | |
288 | + label: '设备', | |
289 | + colProps: { span: 8 }, | |
290 | + rules: [{ required: true, message: '设备名称为必填项' }], | |
291 | + componentProps({ formModel, formActionType }) { | |
292 | + const { setFieldsValue } = formActionType; | |
293 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | |
294 | + return { | |
295 | + api: async () => { | |
296 | + if (organizationId) { | |
297 | + try { | |
298 | + const data = await getAllDeviceByOrg(organizationId); | |
299 | + if (data) | |
300 | + return data.map((item) => ({ | |
301 | + label: item.name, | |
302 | + value: item.id, | |
303 | + deviceType: item.deviceType, | |
304 | + })); | |
305 | + } catch (error) {} | |
306 | + } | |
307 | + return []; | |
308 | + }, | |
309 | + onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) { | |
310 | + setFieldsValue({ | |
311 | + [DataSourceField.LONGITUDE_ATTRIBUTE]: null, | |
312 | + [DataSourceField.LATITUDE_ATTRIBUTE]: null, | |
313 | + [DataSourceField.IS_GATEWAY_DEVICE]: record?.deviceType === 'GATEWAY', | |
314 | + [DataSourceField.SLAVE_DEVICE_ID]: null, | |
315 | + [DataSourceField.DEVICE_NAME]: record?.label, | |
316 | + }); | |
317 | + }, | |
318 | + placeholder: '请选择设备', | |
319 | + getPopupContainer: () => document.body, | |
320 | + }; | |
321 | + }, | |
322 | + }, | |
323 | + { | |
324 | + field: DataSourceField.SLAVE_DEVICE_ID, | |
325 | + label: '网关子设备', | |
326 | + component: 'ApiSelect', | |
327 | + colProps: { span: 8 }, | |
328 | + rules: [{ required: true, message: '网关子设备为必填项' }], | |
329 | + ifShow({ model }) { | |
330 | + return model[DataSourceField.IS_GATEWAY_DEVICE]; | |
331 | + }, | |
332 | + dynamicRules({ model }) { | |
333 | + return [{ required: model[DataSourceField.IS_GATEWAY_DEVICE], message: '请选择网关子设备' }]; | |
334 | + }, | |
335 | + componentProps({ formModel, formActionType }) { | |
336 | + const { setFieldsValue } = formActionType; | |
337 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | |
338 | + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE]; | |
339 | + const deviceId = formModel[DataSourceField.DEVICE_ID]; | |
340 | + return { | |
341 | + api: async () => { | |
342 | + if (organizationId && isGatewayDevice) { | |
343 | + try { | |
344 | + const data = await getGatewaySlaveDevice({ organizationId, masterId: deviceId }); | |
345 | + if (data) | |
346 | + return data.map((item) => ({ | |
347 | + label: item.name, | |
348 | + value: item.id, | |
349 | + deviceType: item.deviceType, | |
350 | + })); | |
351 | + } catch (error) {} | |
352 | + } | |
353 | + return []; | |
354 | + }, | |
355 | + onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) { | |
356 | + setFieldsValue({ | |
357 | + [DataSourceField.LATITUDE_ATTRIBUTE]: null, | |
358 | + [DataSourceField.LONGITUDE_ATTRIBUTE]: null, | |
359 | + [DataSourceField.DEVICE_NAME]: record?.label, | |
360 | + }); | |
361 | + }, | |
362 | + placeholder: '请选择网关子设备', | |
363 | + getPopupContainer: () => document.body, | |
364 | + }; | |
365 | + }, | |
366 | + }, | |
367 | + { | |
368 | + field: DataSourceField.LONGITUDE_ATTRIBUTE, | |
369 | + component: 'ApiSelect', | |
370 | + label: '经度属性', | |
371 | + colProps: { span: 8 }, | |
372 | + rules: [{ required: true, message: '属性为必填项' }], | |
373 | + componentProps({ formModel, formActionType }) { | |
374 | + const { updateSchema, setFieldsValue } = formActionType; | |
375 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | |
376 | + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE]; | |
377 | + const deviceId = formModel[DataSourceField.DEVICE_ID]; | |
378 | + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID]; | |
379 | + | |
380 | + let attrs: Record<'label' | 'value', string>[] = []; | |
381 | + return { | |
382 | + api: async () => { | |
383 | + if (organizationId && deviceId) { | |
384 | + try { | |
385 | + if (isGatewayDevice && slaveDeviceId) { | |
386 | + return (attrs = await getDeviceAttribute(slaveDeviceId)); | |
387 | + } | |
388 | + if (!isGatewayDevice) { | |
389 | + return (attrs = await getDeviceAttribute(deviceId)); | |
390 | + } | |
391 | + } catch (error) {} | |
392 | + } | |
393 | + return []; | |
394 | + }, | |
395 | + placeholder: '请选择经度属性', | |
396 | + getPopupContainer: () => document.body, | |
397 | + onChange: (value: string) => { | |
398 | + if (!value) return; | |
399 | + setFieldsValue({ [DataSourceField.LATITUDE_ATTRIBUTE]: null }); | |
400 | + updateSchema({ | |
401 | + field: DataSourceField.LATITUDE_ATTRIBUTE, | |
402 | + componentProps: { | |
403 | + options: attrs.filter((item) => item.value !== value), | |
404 | + }, | |
405 | + }); | |
406 | + }, | |
407 | + }; | |
408 | + }, | |
409 | + }, | |
410 | + { | |
411 | + field: DataSourceField.LATITUDE_ATTRIBUTE, | |
412 | + component: 'ApiSelect', | |
413 | + label: '纬度属性', | |
414 | + colProps: { span: 8 }, | |
415 | + rules: [{ required: true, message: '属性为必填项' }], | |
416 | + componentProps({ formModel, formActionType }) { | |
417 | + const { updateSchema, setFieldsValue } = formActionType; | |
418 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | |
419 | + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE]; | |
420 | + const deviceId = formModel[DataSourceField.DEVICE_ID]; | |
421 | + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID]; | |
422 | + let attrs: Record<'label' | 'value', string>[] = []; | |
423 | + | |
424 | + return { | |
425 | + api: async () => { | |
426 | + if (organizationId && deviceId) { | |
427 | + try { | |
428 | + if (isGatewayDevice && slaveDeviceId) { | |
429 | + return (attrs = await getDeviceAttribute(slaveDeviceId)); | |
430 | + } | |
431 | + if (!isGatewayDevice) { | |
432 | + return (attrs = await getDeviceAttribute(deviceId)); | |
433 | + } | |
434 | + } catch (error) {} | |
435 | + } | |
436 | + return []; | |
437 | + }, | |
438 | + onChange: (value: string) => { | |
439 | + if (!value) return; | |
440 | + setFieldsValue({ [DataSourceField.LONGITUDE_ATTRIBUTE]: null }); | |
441 | + updateSchema({ | |
442 | + field: DataSourceField.LATITUDE_ATTRIBUTE, | |
443 | + componentProps: { | |
444 | + options: attrs.filter((item) => item.value !== value), | |
445 | + }, | |
446 | + }); | |
447 | + }, | |
448 | + placeholder: '请输入纬度属性', | |
449 | + getPopupContainer: () => document.body, | |
450 | + }; | |
451 | + }, | |
452 | + }, | |
453 | +]; | ... | ... |