Showing
16 changed files
with
1003 additions
and
3 deletions
src/api/device/batchImport.ts
0 → 100644
1 | +import { ImportDeviceParams, ImportDeviceResponse } from './model/batchImportModel'; | ||
2 | +import { defHttp } from '/@/utils/http/axios'; | ||
3 | + | ||
4 | +enum BatchImportApi { | ||
5 | + IMPORT = '/device/batch_import', | ||
6 | +} | ||
7 | + | ||
8 | +export const batchImportDevice = (data: ImportDeviceParams) => { | ||
9 | + return defHttp.post<ImportDeviceResponse>( | ||
10 | + { | ||
11 | + url: BatchImportApi.IMPORT, | ||
12 | + data, | ||
13 | + }, | ||
14 | + { joinPrefix: false } | ||
15 | + ); | ||
16 | +}; |
src/api/device/model/batchImportModel.ts
0 → 100644
1 | +import { DeviceTypeEnum } from './deviceModel'; | ||
2 | + | ||
3 | +export interface ImportDeviceParams { | ||
4 | + file: string; | ||
5 | + tkDeviceProfileId: string; | ||
6 | + organizationId: string; | ||
7 | + deviceTypeEnum: DeviceTypeEnum; | ||
8 | + mapping: { | ||
9 | + columns: Record<'type', string>[]; | ||
10 | + delimiter: string; | ||
11 | + header: boolean; | ||
12 | + update: boolean; | ||
13 | + }; | ||
14 | +} | ||
15 | + | ||
16 | +export interface ImportDeviceResponse { | ||
17 | + created: number; | ||
18 | + updated: number; | ||
19 | + errors: number; | ||
20 | + errorsList: []; | ||
21 | +} |
1 | +<script lang="ts" setup> | ||
2 | + import { Button } from 'ant-design-vue'; | ||
3 | + import { nextTick, onMounted, ref } from 'vue'; | ||
4 | + import { basicInfoForm, FieldsEnum } from './config'; | ||
5 | + import { basicProps } from './props'; | ||
6 | + import StepContainer from './StepContainer.vue'; | ||
7 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
8 | + | ||
9 | + const props = defineProps({ | ||
10 | + ...basicProps, | ||
11 | + value: { | ||
12 | + required: true, | ||
13 | + }, | ||
14 | + }); | ||
15 | + | ||
16 | + const emit = defineEmits(['update:value']); | ||
17 | + | ||
18 | + const canGoNext = ref(false); | ||
19 | + const [register, { getFieldsValue, setFieldsValue }] = useForm({ | ||
20 | + schemas: basicInfoForm, | ||
21 | + layout: 'vertical', | ||
22 | + showActionButtonGroup: false, | ||
23 | + submitFunc: async () => { | ||
24 | + await nextTick(); | ||
25 | + const values = getFieldsValue() || {}; | ||
26 | + canGoNext.value = | ||
27 | + Reflect.has(values, FieldsEnum.ORGANIZATION_ID) && | ||
28 | + Reflect.has(values, FieldsEnum.TK_DEVICE_PROFILE_ID); | ||
29 | + }, | ||
30 | + }); | ||
31 | + | ||
32 | + const handleGoNext = () => { | ||
33 | + emit('update:value', getFieldsValue()); | ||
34 | + props.goNextStep?.(); | ||
35 | + }; | ||
36 | + | ||
37 | + onMounted(() => { | ||
38 | + setFieldsValue(props.value || {}); | ||
39 | + }); | ||
40 | +</script> | ||
41 | + | ||
42 | +<template> | ||
43 | + <StepContainer> | ||
44 | + <BasicForm @register="register" /> | ||
45 | + <div class="flex justify-end gap-2"> | ||
46 | + <Button type="primary" @click="handleGoNext" :disabled="!canGoNext">下一步</Button> | ||
47 | + </div> | ||
48 | + </StepContainer> | ||
49 | +</template> |
1 | +<script lang="ts" setup> | ||
2 | + import { Select, Button } from 'ant-design-vue'; | ||
3 | + import { basicProps } from './props'; | ||
4 | + import StepContainer from './StepContainer.vue'; | ||
5 | + import { BasicTable, useTable } from '/@/components/Table'; | ||
6 | + import { | ||
7 | + columnTypeSchema, | ||
8 | + ColumnFileEnum, | ||
9 | + ColumTypeEnum, | ||
10 | + generateColumnTypeOptions, | ||
11 | + } from './config'; | ||
12 | + import { nextTick, onMounted, ref, unref, watch } from 'vue'; | ||
13 | + import { ColumnDataRecord, Options, UploadFileParseValue } from './type'; | ||
14 | + import { DEVICE_NAME_INDEX } from './const'; | ||
15 | + import { buildUUID } from '/@/utils/uuid'; | ||
16 | + | ||
17 | + const props = defineProps({ | ||
18 | + ...basicProps, | ||
19 | + value: { | ||
20 | + required: true, | ||
21 | + type: Array as PropType<Record<'type', string>[]>, | ||
22 | + }, | ||
23 | + fileParseValue: { | ||
24 | + require: true, | ||
25 | + type: Object as PropType<UploadFileParseValue>, | ||
26 | + }, | ||
27 | + }); | ||
28 | + | ||
29 | + const emit = defineEmits(['update:value']); | ||
30 | + | ||
31 | + const columnTypeRepeatWhiteList = [ | ||
32 | + ColumTypeEnum.SERVER_ATTRIBUTE, | ||
33 | + ColumTypeEnum.SHARED_ATTRIBUTE, | ||
34 | + ColumTypeEnum.TIMESERIES, | ||
35 | + ]; | ||
36 | + | ||
37 | + const [register, { setProps, getDataSource }] = useTable({ | ||
38 | + size: 'small', | ||
39 | + rowKey: 'id', | ||
40 | + showIndexColumn: false, | ||
41 | + columns: columnTypeSchema, | ||
42 | + resizeHeightOffset: -300, | ||
43 | + }); | ||
44 | + | ||
45 | + const parseValueToTableData = () => { | ||
46 | + const { header = [], content = [] } = props.fileParseValue || {}; | ||
47 | + const exampleRow = content.at(0) || {}; | ||
48 | + | ||
49 | + const getColumnType = (index: number) => { | ||
50 | + return index === DEVICE_NAME_INDEX ? ColumTypeEnum.NAME : ColumTypeEnum.SERVER_ATTRIBUTE; | ||
51 | + }; | ||
52 | + | ||
53 | + const dataSource = header.map((columnKey, index) => { | ||
54 | + return { | ||
55 | + [ColumnFileEnum.EXAMPLE_VALUE]: exampleRow[columnKey], | ||
56 | + [ColumnFileEnum.TYPE]: getColumnType(index), | ||
57 | + [ColumnFileEnum.KEY]: columnKey, | ||
58 | + id: buildUUID(), | ||
59 | + } as ColumnDataRecord; | ||
60 | + }); | ||
61 | + | ||
62 | + setProps({ | ||
63 | + dataSource, | ||
64 | + pagination: { pageSize: dataSource.length }, | ||
65 | + maxHeight: 40 * dataSource.length, | ||
66 | + }); | ||
67 | + }; | ||
68 | + | ||
69 | + const columnTypeOptions = ref(generateColumnTypeOptions()); | ||
70 | + | ||
71 | + const handleDisableSelectedOption = () => { | ||
72 | + let dataSource = getDataSource().reduce<string[]>( | ||
73 | + (prev, next) => [...prev, next[ColumnFileEnum.TYPE], next[ColumnFileEnum.OLD_VALUE]], | ||
74 | + [] | ||
75 | + ); | ||
76 | + | ||
77 | + dataSource = [...new Set(dataSource.filter(Boolean))]; | ||
78 | + | ||
79 | + const disableSelectedOptions = ( | ||
80 | + options: Options[], | ||
81 | + whiteList: string[] = columnTypeRepeatWhiteList | ||
82 | + ) => { | ||
83 | + for (const item of options) { | ||
84 | + if (item.options?.length) { | ||
85 | + disableSelectedOptions(item.options); | ||
86 | + continue; | ||
87 | + } | ||
88 | + if (whiteList.includes(item.value!)) continue; | ||
89 | + item.disabled = dataSource.includes(item.value!); | ||
90 | + } | ||
91 | + }; | ||
92 | + | ||
93 | + disableSelectedOptions(unref(columnTypeOptions)); | ||
94 | + }; | ||
95 | + | ||
96 | + const getColumnNeedDisabled = (index: number) => { | ||
97 | + return index === DEVICE_NAME_INDEX; | ||
98 | + }; | ||
99 | + | ||
100 | + const handleGoPreviousStep = () => { | ||
101 | + props.goPreviousStep?.(); | ||
102 | + }; | ||
103 | + | ||
104 | + const handleGoNextStep = () => { | ||
105 | + const columns = getDataSource<ColumnDataRecord>().map((item) => ({ type: item.type })); | ||
106 | + emit('update:value', columns); | ||
107 | + props.goNextStep?.(); | ||
108 | + }; | ||
109 | + | ||
110 | + watch( | ||
111 | + () => props.fileParseValue, | ||
112 | + () => { | ||
113 | + parseValueToTableData(); | ||
114 | + } | ||
115 | + ); | ||
116 | + | ||
117 | + onMounted(async () => { | ||
118 | + parseValueToTableData(); | ||
119 | + await nextTick(); | ||
120 | + handleDisableSelectedOption(); | ||
121 | + }); | ||
122 | +</script> | ||
123 | + | ||
124 | +<template> | ||
125 | + <StepContainer> | ||
126 | + <BasicTable class="import-device-column-type-table" @register="register"> | ||
127 | + <template #type="{ record, index }"> | ||
128 | + <section | ||
129 | + class="select-column-type-container flex relative" | ||
130 | + :class="!record.editable && 'justify-center'" | ||
131 | + > | ||
132 | + <Select | ||
133 | + v-model:value="record[ColumnFileEnum.TYPE]" | ||
134 | + @change="handleDisableSelectedOption" | ||
135 | + :disabled="getColumnNeedDisabled(index)" | ||
136 | + :options="columnTypeOptions" | ||
137 | + :dropdown-match-select-width="false" | ||
138 | + /> | ||
139 | + </section> | ||
140 | + </template> | ||
141 | + <template #key="{ record }"> | ||
142 | + <div> | ||
143 | + <span>{{ record[ColumnFileEnum.KEY] }}</span> | ||
144 | + </div> | ||
145 | + </template> | ||
146 | + </BasicTable> | ||
147 | + <section class="flex justify-end gap-4 mt-4"> | ||
148 | + <Button type="primary" @click="handleGoPreviousStep">上一步</Button> | ||
149 | + <Button type="primary" @click="handleGoNextStep">下一步</Button> | ||
150 | + </section> | ||
151 | + </StepContainer> | ||
152 | +</template> | ||
153 | + | ||
154 | +<style lang="less" scoped> | ||
155 | + .import-device-column-type-table { | ||
156 | + .ant-select { | ||
157 | + width: 80%; | ||
158 | + } | ||
159 | + } | ||
160 | +</style> | ||
161 | + | ||
162 | +<style lang="less"> | ||
163 | + .import-device-column-type-table { | ||
164 | + .ant-select, | ||
165 | + .ant-input, | ||
166 | + .ant-select-selector { | ||
167 | + height: 22px !important; | ||
168 | + } | ||
169 | + | ||
170 | + .ant-select-selection-item { | ||
171 | + line-height: 22px !important; | ||
172 | + } | ||
173 | + } | ||
174 | +</style> |
1 | +<script lang="ts" setup> | ||
2 | + import StepContainer from './StepContainer.vue'; | ||
3 | + import { ImportDeviceResponse } from '/@/api/device/model/batchImportModel'; | ||
4 | + | ||
5 | + const props = defineProps({ | ||
6 | + value: { | ||
7 | + required: true, | ||
8 | + type: Object as PropType<ImportDeviceResponse>, | ||
9 | + }, | ||
10 | + }); | ||
11 | +</script> | ||
12 | + | ||
13 | +<template> | ||
14 | + <StepContainer> | ||
15 | + <div>创建成功:{{ props.value.created }}</div> | ||
16 | + <div>错误:{{ props.value.errors }}</div> | ||
17 | + <div>错误列表:{{ props.value.errorsList }}</div> | ||
18 | + <div>更新:{{ props.value.updated }}</div> | ||
19 | + </StepContainer> | ||
20 | +</template> |
1 | +<script lang="ts" setup> | ||
2 | + import { Spin } from 'ant-design-vue'; | ||
3 | + import { onMounted, ref } from 'vue'; | ||
4 | + import { DelimiterEnum } from './config'; | ||
5 | + import { basicProps } from './props'; | ||
6 | + import StepContainer from './StepContainer.vue'; | ||
7 | + import { CreateEntityValue, UploadFileParseValue } from './type'; | ||
8 | + import { batchImportDevice } from '/@/api/device/batchImport'; | ||
9 | + import { ImportDeviceParams, ImportDeviceResponse } from '/@/api/device/model/batchImportModel'; | ||
10 | + | ||
11 | + const props = defineProps({ | ||
12 | + ...basicProps, | ||
13 | + value: { | ||
14 | + required: true, | ||
15 | + type: Object as PropType<CreateEntityValue>, | ||
16 | + }, | ||
17 | + result: { | ||
18 | + required: true, | ||
19 | + type: Object as PropType<ImportDeviceResponse>, | ||
20 | + }, | ||
21 | + }); | ||
22 | + | ||
23 | + const emit = defineEmits(['update:result']); | ||
24 | + | ||
25 | + const spinning = ref(true); | ||
26 | + | ||
27 | + const sleep = (time: number) => { | ||
28 | + return new Promise((resolve) => { | ||
29 | + setTimeout(() => { | ||
30 | + resolve(time); | ||
31 | + }, time); | ||
32 | + }); | ||
33 | + }; | ||
34 | + | ||
35 | + const submit = async () => { | ||
36 | + spinning.value = true; | ||
37 | + try { | ||
38 | + const value = transfromData(JSON.parse(JSON.stringify(props.value))); | ||
39 | + const result = await batchImportDevice(value); | ||
40 | + await sleep(3000); | ||
41 | + emit('update:result', result); | ||
42 | + props.goNextStep?.(); | ||
43 | + } catch (error) { | ||
44 | + throw error; | ||
45 | + } finally { | ||
46 | + spinning.value = false; | ||
47 | + } | ||
48 | + }; | ||
49 | + | ||
50 | + const insertDeviceTypeName = ( | ||
51 | + deviceTypeName: string, | ||
52 | + fileParseValue: UploadFileParseValue, | ||
53 | + columns: Record<'type', string>[] | ||
54 | + ): { file: string; columns: Record<'type', string>[] } => { | ||
55 | + const { header, content } = fileParseValue; | ||
56 | + const insertIndex = 1; | ||
57 | + | ||
58 | + const csvArray = content.map((item) => header.map((key) => item[key]) as string[]); | ||
59 | + for (const item of csvArray) { | ||
60 | + item.splice(insertIndex, 0, deviceTypeName); | ||
61 | + } | ||
62 | + | ||
63 | + const _header = [...header]; | ||
64 | + _header.splice(insertIndex, 0, deviceTypeName); | ||
65 | + csvArray.unshift(_header); | ||
66 | + const file = csvArray.map((item) => item.join(DelimiterEnum.COMMA)).join('\n'); | ||
67 | + | ||
68 | + columns.splice(insertIndex, 0, { type: 'TYPE' }); | ||
69 | + | ||
70 | + return { | ||
71 | + file, | ||
72 | + columns, | ||
73 | + }; | ||
74 | + }; | ||
75 | + | ||
76 | + const transfromData = (data: CreateEntityValue) => { | ||
77 | + const { basicInfo, columnConfiguration, fileParseValue } = data; | ||
78 | + const { tkDeviceProfileId, organizationId, deviceTypeEnum, deviceTypeName } = basicInfo; | ||
79 | + const { file, columns } = insertDeviceTypeName( | ||
80 | + deviceTypeName, | ||
81 | + fileParseValue, | ||
82 | + columnConfiguration | ||
83 | + ); | ||
84 | + return { | ||
85 | + file, | ||
86 | + tkDeviceProfileId, | ||
87 | + organizationId, | ||
88 | + deviceTypeEnum, | ||
89 | + mapping: { | ||
90 | + columns: columns, | ||
91 | + delimiter: DelimiterEnum.COMMA, | ||
92 | + header: true, | ||
93 | + update: true, | ||
94 | + }, | ||
95 | + } as ImportDeviceParams; | ||
96 | + }; | ||
97 | + | ||
98 | + onMounted(() => { | ||
99 | + submit(); | ||
100 | + }); | ||
101 | +</script> | ||
102 | + | ||
103 | +<template> | ||
104 | + <StepContainer class="flex justify-center"> | ||
105 | + <Spin :spinning="spinning" tip="正在创建..." /> | ||
106 | + </StepContainer> | ||
107 | +</template> |
1 | +<script lang="ts" setup> | ||
2 | + import { Button } from 'ant-design-vue'; | ||
3 | + import { importConfigurationSchema } from './config'; | ||
4 | + import { basicProps } from './props'; | ||
5 | + import StepContainer from './StepContainer.vue'; | ||
6 | + import { BasicForm, useForm } from '/@/components/Form'; | ||
7 | + const props = defineProps({ | ||
8 | + ...basicProps, | ||
9 | + }); | ||
10 | + | ||
11 | + const [register] = useForm({ | ||
12 | + schemas: importConfigurationSchema, | ||
13 | + showActionButtonGroup: false, | ||
14 | + labelWidth: 120, | ||
15 | + layout: 'vertical', | ||
16 | + }); | ||
17 | + | ||
18 | + const handlePreviousStep = () => { | ||
19 | + props.goNextStep?.(); | ||
20 | + }; | ||
21 | + | ||
22 | + const handleNextStep = () => { | ||
23 | + props.goNextStep?.(); | ||
24 | + }; | ||
25 | +</script> | ||
26 | + | ||
27 | +<template> | ||
28 | + <StepContainer> | ||
29 | + <BasicForm class="import-configuration-form" @register="register" /> | ||
30 | + <div class="flex justify-end gap-4 mt-4"> | ||
31 | + <Button type="primary" @click="handlePreviousStep">上一步</Button> | ||
32 | + <Button type="primary" @click="handleNextStep">下一步</Button> | ||
33 | + </div> | ||
34 | + </StepContainer> | ||
35 | +</template> | ||
36 | + | ||
37 | +<style lang="less"> | ||
38 | + .import-configuration-form { | ||
39 | + .ant-row { | ||
40 | + > .ant-col:nth-of-type(2), | ||
41 | + > .ant-col:nth-of-type(3) { | ||
42 | + > .ant-row { | ||
43 | + flex-direction: row; | ||
44 | + align-items: center; | ||
45 | + | ||
46 | + .ant-form-item-label { | ||
47 | + padding: 0 !important; | ||
48 | + } | ||
49 | + } | ||
50 | + } | ||
51 | + } | ||
52 | + } | ||
53 | +</style> |
1 | +<script lang="ts" setup> | ||
2 | + import { Upload, Button } from 'ant-design-vue'; | ||
3 | + import { InboxOutlined } from '@ant-design/icons-vue'; | ||
4 | + import { computed, ref } from 'vue'; | ||
5 | + import StepContainer from './StepContainer.vue'; | ||
6 | + import XLSX, { CellObject } from 'xlsx'; | ||
7 | + import { basicProps } from './props'; | ||
8 | + import { UploadFileParseValue } from './type'; | ||
9 | + | ||
10 | + const props = defineProps({ | ||
11 | + ...basicProps, | ||
12 | + value: { | ||
13 | + require: true, | ||
14 | + type: Object as PropType<UploadFileParseValue>, | ||
15 | + }, | ||
16 | + }); | ||
17 | + | ||
18 | + const emit = defineEmits(['update:value']); | ||
19 | + | ||
20 | + const fileList = ref<Record<'uid' | 'name', string>[]>([]); | ||
21 | + | ||
22 | + interface FileRequestParams { | ||
23 | + file: File & { uid: string }; | ||
24 | + onSuccess: Fn; | ||
25 | + onProgress: Fn; | ||
26 | + onError: Fn; | ||
27 | + status: string; | ||
28 | + } | ||
29 | + | ||
30 | + const readFile = async (file: File): Promise<UploadFileParseValue | boolean> => { | ||
31 | + /** | ||
32 | + * @description 读取表头 | ||
33 | + * @param sheet 工作表 | ||
34 | + * @param range 区间 | ||
35 | + * @param headerRow 表头行 | ||
36 | + */ | ||
37 | + const getTableHeader = (sheet: XLSX.WorkSheet, range: [number, number], headerRow = 1) => { | ||
38 | + const [startColumn, endColumn] = range; | ||
39 | + const header: string[] = []; | ||
40 | + for (let i = startColumn; i <= endColumn; i++) { | ||
41 | + const columnIndex = XLSX.utils.encode_col(i) + headerRow; | ||
42 | + const value = (sheet[columnIndex] as CellObject).v; | ||
43 | + header.push(value as string); | ||
44 | + } | ||
45 | + return header; | ||
46 | + }; | ||
47 | + | ||
48 | + return new Promise((resolve, reject) => { | ||
49 | + const fileReader = new FileReader(); | ||
50 | + fileReader.onload = (event: ProgressEvent) => { | ||
51 | + const data = (event.target as FileReader).result as string; | ||
52 | + const result = XLSX.read(data, { type: 'string' }); | ||
53 | + | ||
54 | + const sheetName = result.SheetNames.at(0); | ||
55 | + const workbook = result.Sheets; | ||
56 | + const sheet = workbook[sheetName as string]; | ||
57 | + const sheetRange = sheet['!ref']; | ||
58 | + | ||
59 | + const { | ||
60 | + s: { c: startColumn }, | ||
61 | + e: { c: endColumn }, | ||
62 | + } = XLSX.utils.decode_range(sheetRange!); | ||
63 | + | ||
64 | + const header = getTableHeader(sheet, [startColumn, endColumn]); | ||
65 | + const content = XLSX.utils.sheet_to_json(sheet, { range: sheetRange }) as Recordable[]; | ||
66 | + resolve({ header, content }); | ||
67 | + }; | ||
68 | + | ||
69 | + fileReader.onerror = () => { | ||
70 | + reject(false); | ||
71 | + }; | ||
72 | + fileReader.readAsText(file); | ||
73 | + }); | ||
74 | + }; | ||
75 | + | ||
76 | + const handleParseFile = async ({ file, onSuccess, onError }: FileRequestParams) => { | ||
77 | + fileList.value = []; | ||
78 | + const value = await readFile(file); | ||
79 | + if (!value) { | ||
80 | + onError(); | ||
81 | + return; | ||
82 | + } | ||
83 | + fileList.value = [file]; | ||
84 | + emit('update:value', value); | ||
85 | + onSuccess({}, file); | ||
86 | + }; | ||
87 | + | ||
88 | + const canGoNextStep = computed(() => { | ||
89 | + return !!fileList.value.length; | ||
90 | + }); | ||
91 | + | ||
92 | + const handlePreviousStep = () => { | ||
93 | + props.goPreviousStep?.(); | ||
94 | + }; | ||
95 | + | ||
96 | + const handleNextStep = () => { | ||
97 | + props.goNextStep?.(); | ||
98 | + }; | ||
99 | +</script> | ||
100 | + | ||
101 | +<template> | ||
102 | + <StepContainer> | ||
103 | + <div class="">设备文件</div> | ||
104 | + <Upload.Dragger :fileList="fileList" :customRequest="handleParseFile" accept=".csv" name="file"> | ||
105 | + <section class="cursor-pointer flex flex-col justify-center items-center"> | ||
106 | + <InboxOutlined class="text-[4rem] !text-blue-400" /> | ||
107 | + <div class="text-gray-500">点击上传或拖拽上传</div> | ||
108 | + </section> | ||
109 | + </Upload.Dragger> | ||
110 | + <div class="flex justify-end gap-4 mt-4"> | ||
111 | + <Button type="primary" @click="handlePreviousStep">上一步</Button> | ||
112 | + <Button type="primary" @click="handleNextStep" :disabled="!canGoNextStep">下一步</Button> | ||
113 | + </div> | ||
114 | + </StepContainer> | ||
115 | +</template> |
1 | +import { Options } from './type'; | ||
2 | +import { getDeviceProfile } from '/@/api/alarm/position'; | ||
3 | +import { getOrganizationList } from '/@/api/system/system'; | ||
4 | +import { FormSchema } from '/@/components/Form'; | ||
5 | +import { BasicColumn } from '/@/components/Table'; | ||
6 | +import { copyTransFun } from '/@/utils/fnUtils'; | ||
7 | + | ||
8 | +export enum FieldsEnum { | ||
9 | + ORGANIZATION_ID = 'organizationId', | ||
10 | + TK_DEVICE_PROFILE_ID = 'tkDeviceProfileId', | ||
11 | + DEVICE_TYPE_ENUM = 'deviceTypeEnum', | ||
12 | + DEVICE_TYPE_NAME = 'deviceTypeName', | ||
13 | + DELIMITER = 'delimiter', | ||
14 | + HEADER = 'header', | ||
15 | + UPDATE = 'update', | ||
16 | +} | ||
17 | + | ||
18 | +export enum DelimiterEnum { | ||
19 | + COMMA = ',', | ||
20 | + SEMICOLON = ';', | ||
21 | + VERTICAL_LINE = '|', | ||
22 | + TAB = 'Tab', | ||
23 | +} | ||
24 | + | ||
25 | +export enum DelimiterNameEnum { | ||
26 | + ',' = 'COMMA', | ||
27 | + ';' = 'SEMICOLON', | ||
28 | + '|' = 'VERTICAL_LINE', | ||
29 | + 'Tab' = 'TAB', | ||
30 | +} | ||
31 | + | ||
32 | +export enum ColumnFileEnum { | ||
33 | + EXAMPLE_VALUE = 'exampleValue', | ||
34 | + TYPE = 'type', | ||
35 | + KEY = 'key', | ||
36 | + OLD_VALUE = 'oldValue', | ||
37 | +} | ||
38 | + | ||
39 | +export enum ColumTypeEnum { | ||
40 | + NAME = 'NAME', | ||
41 | + // TYPE = 'TYPE', | ||
42 | + LABEL = 'LABEL', | ||
43 | + DESCRIPTION = 'DESCRIPTION', | ||
44 | + SHARED_ATTRIBUTE = 'SHARED_ATTRIBUTE', | ||
45 | + SERVER_ATTRIBUTE = 'SERVER_ATTRIBUTE', | ||
46 | + TIMESERIES = 'TIMESERIES', | ||
47 | + IS_GATEWAY = 'IS_GATEWAY', | ||
48 | +} | ||
49 | + | ||
50 | +export enum CredentialsEnum { | ||
51 | + ACCESS_TOKEN = 'ACCESS_TOKEN', | ||
52 | + X509 = 'X509', | ||
53 | + MQTT_CLIENT_ID = 'MQTT_CLIENT_ID', | ||
54 | + MQTT_USER_NAME = 'MQTT_USER_NAME', | ||
55 | + MQTT_PASSWORD = 'MQTT_PASSWORD', | ||
56 | + LWM2M_CLIENT_ENDPOINT = 'LWM2M_CLIENT_ENDPOINT', | ||
57 | + LWM2M_CLIENT_SECURITY_CONFIG_MODE = 'LWM2M_CLIENT_SECURITY_CONFIG_MODE', | ||
58 | + LWM2M_CLIENT_IDENTITY = 'LWM2M_CLIENT_IDENTITY', | ||
59 | + LWM2M_CLIENT_KEY = 'LWM2M_CLIENT_KEY', | ||
60 | + LWM2M_CLIENT_CERT = 'LWM2M_CLIENT_CERT', | ||
61 | + LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE = 'LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE', | ||
62 | + LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID = 'LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID', | ||
63 | + LWM2M_BOOTSTRAP_SERVER_SECRET_KEY = 'LWM2M_BOOTSTRAP_SERVER_SECRET_KEY', | ||
64 | + LWM2M_SERVER_SECURITY_MODE = 'LWM2M_SERVER_SECURITY_MODE', | ||
65 | + LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID', | ||
66 | + LWM2M_SERVER_CLIENT_SECRET_KEY = 'LWM2M_SERVER_CLIENT_SECRET_KEY', | ||
67 | +} | ||
68 | + | ||
69 | +export enum ColumnTypeNameEnum { | ||
70 | + NAME = '名称', | ||
71 | + // TYPE = '类型', | ||
72 | + LABEL = '标签', | ||
73 | + DESCRIPTION = '说明', | ||
74 | + SHARED_ATTRIBUTE = '共享属性', | ||
75 | + SERVER_ATTRIBUTE = '服务器属性', | ||
76 | + TIMESERIES = 'Timeseries', | ||
77 | + IS_GATEWAY = 'Is网关', | ||
78 | +} | ||
79 | + | ||
80 | +export enum CredentialsNameEnum { | ||
81 | + ACCESS_TOKEN = '访问令牌', | ||
82 | + X509 = 'X.509', | ||
83 | + MQTT_CLIENT_ID = 'MQTT client ID', | ||
84 | + MQTT_USER_NAME = 'MQTT user name', | ||
85 | + MQTT_PASSWORD = 'MQTT password', | ||
86 | + LWM2M_CLIENT_ENDPOINT = 'LwM2M endpoint client name', | ||
87 | + LWM2M_CLIENT_SECURITY_CONFIG_MODE = 'LwM2M security config mode', | ||
88 | + LWM2M_CLIENT_IDENTITY = 'LwM2M client identity', | ||
89 | + LWM2M_CLIENT_KEY = 'LwM2M client key', | ||
90 | + LWM2M_CLIENT_CERT = 'LwM2M client public key', | ||
91 | + LWM2M_BOOTSTRAP_SERVER_SECURITY_MODE = 'LwM2M bootstrap server security mode', | ||
92 | + LWM2M_BOOTSTRAP_SERVER_PUBLIC_KEY_OR_ID = 'LwM2M bootstrap server public key or id', | ||
93 | + LWM2M_BOOTSTRAP_SERVER_SECRET_KEY = 'LwM2M bootstrap server secret key', | ||
94 | + LWM2M_SERVER_SECURITY_MODE = 'LwM2M server security mode', | ||
95 | + LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID = 'LwM2M server public key or id', | ||
96 | + LWM2M_SERVER_CLIENT_SECRET_KEY = 'LwM2M server secret key', | ||
97 | +} | ||
98 | + | ||
99 | +export const basicInfoForm: FormSchema[] = [ | ||
100 | + { | ||
101 | + field: FieldsEnum.ORGANIZATION_ID, | ||
102 | + label: '所属组织', | ||
103 | + component: 'ApiTreeSelect', | ||
104 | + rules: [{ required: true, message: '所属组织为必填项' }], | ||
105 | + componentProps: ({ formActionType }) => { | ||
106 | + const { submit } = formActionType; | ||
107 | + return { | ||
108 | + maxLength: 250, | ||
109 | + placeholder: '请选择所属组织', | ||
110 | + api: async () => { | ||
111 | + const data = (await getOrganizationList()) as unknown as Recordable[]; | ||
112 | + copyTransFun(data); | ||
113 | + return data; | ||
114 | + }, | ||
115 | + getPopupContainer: () => document.body, | ||
116 | + onChange: () => submit(), | ||
117 | + }; | ||
118 | + }, | ||
119 | + }, | ||
120 | + { | ||
121 | + field: FieldsEnum.TK_DEVICE_PROFILE_ID, | ||
122 | + component: 'ApiSelect', | ||
123 | + label: '产品', | ||
124 | + rules: [{ required: true, message: '产品为必填项' }], | ||
125 | + componentProps: ({ formActionType }) => { | ||
126 | + const { submit, setFieldsValue } = formActionType; | ||
127 | + return { | ||
128 | + api: getDeviceProfile, | ||
129 | + labelField: 'name', | ||
130 | + valueField: 'id', | ||
131 | + placeholder: '请选择产品', | ||
132 | + 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(); | ||
140 | + }, | ||
141 | + }; | ||
142 | + }, | ||
143 | + }, | ||
144 | + { | ||
145 | + field: FieldsEnum.DEVICE_TYPE_ENUM, | ||
146 | + component: 'Input', | ||
147 | + label: '设备类型', | ||
148 | + show: false, | ||
149 | + }, | ||
150 | + { | ||
151 | + field: FieldsEnum.DEVICE_TYPE_NAME, | ||
152 | + component: 'Input', | ||
153 | + label: '设备名称', | ||
154 | + show: false, | ||
155 | + }, | ||
156 | +]; | ||
157 | + | ||
158 | +export const importConfigurationSchema: FormSchema[] = [ | ||
159 | + { | ||
160 | + field: FieldsEnum.DELIMITER, | ||
161 | + label: 'csv分隔符', | ||
162 | + component: 'Select', | ||
163 | + rules: [{ required: true, message: 'csv分隔符为必填项' }], | ||
164 | + defaultValue: DelimiterEnum.COMMA, | ||
165 | + componentProps: { | ||
166 | + options: [ | ||
167 | + { label: DelimiterEnum.COMMA, value: DelimiterEnum.COMMA }, | ||
168 | + { label: DelimiterEnum.SEMICOLON, value: DelimiterEnum.SEMICOLON }, | ||
169 | + { label: DelimiterEnum.VERTICAL_LINE, value: DelimiterEnum.VERTICAL_LINE }, | ||
170 | + { label: DelimiterEnum.TAB, value: DelimiterEnum.TAB }, | ||
171 | + ], | ||
172 | + }, | ||
173 | + }, | ||
174 | + { | ||
175 | + field: FieldsEnum.HEADER, | ||
176 | + label: '第一行包含列名', | ||
177 | + component: 'Checkbox', | ||
178 | + defaultValue: true, | ||
179 | + }, | ||
180 | + { | ||
181 | + field: FieldsEnum.UPDATE, | ||
182 | + label: '更新属性/遥测', | ||
183 | + component: 'Checkbox', | ||
184 | + defaultValue: true, | ||
185 | + }, | ||
186 | +]; | ||
187 | + | ||
188 | +export const generateColumnTypeOptions = () => { | ||
189 | + const valueOptions = Object.keys(ColumTypeEnum); | ||
190 | + const labelOptions = Object.values(ColumnTypeNameEnum); | ||
191 | + const credentialsValueOptions = Object.keys(CredentialsEnum); | ||
192 | + const credentialsNameOptions = Object.values(CredentialsNameEnum); | ||
193 | + const options: Options[] = valueOptions.map((value, index) => ({ | ||
194 | + label: labelOptions[index], | ||
195 | + value, | ||
196 | + })); | ||
197 | + | ||
198 | + const credentialsOption: Options = { | ||
199 | + label: 'credentials', | ||
200 | + options: credentialsValueOptions.map((value, index) => ({ | ||
201 | + label: credentialsNameOptions[index], | ||
202 | + value: value, | ||
203 | + })) as Options[], | ||
204 | + }; | ||
205 | + options.push(credentialsOption); | ||
206 | + | ||
207 | + return options; | ||
208 | +}; | ||
209 | + | ||
210 | +export const columnTypeSchema: BasicColumn[] = [ | ||
211 | + { | ||
212 | + title: '列名称', | ||
213 | + key: ColumnFileEnum.KEY, | ||
214 | + dataIndex: ColumnFileEnum.KEY, | ||
215 | + slots: { | ||
216 | + customRender: ColumnFileEnum.KEY, | ||
217 | + }, | ||
218 | + }, | ||
219 | + { | ||
220 | + title: '列类型', | ||
221 | + dataIndex: ColumnFileEnum.TYPE, | ||
222 | + key: ColumnFileEnum.TYPE, | ||
223 | + slots: { | ||
224 | + customRender: ColumnFileEnum.TYPE, | ||
225 | + }, | ||
226 | + }, | ||
227 | +]; | ||
228 | + | ||
229 | +export const assemblyData = () => {}; |
1 | +<script lang="ts" setup> | ||
2 | + import { computed, ref, unref } from 'vue'; | ||
3 | + import { BasicModal, useModal } from '/@/components/Modal'; | ||
4 | + import { Steps } from 'ant-design-vue'; | ||
5 | + import BasicInfoForm from './BasicInfoForm.vue'; | ||
6 | + import ImportCsv from './ImportCsv.vue'; | ||
7 | + import { CreateEntityValue, UploadFileParseValue } from './type'; | ||
8 | + import ColumnType from './ColumnType.vue'; | ||
9 | + import CreateEntity from './CreateEntity.vue'; | ||
10 | + import CompleteResult from './CompleteResult.vue'; | ||
11 | + import { ImportDeviceResponse } from '/@/api/device/model/batchImportModel'; | ||
12 | + | ||
13 | + const emit = defineEmits(['importFinally']); | ||
14 | + | ||
15 | + const [register] = useModal(); | ||
16 | + | ||
17 | + const basicInfo = ref({}); | ||
18 | + | ||
19 | + const fileParseValue = ref<UploadFileParseValue>({ header: [], content: [] }); | ||
20 | + | ||
21 | + const columnConfiguration = ref<Record<'type', string>[]>([]); | ||
22 | + | ||
23 | + const importResult = ref<ImportDeviceResponse>({} as unknown as ImportDeviceResponse); | ||
24 | + | ||
25 | + const assemblyData = computed(() => { | ||
26 | + return { | ||
27 | + basicInfo: unref(basicInfo), | ||
28 | + fileParseValue: unref(fileParseValue), | ||
29 | + columnConfiguration: unref(columnConfiguration), | ||
30 | + } as CreateEntityValue; | ||
31 | + }); | ||
32 | + | ||
33 | + enum StepsEnum { | ||
34 | + BASIC_INFO, | ||
35 | + IMPORT_FILE, | ||
36 | + COLUMN_CONFIGURATION, | ||
37 | + CREATE_ENTITY, | ||
38 | + FINISH, | ||
39 | + } | ||
40 | + | ||
41 | + enum StepsNameEnum { | ||
42 | + BASIC_INFO = '基本信息', | ||
43 | + IMPORT_FILE = '选择一个文件', | ||
44 | + COLUMN_CONFIGURATION = '选择列类型', | ||
45 | + CREATE_ENTITY = '创建新实体', | ||
46 | + FINISH = '完成', | ||
47 | + } | ||
48 | + | ||
49 | + const currentStep = ref<StepsEnum>(StepsEnum.BASIC_INFO); | ||
50 | + | ||
51 | + const goNextStep = () => { | ||
52 | + if (unref(currentStep) >= StepsEnum.FINISH) return; | ||
53 | + currentStep.value = unref(currentStep) + 1; | ||
54 | + }; | ||
55 | + | ||
56 | + const goPreviousStep = () => { | ||
57 | + if (unref(currentStep) <= StepsEnum.BASIC_INFO) return; | ||
58 | + currentStep.value = unref(currentStep) - 1; | ||
59 | + }; | ||
60 | + | ||
61 | + const reset = () => { | ||
62 | + basicInfo.value = {}; | ||
63 | + fileParseValue.value = { header: [], content: [] }; | ||
64 | + columnConfiguration.value = []; | ||
65 | + importResult.value = {} as unknown as ImportDeviceResponse; | ||
66 | + }; | ||
67 | + | ||
68 | + const handleCancel = async () => { | ||
69 | + if (unref(currentStep) === StepsEnum.FINISH) { | ||
70 | + emit('importFinally'); | ||
71 | + } | ||
72 | + currentStep.value = StepsEnum.BASIC_INFO; | ||
73 | + reset(); | ||
74 | + return true; | ||
75 | + }; | ||
76 | +</script> | ||
77 | + | ||
78 | +<template> | ||
79 | + <BasicModal | ||
80 | + title="导入设备" | ||
81 | + @register="register" | ||
82 | + width="60%" | ||
83 | + wrap-class-name="import-device-modal" | ||
84 | + :show-ok-btn="false" | ||
85 | + cancel-text="关闭" | ||
86 | + :close-func="handleCancel" | ||
87 | + :destroy-on-close="true" | ||
88 | + > | ||
89 | + <section class="overflow-auto"> | ||
90 | + <Steps direction="vertical" :current="currentStep"> | ||
91 | + <Steps.Step | ||
92 | + v-for="item in Object.keys(StepsEnum).filter((key) => isNaN(key as unknown as number))" | ||
93 | + :key="item" | ||
94 | + > | ||
95 | + <template #title> | ||
96 | + <div> | ||
97 | + {{ StepsNameEnum[item] }} | ||
98 | + </div> | ||
99 | + <BasicInfoForm | ||
100 | + v-if="StepsEnum[item] === StepsEnum.BASIC_INFO && StepsEnum[item] === currentStep" | ||
101 | + v-model:value="basicInfo" | ||
102 | + :go-next-step="goNextStep" | ||
103 | + :go-previous-step="goPreviousStep" | ||
104 | + /> | ||
105 | + <ImportCsv | ||
106 | + v-if="StepsEnum[item] === StepsEnum.IMPORT_FILE && StepsEnum[item] === currentStep" | ||
107 | + v-model:value="fileParseValue" | ||
108 | + :go-next-step="goNextStep" | ||
109 | + :go-previous-step="goPreviousStep" | ||
110 | + /> | ||
111 | + <ColumnType | ||
112 | + v-model:value="columnConfiguration" | ||
113 | + v-if=" | ||
114 | + StepsEnum[item] === StepsEnum.COLUMN_CONFIGURATION && | ||
115 | + StepsEnum[item] === currentStep | ||
116 | + " | ||
117 | + :file-parse-value="fileParseValue" | ||
118 | + :go-next-step="goNextStep" | ||
119 | + :go-previous-step="goPreviousStep" | ||
120 | + /> | ||
121 | + <CreateEntity | ||
122 | + v-model:result="importResult" | ||
123 | + :value="assemblyData" | ||
124 | + v-if="StepsEnum[item] === StepsEnum.CREATE_ENTITY && StepsEnum[item] === currentStep" | ||
125 | + :go-next-step="goNextStep" | ||
126 | + :go-previous-step="goPreviousStep" | ||
127 | + /> | ||
128 | + <CompleteResult | ||
129 | + v-if="StepsEnum[item] === StepsEnum.FINISH && StepsEnum[item] === currentStep" | ||
130 | + :value="importResult" | ||
131 | + /> | ||
132 | + </template> | ||
133 | + </Steps.Step> | ||
134 | + </Steps> | ||
135 | + </section> | ||
136 | + </BasicModal> | ||
137 | +</template> | ||
138 | + | ||
139 | +<style lang="less"> | ||
140 | + .import-device-modal { | ||
141 | + .ant-steps-item-title { | ||
142 | + width: 100%; | ||
143 | + } | ||
144 | + } | ||
145 | +</style> |
1 | +import { ColumnFileEnum } from './config'; | ||
2 | + | ||
3 | +export interface UploadFileParseValue { | ||
4 | + header: string[]; | ||
5 | + content: Recordable[]; | ||
6 | +} | ||
7 | + | ||
8 | +export interface ColumnDataRecord extends Record<ColumnFileEnum, string> { | ||
9 | + editable?: boolean; | ||
10 | + oldValue: any | undefined; | ||
11 | + id?: string; | ||
12 | + name?: string; | ||
13 | +} | ||
14 | + | ||
15 | +export interface Options { | ||
16 | + label: string; | ||
17 | + value?: string; | ||
18 | + options?: Options[]; | ||
19 | + disabled?: boolean; | ||
20 | +} | ||
21 | + | ||
22 | +export type BasicInfoRecord = Record< | ||
23 | + 'organizationId' | 'tkDeviceProfileId' | 'deviceTypeEnum' | 'deviceTypeName', | ||
24 | + string | ||
25 | +>; | ||
26 | + | ||
27 | +export interface CreateEntityValue { | ||
28 | + basicInfo: BasicInfoRecord; | ||
29 | + fileParseValue: UploadFileParseValue; | ||
30 | + columnConfiguration: Record<'type', string>[]; | ||
31 | +} |
@@ -380,7 +380,7 @@ | @@ -380,7 +380,7 @@ | ||
380 | }; | 380 | }; |
381 | // 父组件调用更新字段值的方法 | 381 | // 父组件调用更新字段值的方法 |
382 | function parentSetFieldsValue(data) { | 382 | function parentSetFieldsValue(data) { |
383 | - const { deviceInfo } = data; | 383 | + const { deviceInfo = {} } = data; |
384 | positionState.longitude = deviceInfo.longitude; | 384 | positionState.longitude = deviceInfo.longitude; |
385 | positionState.latitude = deviceInfo.latitude; | 385 | positionState.latitude = deviceInfo.latitude; |
386 | positionState.address = deviceInfo.address; | 386 | positionState.address = deviceInfo.address; |
@@ -21,6 +21,9 @@ | @@ -21,6 +21,9 @@ | ||
21 | </a-button> | 21 | </a-button> |
22 | </Popconfirm> | 22 | </Popconfirm> |
23 | </Authority> | 23 | </Authority> |
24 | + <Authority> | ||
25 | + <Button type="primary" @click="handleBatchImport">导入</Button> | ||
26 | + </Authority> | ||
24 | <a-button | 27 | <a-button |
25 | v-if="authBtn(role)" | 28 | v-if="authBtn(role)" |
26 | type="primary" | 29 | type="primary" |
@@ -173,6 +176,8 @@ | @@ -173,6 +176,8 @@ | ||
173 | 176 | ||
174 | <DeviceModal @register="registerModal" @success="handleSuccess" @reload="handleSuccess" /> | 177 | <DeviceModal @register="registerModal" @success="handleSuccess" @reload="handleSuccess" /> |
175 | <CustomerModal @register="registerCustomerModal" @reload="handleReload" /> | 178 | <CustomerModal @register="registerCustomerModal" @reload="handleReload" /> |
179 | + | ||
180 | + <BatchImportModal @register="registerImportModal" @import-finally="handleImportFinally" /> | ||
176 | </PageWrapper> | 181 | </PageWrapper> |
177 | </div> | 182 | </div> |
178 | </template> | 183 | </template> |
@@ -186,7 +191,7 @@ | @@ -186,7 +191,7 @@ | ||
186 | } from '/@/api/device/model/deviceModel'; | 191 | } from '/@/api/device/model/deviceModel'; |
187 | import { BasicTable, useTable, TableAction, TableImg } from '/@/components/Table'; | 192 | import { BasicTable, useTable, TableAction, TableImg } from '/@/components/Table'; |
188 | import { columns, searchFormSchema } from './config/device.data'; | 193 | import { columns, searchFormSchema } from './config/device.data'; |
189 | - import { Tag, Tooltip, Popover, Popconfirm } from 'ant-design-vue'; | 194 | + import { Tag, Tooltip, Popover, Popconfirm, Button } from 'ant-design-vue'; |
190 | import { | 195 | import { |
191 | deleteDevice, | 196 | deleteDevice, |
192 | devicePage, | 197 | devicePage, |
@@ -203,6 +208,7 @@ | @@ -203,6 +208,7 @@ | ||
203 | import { useDrawer } from '/@/components/Drawer'; | 208 | import { useDrawer } from '/@/components/Drawer'; |
204 | import DeviceDetailDrawer from './cpns/modal/DeviceDetailDrawer.vue'; | 209 | import DeviceDetailDrawer from './cpns/modal/DeviceDetailDrawer.vue'; |
205 | import CustomerModal from './cpns/modal/CustomerModal.vue'; | 210 | import CustomerModal from './cpns/modal/CustomerModal.vue'; |
211 | + import BatchImportModal from './cpns/modal/BatchImportModal/index.vue'; | ||
206 | import { useMessage } from '/@/hooks/web/useMessage'; | 212 | import { useMessage } from '/@/hooks/web/useMessage'; |
207 | import { USER_INFO_KEY } from '/@/enums/cacheEnum'; | 213 | import { USER_INFO_KEY } from '/@/enums/cacheEnum'; |
208 | import { getAuthCache } from '/@/utils/auth'; | 214 | import { getAuthCache } from '/@/utils/auth'; |
@@ -230,6 +236,8 @@ | @@ -230,6 +236,8 @@ | ||
230 | Popover, | 236 | Popover, |
231 | Authority, | 237 | Authority, |
232 | Popconfirm, | 238 | Popconfirm, |
239 | + BatchImportModal, | ||
240 | + Button, | ||
233 | }, | 241 | }, |
234 | setup(_) { | 242 | setup(_) { |
235 | const { createMessage } = useMessage(); | 243 | const { createMessage } = useMessage(); |
@@ -243,6 +251,7 @@ | @@ -243,6 +251,7 @@ | ||
243 | const [registerDetailDrawer, { openDrawer }] = useDrawer(); | 251 | const [registerDetailDrawer, { openDrawer }] = useDrawer(); |
244 | const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer(); | 252 | const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer(); |
245 | const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer(); | 253 | const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer(); |
254 | + const [registerImportModal, { openModal: openImportModal }] = useModal(); | ||
246 | 255 | ||
247 | const [ | 256 | const [ |
248 | registerTable, | 257 | registerTable, |
@@ -451,7 +460,7 @@ | @@ -451,7 +460,7 @@ | ||
451 | 460 | ||
452 | const handleBatchAssign = () => { | 461 | const handleBatchAssign = () => { |
453 | const options = getSelectRows(); | 462 | const options = getSelectRows(); |
454 | - if (handleCheckHasDiffenterOrg(options)) { | 463 | + if (handleCheckHasDiffenterOrg(options as DeviceModel[])) { |
455 | createMessage.error('当前选中项中存在不同所属组织的设备!'); | 464 | createMessage.error('当前选中项中存在不同所属组织的设备!'); |
456 | return; | 465 | return; |
457 | } | 466 | } |
@@ -477,6 +486,14 @@ | @@ -477,6 +486,14 @@ | ||
477 | } | 486 | } |
478 | }; | 487 | }; |
479 | 488 | ||
489 | + const handleBatchImport = () => { | ||
490 | + openImportModal(true); | ||
491 | + }; | ||
492 | + | ||
493 | + const handleImportFinally = () => { | ||
494 | + reload(); | ||
495 | + }; | ||
496 | + | ||
480 | return { | 497 | return { |
481 | registerTable, | 498 | registerTable, |
482 | handleCreate, | 499 | handleCreate, |
@@ -508,6 +525,9 @@ | @@ -508,6 +525,9 @@ | ||
508 | registerGatewayDetailDrawer, | 525 | registerGatewayDetailDrawer, |
509 | handleUpAndDownRecord, | 526 | handleUpAndDownRecord, |
510 | handleBatchAssign, | 527 | handleBatchAssign, |
528 | + registerImportModal, | ||
529 | + handleBatchImport, | ||
530 | + handleImportFinally, | ||
511 | }; | 531 | }; |
512 | }, | 532 | }, |
513 | }); | 533 | }); |