Showing
23 changed files
with
1127 additions
and
32 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 | +} | ... | ... |
src/utils/useBatchOperation.ts
0 → 100644
1 | +import { computed } from 'vue'; | |
2 | +import { TableActionType } from '/@//components/Table'; | |
3 | + | |
4 | +const useBatchOperation = ( | |
5 | + getRowSelection: TableActionType['getRowSelection'], | |
6 | + setSelectedRowKeys: TableActionType['setSelectedRowKeys'] | |
7 | +) => { | |
8 | + const isExistOption = computed(() => { | |
9 | + const rowSelection = getRowSelection(); | |
10 | + return !!rowSelection.selectedRowKeys?.length; | |
11 | + }); | |
12 | + | |
13 | + const resetSelectedOptions = () => { | |
14 | + setSelectedRowKeys([]); | |
15 | + }; | |
16 | + | |
17 | + return { | |
18 | + isExistOption, | |
19 | + resetSelectedOptions, | |
20 | + }; | |
21 | +}; | |
22 | + | |
23 | +export { useBatchOperation }; | ... | ... |
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 | +} | ... | ... |
... | ... | @@ -20,6 +20,8 @@ |
20 | 20 | import { BasicForm, useForm } from '/@/components/Form'; |
21 | 21 | import { customerForm } from '../../config/detail.config'; |
22 | 22 | import { dispatchCustomer as dispatchCustomerApi } from '/@/api/device/deviceManager'; |
23 | + import { DeviceModel } from '/@/api/device/model/deviceModel'; | |
24 | + import { isArray } from '/@/utils/is'; | |
23 | 25 | export default defineComponent({ |
24 | 26 | name: 'AlarmDetailModal', |
25 | 27 | components: { |
... | ... | @@ -28,9 +30,10 @@ |
28 | 30 | }, |
29 | 31 | emits: ['reload', 'register'], |
30 | 32 | setup(_, { emit }) { |
31 | - let record = {}; | |
32 | - const [registerModal, { closeModal }] = useModalInner((data: any) => { | |
33 | - const { organizationId } = data; | |
33 | + let record: DeviceModel[] = []; | |
34 | + const [registerModal, { closeModal }] = useModalInner((data: DeviceModel | DeviceModel[]) => { | |
35 | + data = isArray(data) ? data : [data as DeviceModel]; | |
36 | + const { organizationId } = data.at(0) || {}; | |
34 | 37 | record = data; |
35 | 38 | updateSchema([ |
36 | 39 | { |
... | ... | @@ -50,7 +53,11 @@ |
50 | 53 | const dispatchCustomer = async () => { |
51 | 54 | await validate(); |
52 | 55 | const { customerId } = getFieldsValue(); |
53 | - await dispatchCustomerApi({ ...record, customerId }); | |
56 | + const task: Promise<any>[] = []; | |
57 | + for (const item of record) { | |
58 | + task.push(dispatchCustomerApi({ ...item, customerId })); | |
59 | + } | |
60 | + await Promise.all(task); | |
54 | 61 | closeModal(); |
55 | 62 | resetFields(); |
56 | 63 | emit('reload'); | ... | ... |
... | ... | @@ -102,7 +102,7 @@ |
102 | 102 | // 设备详情 |
103 | 103 | const res = await getDeviceDetail(id); |
104 | 104 | deviceDetail.value = res; |
105 | - const { latitude, longitude, address } = res.deviceInfo; | |
105 | + const { latitude, longitude, address } = res.deviceInfo || {}; | |
106 | 106 | if (latitude) { |
107 | 107 | deviceDetailRef.value.initMap(longitude, latitude, address); |
108 | 108 | } | ... | ... |
... | ... | @@ -380,7 +380,7 @@ |
380 | 380 | }; |
381 | 381 | // 父组件调用更新字段值的方法 |
382 | 382 | function parentSetFieldsValue(data) { |
383 | - const { deviceInfo } = data; | |
383 | + const { deviceInfo = {} } = data; | |
384 | 384 | positionState.longitude = deviceInfo.longitude; |
385 | 385 | positionState.latitude = deviceInfo.latitude; |
386 | 386 | positionState.address = deviceInfo.address; | ... | ... |
... | ... | @@ -14,13 +14,24 @@ |
14 | 14 | title="您确定要批量删除数据" |
15 | 15 | ok-text="确定" |
16 | 16 | cancel-text="取消" |
17 | - @confirm="handleDeleteOrBatchDelete(null)" | |
17 | + @confirm="handleDelete()" | |
18 | 18 | > |
19 | - <a-button color="error" v-if="authBtn(role)" :disabled="hasBatchDelete"> | |
19 | + <a-button color="error" v-if="authBtn(role)" :disabled="!isExistOption"> | |
20 | 20 | 批量删除 |
21 | 21 | </a-button> |
22 | 22 | </Popconfirm> |
23 | 23 | </Authority> |
24 | + <Authority> | |
25 | + <Button type="primary" @click="handleBatchImport">导入</Button> | |
26 | + </Authority> | |
27 | + <a-button | |
28 | + v-if="authBtn(role)" | |
29 | + type="primary" | |
30 | + @click="handleBatchAssign" | |
31 | + :disabled="!isExistOption" | |
32 | + > | |
33 | + 批量分配 | |
34 | + </a-button> | |
24 | 35 | </template> |
25 | 36 | <template #img="{ record }"> |
26 | 37 | <TableImg |
... | ... | @@ -147,7 +158,7 @@ |
147 | 158 | color: 'error', |
148 | 159 | popConfirm: { |
149 | 160 | title: '是否确认删除', |
150 | - confirm: handleDeleteOrBatchDelete.bind(null, record), | |
161 | + confirm: handleDelete.bind(null, record), | |
151 | 162 | }, |
152 | 163 | }, |
153 | 164 | ]" |
... | ... | @@ -165,15 +176,22 @@ |
165 | 176 | |
166 | 177 | <DeviceModal @register="registerModal" @success="handleSuccess" @reload="handleSuccess" /> |
167 | 178 | <CustomerModal @register="registerCustomerModal" @reload="handleReload" /> |
179 | + | |
180 | + <BatchImportModal @register="registerImportModal" @import-finally="handleImportFinally" /> | |
168 | 181 | </PageWrapper> |
169 | 182 | </div> |
170 | 183 | </template> |
171 | 184 | <script lang="ts"> |
172 | 185 | import { defineComponent, reactive, unref, nextTick, h, onUnmounted, ref } from 'vue'; |
173 | - import { DeviceState, DeviceTypeEnum } from '/@/api/device/model/deviceModel'; | |
186 | + import { | |
187 | + DeviceModel, | |
188 | + DeviceRecord, | |
189 | + DeviceState, | |
190 | + DeviceTypeEnum, | |
191 | + } from '/@/api/device/model/deviceModel'; | |
174 | 192 | import { BasicTable, useTable, TableAction, TableImg } from '/@/components/Table'; |
175 | 193 | import { columns, searchFormSchema } from './config/device.data'; |
176 | - import { Tag, Tooltip, Popover, Popconfirm } from 'ant-design-vue'; | |
194 | + import { Tag, Tooltip, Popover, Popconfirm, Button } from 'ant-design-vue'; | |
177 | 195 | import { |
178 | 196 | deleteDevice, |
179 | 197 | devicePage, |
... | ... | @@ -190,15 +208,16 @@ |
190 | 208 | import { useDrawer } from '/@/components/Drawer'; |
191 | 209 | import DeviceDetailDrawer from './cpns/modal/DeviceDetailDrawer.vue'; |
192 | 210 | import CustomerModal from './cpns/modal/CustomerModal.vue'; |
211 | + import BatchImportModal from './cpns/modal/BatchImportModal/index.vue'; | |
193 | 212 | import { useMessage } from '/@/hooks/web/useMessage'; |
194 | 213 | import { USER_INFO_KEY } from '/@/enums/cacheEnum'; |
195 | 214 | import { getAuthCache } from '/@/utils/auth'; |
196 | 215 | import { authBtn } from '/@/enums/roleEnum'; |
197 | - import { useBatchDelete } from '/@/hooks/web/useBatchDelete'; | |
198 | 216 | import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard'; |
199 | 217 | import { QuestionCircleOutlined } from '@ant-design/icons-vue'; |
200 | 218 | import { Authority } from '/@/components/Authority'; |
201 | 219 | import { useRouter } from 'vue-router'; |
220 | + import { useBatchOperation } from '/@/utils/useBatchOperation'; | |
202 | 221 | |
203 | 222 | export default defineComponent({ |
204 | 223 | name: 'DeviceManagement', |
... | ... | @@ -217,6 +236,8 @@ |
217 | 236 | Popover, |
218 | 237 | Authority, |
219 | 238 | Popconfirm, |
239 | + BatchImportModal, | |
240 | + Button, | |
220 | 241 | }, |
221 | 242 | setup(_) { |
222 | 243 | const { createMessage } = useMessage(); |
... | ... | @@ -230,10 +251,21 @@ |
230 | 251 | const [registerDetailDrawer, { openDrawer }] = useDrawer(); |
231 | 252 | const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer(); |
232 | 253 | const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer(); |
254 | + const [registerImportModal, { openModal: openImportModal }] = useModal(); | |
233 | 255 | |
234 | 256 | const [ |
235 | 257 | registerTable, |
236 | - { reload, setSelectedRowKeys, setProps, setTableData, getForm, setPagination }, | |
258 | + { | |
259 | + reload, | |
260 | + setLoading, | |
261 | + setSelectedRowKeys, | |
262 | + setTableData, | |
263 | + getForm, | |
264 | + setPagination, | |
265 | + getSelectRowKeys, | |
266 | + getSelectRows, | |
267 | + getRowSelection, | |
268 | + }, | |
237 | 269 | ] = useTable({ |
238 | 270 | title: '设备列表', |
239 | 271 | api: devicePage, |
... | ... | @@ -241,7 +273,6 @@ |
241 | 273 | columns, |
242 | 274 | beforeFetch: (params) => { |
243 | 275 | const { deviceProfileId } = params; |
244 | - console.log(deviceProfileId); | |
245 | 276 | const obj = { |
246 | 277 | ...params, |
247 | 278 | ...{ |
... | ... | @@ -272,20 +303,15 @@ |
272 | 303 | slots: { customRender: 'action' }, |
273 | 304 | fixed: 'right', |
274 | 305 | }, |
306 | + rowSelection: { | |
307 | + type: 'checkbox', | |
308 | + getCheckboxProps: (record: DeviceModel) => { | |
309 | + return { disabled: !!record.customerId }; | |
310 | + }, | |
311 | + }, | |
275 | 312 | }); |
276 | - const { hasBatchDelete, handleDeleteOrBatchDelete, selectionOptions, resetSelectedRowKeys } = | |
277 | - useBatchDelete(deleteDevice, handleSuccess, setProps); | |
278 | - selectionOptions.rowSelection.getCheckboxProps = (record: Recordable) => { | |
279 | - // Demo:status为1的选择框禁用 | |
280 | - if (record.customerId) { | |
281 | - return { disabled: true }; | |
282 | - } else { | |
283 | - return { disabled: false }; | |
284 | - } | |
285 | - }; | |
286 | - nextTick(() => { | |
287 | - setProps(selectionOptions); | |
288 | - }); | |
313 | + | |
314 | + const { isExistOption } = useBatchOperation(getRowSelection, setSelectedRowKeys); | |
289 | 315 | |
290 | 316 | function getParams(keyword) { |
291 | 317 | const reg = new RegExp('(^|&)' + keyword + '=([^&]*)(&|$)', 'i'); |
... | ... | @@ -349,7 +375,6 @@ |
349 | 375 | } |
350 | 376 | function handleReload() { |
351 | 377 | setSelectedRowKeys([]); |
352 | - resetSelectedRowKeys(); | |
353 | 378 | handleSuccess(); |
354 | 379 | } |
355 | 380 | // 取消分配客户 |
... | ... | @@ -419,6 +444,56 @@ |
419 | 444 | }); |
420 | 445 | }; |
421 | 446 | |
447 | + const handleCheckHasDiffenterOrg = (options: DeviceModel[]) => { | |
448 | + let orgId: string | undefined; | |
449 | + let flag = false; | |
450 | + for (const item of options) { | |
451 | + const _orgId = item.organizationId; | |
452 | + if (!orgId) orgId = _orgId; | |
453 | + if (orgId !== _orgId) { | |
454 | + flag = true; | |
455 | + break; | |
456 | + } | |
457 | + } | |
458 | + return flag; | |
459 | + }; | |
460 | + | |
461 | + const handleBatchAssign = () => { | |
462 | + const options = getSelectRows(); | |
463 | + if (handleCheckHasDiffenterOrg(options as DeviceModel[])) { | |
464 | + createMessage.error('当前选中项中存在不同所属组织的设备!'); | |
465 | + return; | |
466 | + } | |
467 | + openCustomerModal(true, options); | |
468 | + }; | |
469 | + | |
470 | + const handleDelete = async (record?: DeviceRecord) => { | |
471 | + let ids: string[] = []; | |
472 | + if (record) { | |
473 | + ids.push(record.id); | |
474 | + } else { | |
475 | + ids = getSelectRowKeys(); | |
476 | + } | |
477 | + try { | |
478 | + setLoading(true); | |
479 | + await deleteDevice(ids); | |
480 | + createMessage.success('删除成功'); | |
481 | + handleReload(); | |
482 | + } catch (error) { | |
483 | + createMessage.error('删除失败'); | |
484 | + } finally { | |
485 | + setLoading(false); | |
486 | + } | |
487 | + }; | |
488 | + | |
489 | + const handleBatchImport = () => { | |
490 | + openImportModal(true); | |
491 | + }; | |
492 | + | |
493 | + const handleImportFinally = () => { | |
494 | + reload(); | |
495 | + }; | |
496 | + | |
422 | 497 | return { |
423 | 498 | registerTable, |
424 | 499 | handleCreate, |
... | ... | @@ -439,14 +514,20 @@ |
439 | 514 | authBtn, |
440 | 515 | role, |
441 | 516 | copySN, |
442 | - hasBatchDelete, | |
443 | - handleDeleteOrBatchDelete, | |
517 | + isExistOption, | |
518 | + handleDelete, | |
519 | + // hasBatchDelete, | |
520 | + // handleDeleteOrBatchDelete, | |
444 | 521 | handleReload, |
445 | 522 | registerTbDetailDrawer, |
446 | 523 | handleOpenTbDeviceDetail, |
447 | 524 | handleOpenGatewayDetail, |
448 | 525 | registerGatewayDetailDrawer, |
449 | 526 | handleUpAndDownRecord, |
527 | + handleBatchAssign, | |
528 | + registerImportModal, | |
529 | + handleBatchImport, | |
530 | + handleImportFinally, | |
450 | 531 | }; |
451 | 532 | }, |
452 | 533 | }); | ... | ... |
... | ... | @@ -260,6 +260,8 @@ |
260 | 260 | } |
261 | 261 | }; |
262 | 262 | const setClientProperties = (record: Recordable) => { |
263 | + const type = Reflect.get(record, 'type'); | |
264 | + if (type === 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode') return; | |
263 | 265 | const configuration = Reflect.get(record, 'configuration'); |
264 | 266 | const clientProperties = Reflect.get(configuration, 'clientProperties'); |
265 | 267 | !clientProperties && record.configuration && (record.configuration.clientProperties = {}); | ... | ... |