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 637 label: '客户端ID',
638 638 component: 'Input',
639 639 field: 'clientId',
640   - required: true,
641 640 ifShow: false,
642 641 slot: 'clientId',
643 642 componentProps: {
... ...
1 1 <script lang="ts" setup>
2 2 import { Button } from 'ant-design-vue';
3   - import { nextTick, onMounted, ref } from 'vue';
  3 + import { computed, nextTick, onMounted } from 'vue';
4 4 import { basicInfoForm, FieldsEnum } from './config';
5 5 import { basicProps } from './props';
6 6 import StepContainer from './StepContainer.vue';
... ... @@ -15,7 +15,14 @@
15 15
16 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 26 const [register, { getFieldsValue, setFieldsValue }] = useForm({
20 27 schemas: basicInfoForm,
21 28 layout: 'vertical',
... ... @@ -23,9 +30,7 @@
23 30 submitFunc: async () => {
24 31 await nextTick();
25 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 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 4 import { getOrganizationList } from '/@/api/system/system';
4 5 import { FormSchema } from '/@/components/Form';
5 6 import { BasicColumn } from '/@/components/Table';
... ... @@ -7,8 +8,8 @@ import { copyTransFun } from '/@/utils/fnUtils';
7 8
8 9 export enum FieldsEnum {
9 10 ORGANIZATION_ID = 'organizationId',
  11 + DEVICE_TYPE = 'deviceTypeEnum',
10 12 TK_DEVICE_PROFILE_ID = 'tkDeviceProfileId',
11   - DEVICE_TYPE_ENUM = 'deviceTypeEnum',
12 13 DEVICE_TYPE_NAME = 'deviceTypeName',
13 14 DELIMITER = 'delimiter',
14 15 HEADER = 'header',
... ... @@ -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 147 field: FieldsEnum.TK_DEVICE_PROFILE_ID,
122 148 component: 'ApiSelect',
123 149 label: '产品',
124 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 154 return {
128   - api: getDeviceProfile,
  155 + api: queryDeviceProfileBy,
129 156 labelField: 'name',
130 157 valueField: 'id',
131 158 placeholder: '请选择产品',
  159 + params: { deviceType },
132 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 170 field: FieldsEnum.DEVICE_TYPE_NAME,
152 171 component: 'Input',
153 172 label: '设备名称',
... ... @@ -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 1 <script lang="ts" setup>
2 2 import { computed, ref, unref } from 'vue';
3 3 import { BasicModal, useModal } from '/@/components/Modal';
4   - import { Steps } from 'ant-design-vue';
  4 + import { Steps, Button, Tooltip } from 'ant-design-vue';
5 5 import BasicInfoForm from './BasicInfoForm.vue';
6 6 import ImportCsv from './ImportCsv.vue';
7 7 import { CreateEntityValue, UploadFileParseValue } from './type';
... ... @@ -9,6 +9,7 @@
9 9 import CreateEntity from './CreateEntity.vue';
10 10 import CompleteResult from './CompleteResult.vue';
11 11 import { ImportDeviceResponse } from '/@/api/device/model/batchImportModel';
  12 + import { csvTemplate, downloadFile } from './config';
12 13
13 14 const emit = defineEmits(['importFinally']);
14 15
... ... @@ -73,6 +74,10 @@
73 74 reset();
74 75 return true;
75 76 };
  77 +
  78 + const handleTemplateDownload = () => {
  79 + downloadFile(csvTemplate, 'template', 'text/csv', 'csv');
  80 + };
76 81 </script>
77 82
78 83 <template>
... ... @@ -86,7 +91,7 @@
86 91 :close-func="handleCancel"
87 92 :destroy-on-close="true"
88 93 >
89   - <section class="overflow-auto">
  94 + <section class="overflow-auto relative">
90 95 <Steps direction="vertical" :current="currentStep">
91 96 <Steps.Step
92 97 v-for="item in Object.keys(StepsEnum).filter((key) => isNaN(key as unknown as number))"
... ... @@ -132,6 +137,17 @@
132 137 </template>
133 138 </Steps.Step>
134 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 151 </section>
136 152 </BasicModal>
137 153 </template>
... ...
... ... @@ -21,7 +21,7 @@
21 21 </a-button>
22 22 </Popconfirm>
23 23 </Authority>
24   - <Authority>
  24 + <Authority value="api:yt:device:import">
25 25 <Button type="primary" @click="handleBatchImport">导入</Button>
26 26 </Authority>
27 27 <a-button
... ...