Commit c31c36688f9f34f932ee773a3bc4d25bffc557e2
Merge branch 'ww' into 'main'
feat: 设备列表新增设备导入模版下载 See merge request yunteng/thingskit-front!509
Showing
6 changed files
with
118 additions
and
64 deletions
1 | -<script lang="ts" setup> | ||
2 | - import { Icon } from '/@/components/Icon'; | ||
3 | - import { computed, ExtractPropTypes, unref } from 'vue'; | ||
4 | - import { usePermission } from '/@/hooks/web/usePermission'; | ||
5 | - | ||
6 | - interface AuthIconProps extends ExtractPropTypes<typeof Icon> { | ||
7 | - auth?: string; | ||
8 | - } | ||
9 | - | ||
10 | - const props = defineProps<AuthIconProps>(); | ||
11 | - | ||
12 | - const emit = defineEmits(['click']); | ||
13 | - | ||
14 | - const { hasPermission } = usePermission(); | ||
15 | - | ||
16 | - const getHasPermission = computed(() => { | ||
17 | - const { auth } = props; | ||
18 | - console.log(auth); | ||
19 | - return auth ? hasPermission(auth) : true; | ||
20 | - }); | ||
21 | - | ||
22 | - const getBindProps = computed(() => { | ||
23 | - return { | ||
24 | - ...props, | ||
25 | - ...(unref(getHasPermission) ? { onClick: (event: Event) => emit('click', event) } : {}), | ||
26 | - }; | ||
27 | - }); | ||
28 | -</script> | ||
29 | - | ||
30 | -<template> | ||
31 | - <Icon | ||
32 | - v-bind="getBindProps" | ||
33 | - class="justify-center items-center" | ||
34 | - :class="getHasPermission ? '' : '!cursor-not-allowed !text-gray-200'" | ||
35 | - /> | ||
36 | -</template> | 1 | +<script lang="ts" setup> |
2 | + import { Icon } from '/@/components/Icon'; | ||
3 | + import { computed, ExtractPropTypes, unref } from 'vue'; | ||
4 | + import { usePermission } from '/@/hooks/web/usePermission'; | ||
5 | + | ||
6 | + interface AuthIconProps extends ExtractPropTypes<typeof Icon> { | ||
7 | + auth?: string; | ||
8 | + } | ||
9 | + | ||
10 | + const props = defineProps<AuthIconProps>(); | ||
11 | + | ||
12 | + const emit = defineEmits(['click']); | ||
13 | + | ||
14 | + const { hasPermission } = usePermission(); | ||
15 | + | ||
16 | + const getHasPermission = computed(() => { | ||
17 | + const { auth } = props; | ||
18 | + return auth ? hasPermission(auth) : true; | ||
19 | + }); | ||
20 | + | ||
21 | + const getBindProps = computed(() => { | ||
22 | + return { | ||
23 | + ...props, | ||
24 | + ...(unref(getHasPermission) ? { onClick: (event: Event) => emit('click', event) } : {}), | ||
25 | + }; | ||
26 | + }); | ||
27 | +</script> | ||
28 | + | ||
29 | +<template> | ||
30 | + <Icon | ||
31 | + v-bind="getBindProps" | ||
32 | + class="justify-center items-center" | ||
33 | + :class="getHasPermission ? '' : '!cursor-not-allowed !text-gray-200'" | ||
34 | + /> | ||
35 | +</template> |
@@ -637,7 +637,6 @@ export const TokenSchemas: FormSchema[] = [ | @@ -637,7 +637,6 @@ export const TokenSchemas: FormSchema[] = [ | ||
637 | label: '客户端ID', | 637 | label: '客户端ID', |
638 | component: 'Input', | 638 | component: 'Input', |
639 | field: 'clientId', | 639 | field: 'clientId', |
640 | - required: true, | ||
641 | ifShow: false, | 640 | ifShow: false, |
642 | slot: 'clientId', | 641 | slot: 'clientId', |
643 | componentProps: { | 642 | componentProps: { |
1 | <script lang="ts" setup> | 1 | <script lang="ts" setup> |
2 | import { Button } from 'ant-design-vue'; | 2 | import { Button } from 'ant-design-vue'; |
3 | - import { nextTick, onMounted, ref } from 'vue'; | 3 | + import { computed, nextTick, onMounted } from 'vue'; |
4 | import { basicInfoForm, FieldsEnum } from './config'; | 4 | import { basicInfoForm, FieldsEnum } from './config'; |
5 | import { basicProps } from './props'; | 5 | import { basicProps } from './props'; |
6 | import StepContainer from './StepContainer.vue'; | 6 | import StepContainer from './StepContainer.vue'; |
@@ -15,7 +15,14 @@ | @@ -15,7 +15,14 @@ | ||
15 | 15 | ||
16 | const emit = defineEmits(['update:value']); | 16 | const emit = defineEmits(['update:value']); |
17 | 17 | ||
18 | - const canGoNext = ref(false); | 18 | + const canGoNext = computed(() => { |
19 | + const values = props.value as Recordable; | ||
20 | + return ( | ||
21 | + Reflect.has(values, FieldsEnum.ORGANIZATION_ID) && | ||
22 | + Reflect.has(values, FieldsEnum.TK_DEVICE_PROFILE_ID) && | ||
23 | + Reflect.has(values, FieldsEnum.DEVICE_TYPE) | ||
24 | + ); | ||
25 | + }); | ||
19 | const [register, { getFieldsValue, setFieldsValue }] = useForm({ | 26 | const [register, { getFieldsValue, setFieldsValue }] = useForm({ |
20 | schemas: basicInfoForm, | 27 | schemas: basicInfoForm, |
21 | layout: 'vertical', | 28 | layout: 'vertical', |
@@ -23,9 +30,7 @@ | @@ -23,9 +30,7 @@ | ||
23 | submitFunc: async () => { | 30 | submitFunc: async () => { |
24 | await nextTick(); | 31 | await nextTick(); |
25 | const values = getFieldsValue() || {}; | 32 | const values = getFieldsValue() || {}; |
26 | - canGoNext.value = | ||
27 | - Reflect.has(values, FieldsEnum.ORGANIZATION_ID) && | ||
28 | - Reflect.has(values, FieldsEnum.TK_DEVICE_PROFILE_ID); | 33 | + emit('update:value', values); |
29 | }, | 34 | }, |
30 | }); | 35 | }); |
31 | 36 |
1 | import { Options } from './type'; | 1 | import { Options } from './type'; |
2 | -import { getDeviceProfile } from '/@/api/alarm/position'; | 2 | +import { queryDeviceProfileBy } from '/@/api/device/deviceManager'; |
3 | +import { findDictItemByCode } from '/@/api/system/dict'; | ||
3 | import { getOrganizationList } from '/@/api/system/system'; | 4 | import { getOrganizationList } from '/@/api/system/system'; |
4 | import { FormSchema } from '/@/components/Form'; | 5 | import { FormSchema } from '/@/components/Form'; |
5 | import { BasicColumn } from '/@/components/Table'; | 6 | import { BasicColumn } from '/@/components/Table'; |
@@ -7,8 +8,8 @@ import { copyTransFun } from '/@/utils/fnUtils'; | @@ -7,8 +8,8 @@ import { copyTransFun } from '/@/utils/fnUtils'; | ||
7 | 8 | ||
8 | export enum FieldsEnum { | 9 | export enum FieldsEnum { |
9 | ORGANIZATION_ID = 'organizationId', | 10 | ORGANIZATION_ID = 'organizationId', |
11 | + DEVICE_TYPE = 'deviceTypeEnum', | ||
10 | TK_DEVICE_PROFILE_ID = 'tkDeviceProfileId', | 12 | TK_DEVICE_PROFILE_ID = 'tkDeviceProfileId', |
11 | - DEVICE_TYPE_ENUM = 'deviceTypeEnum', | ||
12 | DEVICE_TYPE_NAME = 'deviceTypeName', | 13 | DEVICE_TYPE_NAME = 'deviceTypeName', |
13 | DELIMITER = 'delimiter', | 14 | DELIMITER = 'delimiter', |
14 | HEADER = 'header', | 15 | HEADER = 'header', |
@@ -118,36 +119,54 @@ export const basicInfoForm: FormSchema[] = [ | @@ -118,36 +119,54 @@ export const basicInfoForm: FormSchema[] = [ | ||
118 | }, | 119 | }, |
119 | }, | 120 | }, |
120 | { | 121 | { |
122 | + field: FieldsEnum.DEVICE_TYPE, | ||
123 | + label: '设备类型', | ||
124 | + component: 'ApiSelect', | ||
125 | + rules: [{ required: true, message: '设备类型为必填项' }], | ||
126 | + componentProps: ({ formActionType }) => { | ||
127 | + const { setFieldsValue, submit, clearValidate } = formActionType; | ||
128 | + return { | ||
129 | + api: findDictItemByCode, | ||
130 | + params: { | ||
131 | + dictCode: 'device_type', | ||
132 | + }, | ||
133 | + labelField: 'itemText', | ||
134 | + valueField: 'itemValue', | ||
135 | + onChange: (value: string, options: Record<'deviceType' | 'label', string>) => { | ||
136 | + setFieldsValue({ | ||
137 | + [FieldsEnum.DEVICE_TYPE_NAME]: value ? options.label : null, | ||
138 | + [FieldsEnum.TK_DEVICE_PROFILE_ID]: null, | ||
139 | + }); | ||
140 | + submit(); | ||
141 | + clearValidate(); | ||
142 | + }, | ||
143 | + }; | ||
144 | + }, | ||
145 | + }, | ||
146 | + { | ||
121 | field: FieldsEnum.TK_DEVICE_PROFILE_ID, | 147 | field: FieldsEnum.TK_DEVICE_PROFILE_ID, |
122 | component: 'ApiSelect', | 148 | component: 'ApiSelect', |
123 | label: '产品', | 149 | label: '产品', |
124 | rules: [{ required: true, message: '产品为必填项' }], | 150 | rules: [{ required: true, message: '产品为必填项' }], |
125 | - componentProps: ({ formActionType }) => { | ||
126 | - const { submit, setFieldsValue } = formActionType; | 151 | + componentProps: ({ formActionType, formModel }) => { |
152 | + const { submit } = formActionType; | ||
153 | + const deviceType = Reflect.get(formModel, FieldsEnum.DEVICE_TYPE); | ||
127 | return { | 154 | return { |
128 | - api: getDeviceProfile, | 155 | + api: queryDeviceProfileBy, |
129 | labelField: 'name', | 156 | labelField: 'name', |
130 | valueField: 'id', | 157 | valueField: 'id', |
131 | placeholder: '请选择产品', | 158 | placeholder: '请选择产品', |
159 | + params: { deviceType }, | ||
132 | getPopupContainer: () => document.body, | 160 | getPopupContainer: () => document.body, |
133 | - onChange: (value: string, options: Record<'deviceType' | 'label', string>) => { | ||
134 | - value && | ||
135 | - setFieldsValue({ | ||
136 | - [FieldsEnum.DEVICE_TYPE_ENUM]: options.deviceType, | ||
137 | - [FieldsEnum.DEVICE_TYPE_NAME]: options.label, | ||
138 | - }); | ||
139 | - submit(); | 161 | + onChange: () => submit(), |
162 | + showSearch: true, | ||
163 | + filterOption: (inputValue: string, options: Record<'label', string>) => { | ||
164 | + return options.label.includes(inputValue); | ||
140 | }, | 165 | }, |
141 | }; | 166 | }; |
142 | }, | 167 | }, |
143 | }, | 168 | }, |
144 | { | 169 | { |
145 | - field: FieldsEnum.DEVICE_TYPE_ENUM, | ||
146 | - component: 'Input', | ||
147 | - label: '设备类型', | ||
148 | - show: false, | ||
149 | - }, | ||
150 | - { | ||
151 | field: FieldsEnum.DEVICE_TYPE_NAME, | 170 | field: FieldsEnum.DEVICE_TYPE_NAME, |
152 | component: 'Input', | 171 | component: 'Input', |
153 | label: '设备名称', | 172 | label: '设备名称', |
@@ -226,4 +245,20 @@ export const columnTypeSchema: BasicColumn[] = [ | @@ -226,4 +245,20 @@ export const columnTypeSchema: BasicColumn[] = [ | ||
226 | }, | 245 | }, |
227 | ]; | 246 | ]; |
228 | 247 | ||
229 | -export const assemblyData = () => {}; | 248 | +export const csvTemplate = `name,username,password |
249 | +Device 11,admin,password | ||
250 | +Device 22,admin1,password1 | ||
251 | +`; | ||
252 | + | ||
253 | +export const downloadFile = (data: string, fileName: string, type: string, ext: string) => { | ||
254 | + const blob = new Blob([data], { type: type }); | ||
255 | + const objectURL = URL.createObjectURL(blob); | ||
256 | + const element = document.createElement('a'); | ||
257 | + element.href = objectURL; | ||
258 | + element.download = `${fileName}.${ext}`; | ||
259 | + element.style.display = 'none'; | ||
260 | + document.body.appendChild(element); | ||
261 | + element.click(); | ||
262 | + element.remove(); | ||
263 | + URL.revokeObjectURL(objectURL); | ||
264 | +}; |
1 | <script lang="ts" setup> | 1 | <script lang="ts" setup> |
2 | import { computed, ref, unref } from 'vue'; | 2 | import { computed, ref, unref } from 'vue'; |
3 | import { BasicModal, useModal } from '/@/components/Modal'; | 3 | import { BasicModal, useModal } from '/@/components/Modal'; |
4 | - import { Steps } from 'ant-design-vue'; | 4 | + import { Steps, Button, Tooltip } from 'ant-design-vue'; |
5 | import BasicInfoForm from './BasicInfoForm.vue'; | 5 | import BasicInfoForm from './BasicInfoForm.vue'; |
6 | import ImportCsv from './ImportCsv.vue'; | 6 | import ImportCsv from './ImportCsv.vue'; |
7 | import { CreateEntityValue, UploadFileParseValue } from './type'; | 7 | import { CreateEntityValue, UploadFileParseValue } from './type'; |
@@ -9,6 +9,7 @@ | @@ -9,6 +9,7 @@ | ||
9 | import CreateEntity from './CreateEntity.vue'; | 9 | import CreateEntity from './CreateEntity.vue'; |
10 | import CompleteResult from './CompleteResult.vue'; | 10 | import CompleteResult from './CompleteResult.vue'; |
11 | import { ImportDeviceResponse } from '/@/api/device/model/batchImportModel'; | 11 | import { ImportDeviceResponse } from '/@/api/device/model/batchImportModel'; |
12 | + import { csvTemplate, downloadFile } from './config'; | ||
12 | 13 | ||
13 | const emit = defineEmits(['importFinally']); | 14 | const emit = defineEmits(['importFinally']); |
14 | 15 | ||
@@ -73,6 +74,10 @@ | @@ -73,6 +74,10 @@ | ||
73 | reset(); | 74 | reset(); |
74 | return true; | 75 | return true; |
75 | }; | 76 | }; |
77 | + | ||
78 | + const handleTemplateDownload = () => { | ||
79 | + downloadFile(csvTemplate, 'template', 'text/csv', 'csv'); | ||
80 | + }; | ||
76 | </script> | 81 | </script> |
77 | 82 | ||
78 | <template> | 83 | <template> |
@@ -86,7 +91,7 @@ | @@ -86,7 +91,7 @@ | ||
86 | :close-func="handleCancel" | 91 | :close-func="handleCancel" |
87 | :destroy-on-close="true" | 92 | :destroy-on-close="true" |
88 | > | 93 | > |
89 | - <section class="overflow-auto"> | 94 | + <section class="overflow-auto relative"> |
90 | <Steps direction="vertical" :current="currentStep"> | 95 | <Steps direction="vertical" :current="currentStep"> |
91 | <Steps.Step | 96 | <Steps.Step |
92 | v-for="item in Object.keys(StepsEnum).filter((key) => isNaN(key as unknown as number))" | 97 | v-for="item in Object.keys(StepsEnum).filter((key) => isNaN(key as unknown as number))" |
@@ -132,6 +137,17 @@ | @@ -132,6 +137,17 @@ | ||
132 | </template> | 137 | </template> |
133 | </Steps.Step> | 138 | </Steps.Step> |
134 | </Steps> | 139 | </Steps> |
140 | + <Tooltip | ||
141 | + title="模板表头的第一列为设备名称,且不能重复,如需预置访问凭证(ACCESS_TOKEN、MQTT client ID、MQTT user name、MQTT password)添加新的列即可。例如:username、password" | ||
142 | + > | ||
143 | + <Button | ||
144 | + type="text" | ||
145 | + class="!absolute top-0 right-0 !text-blue-400" | ||
146 | + @click="handleTemplateDownload" | ||
147 | + > | ||
148 | + 模版下载 | ||
149 | + </Button> | ||
150 | + </Tooltip> | ||
135 | </section> | 151 | </section> |
136 | </BasicModal> | 152 | </BasicModal> |
137 | </template> | 153 | </template> |
@@ -21,7 +21,7 @@ | @@ -21,7 +21,7 @@ | ||
21 | </a-button> | 21 | </a-button> |
22 | </Popconfirm> | 22 | </Popconfirm> |
23 | </Authority> | 23 | </Authority> |
24 | - <Authority> | 24 | + <Authority value="api:yt:device:import"> |
25 | <Button type="primary" @click="handleBatchImport">导入</Button> | 25 | <Button type="primary" @click="handleBatchImport">导入</Button> |
26 | </Authority> | 26 | </Authority> |
27 | <a-button | 27 | <a-button |