Commit c31c36688f9f34f932ee773a3bc4d25bffc557e2

Authored by xp.Huang
2 parents 3661daea 777e82cd

Merge branch 'ww' into 'main'

feat: 设备列表新增设备导入模版下载

See merge request yunteng/thingskit-front!509
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