Commit be64163857f9003c7ce0a7dbe49f5205dfe3b8cd
Merge branch 'feat/device-ota-package' into 'main_dev'
feat: 设备编辑时新增ota升级包 See merge request yunteng/thingskit-front!1220
Showing
6 changed files
with
278 additions
and
2 deletions
@@ -6,6 +6,7 @@ import { | @@ -6,6 +6,7 @@ import { | ||
6 | GetDeviceProfileInfosParams, | 6 | GetDeviceProfileInfosParams, |
7 | GetOtaPackagesParams, | 7 | GetOtaPackagesParams, |
8 | OtaRecordDatum, | 8 | OtaRecordDatum, |
9 | + QueryDeviceProfileOtaPackagesType, | ||
9 | TBResponse, | 10 | TBResponse, |
10 | UploadOtaPackagesParams, | 11 | UploadOtaPackagesParams, |
11 | } from './model'; | 12 | } from './model'; |
@@ -137,3 +138,18 @@ export const getDeviceProfileInfoById = (id: string) => { | @@ -137,3 +138,18 @@ export const getDeviceProfileInfoById = (id: string) => { | ||
137 | { joinPrefix: false } | 138 | { joinPrefix: false } |
138 | ); | 139 | ); |
139 | }; | 140 | }; |
141 | + | ||
142 | +export const getDeviceProfileOtaPackages = (params: QueryDeviceProfileOtaPackagesType) => { | ||
143 | + const { deviceProfileId, type, page, pageSize, textSearch } = params; | ||
144 | + return defHttp.get<TBResponse<OtaRecordDatum>>( | ||
145 | + { | ||
146 | + url: `${Api.GET_OTA_PACKAGES}/${deviceProfileId}/${type}`, | ||
147 | + params: { | ||
148 | + page, | ||
149 | + pageSize, | ||
150 | + textSearch, | ||
151 | + }, | ||
152 | + }, | ||
153 | + { joinPrefix: false } | ||
154 | + ); | ||
155 | +}; |
1 | +import { OTAPackageType } from '/@/enums/otaEnum'; | ||
1 | import { ALG, CheckSumWay } from '/@/views/operation/ota/config/packageDetail.config'; | 2 | import { ALG, CheckSumWay } from '/@/views/operation/ota/config/packageDetail.config'; |
3 | +import { OrderByEnum } from '/@/views/rule/designer/enum/form'; | ||
2 | 4 | ||
3 | export interface GetOtaPackagesParams { | 5 | export interface GetOtaPackagesParams { |
4 | pageSize: number; | 6 | pageSize: number; |
@@ -105,3 +107,13 @@ export interface DeviceProfileRecord { | @@ -105,3 +107,13 @@ export interface DeviceProfileRecord { | ||
105 | type: string; | 107 | type: string; |
106 | transportType: string; | 108 | transportType: string; |
107 | } | 109 | } |
110 | + | ||
111 | +export interface QueryDeviceProfileOtaPackagesType { | ||
112 | + deviceProfileId: string; | ||
113 | + type: OTAPackageType; | ||
114 | + page: number; | ||
115 | + pageSize: number; | ||
116 | + textSearch?: string; | ||
117 | + sortProperty?: string; | ||
118 | + sortOrder?: OrderByEnum; | ||
119 | +} |
1 | +<script lang="ts"> | ||
2 | + export type OptionsItem = { label: string; value: string; disabled?: boolean }; | ||
3 | + export interface OnChangeHookParams { | ||
4 | + options: Ref<OptionsItem[]>; | ||
5 | + } | ||
6 | +</script> | ||
7 | + | ||
8 | +<script lang="ts" setup> | ||
9 | + import { ref, watchEffect, computed, unref, watch, Ref } from 'vue'; | ||
10 | + import { Select } from 'ant-design-vue'; | ||
11 | + import { isFunction } from '/@/utils/is'; | ||
12 | + import { useRuleFormItem } from '/@/hooks/component/useFormItem'; | ||
13 | + import { get, omit } from 'lodash-es'; | ||
14 | + import { LoadingOutlined } from '@ant-design/icons-vue'; | ||
15 | + import { useI18n } from '/@/hooks/web/useI18n'; | ||
16 | + import { useDebounceFn } from '@vueuse/shared'; | ||
17 | + | ||
18 | + const emit = defineEmits(['options-change', 'change']); | ||
19 | + const props = withDefaults( | ||
20 | + defineProps<{ | ||
21 | + value?: Recordable | number | string; | ||
22 | + numberToString?: boolean; | ||
23 | + api?: (arg?: Recordable) => Promise<Recordable>; | ||
24 | + queryApi?: (value?: any) => Promise<Recordable>; | ||
25 | + params?: Recordable | ((searchText?: string) => Recordable); | ||
26 | + resultField?: string; | ||
27 | + labelField?: string; | ||
28 | + valueField?: string; | ||
29 | + immediate?: boolean; | ||
30 | + queryEmptyDataAgin?: boolean; | ||
31 | + }>(), | ||
32 | + { | ||
33 | + resultField: '', | ||
34 | + labelField: 'label', | ||
35 | + valueField: 'value', | ||
36 | + searchField: 'text', | ||
37 | + immediate: true, | ||
38 | + queryEmptyDataAgin: true, | ||
39 | + } | ||
40 | + ); | ||
41 | + | ||
42 | + const selectOption = ref<OptionsItem>(); | ||
43 | + const options = ref<OptionsItem[]>([]); | ||
44 | + const loading = ref(false); | ||
45 | + const isFirstLoad = ref(true); | ||
46 | + const emitData = ref<any[]>([]); | ||
47 | + const { t } = useI18n(); | ||
48 | + | ||
49 | + // Embedded in the form, just use the hook binding to perform form verification | ||
50 | + const [state] = useRuleFormItem(props, 'value', 'change', emitData); | ||
51 | + | ||
52 | + const getOptions = computed(() => { | ||
53 | + const { labelField, valueField = 'value', numberToString } = props; | ||
54 | + const _options = unref(options); | ||
55 | + | ||
56 | + if ( | ||
57 | + unref(selectOption) && | ||
58 | + !_options.find((item) => get(item, valueField) === get(unref(selectOption), valueField)) | ||
59 | + ) { | ||
60 | + _options.push(unref(selectOption)!); | ||
61 | + } | ||
62 | + return _options.reduce((prev, next: Recordable) => { | ||
63 | + if (next) { | ||
64 | + const value = get(next, valueField); | ||
65 | + const label = get(next, labelField); | ||
66 | + prev.push({ | ||
67 | + ...omit(next, [labelField, valueField]), | ||
68 | + label, | ||
69 | + value: numberToString ? `${value}` : value, | ||
70 | + }); | ||
71 | + } | ||
72 | + return prev; | ||
73 | + }, [] as OptionsItem[]); | ||
74 | + }); | ||
75 | + | ||
76 | + watchEffect(() => { | ||
77 | + props.immediate && fetch(); | ||
78 | + }); | ||
79 | + | ||
80 | + watch( | ||
81 | + () => props.params, | ||
82 | + () => { | ||
83 | + !unref(isFirstLoad) && fetch(); | ||
84 | + }, | ||
85 | + { deep: true } | ||
86 | + ); | ||
87 | + | ||
88 | + watch( | ||
89 | + () => props.value, | ||
90 | + async (target) => { | ||
91 | + if (target && props.queryApi && isFunction(props.queryApi)) { | ||
92 | + if (unref(getOptions).find((item) => item.value === target)) return; | ||
93 | + const detail = await props.queryApi(target); | ||
94 | + if ( | ||
95 | + unref(options).find( | ||
96 | + (item) => get(item, props.valueField) === get(detail, props.valueField) | ||
97 | + ) | ||
98 | + ) | ||
99 | + return; | ||
100 | + | ||
101 | + selectOption.value = detail as OptionsItem; | ||
102 | + } | ||
103 | + }, | ||
104 | + { | ||
105 | + immediate: true, | ||
106 | + } | ||
107 | + ); | ||
108 | + | ||
109 | + async function fetch(searchText?: string) { | ||
110 | + const api = props.api; | ||
111 | + if (!api || !isFunction(api)) return; | ||
112 | + options.value = []; | ||
113 | + try { | ||
114 | + loading.value = true; | ||
115 | + const params = | ||
116 | + props.params && isFunction(props.params) ? props.params(searchText) : props.params; | ||
117 | + | ||
118 | + const res = await api(params); | ||
119 | + | ||
120 | + if (Array.isArray(res)) { | ||
121 | + options.value = res; | ||
122 | + emitChange(); | ||
123 | + return; | ||
124 | + } | ||
125 | + | ||
126 | + if (props.resultField) { | ||
127 | + options.value = get(res, props.resultField) || []; | ||
128 | + } | ||
129 | + emitChange(); | ||
130 | + } catch (error) { | ||
131 | + console.warn(error); | ||
132 | + } finally { | ||
133 | + loading.value = false; | ||
134 | + } | ||
135 | + } | ||
136 | + | ||
137 | + async function handleFetch() { | ||
138 | + const { immediate } = props; | ||
139 | + if (!immediate && unref(isFirstLoad)) { | ||
140 | + await fetch(); | ||
141 | + isFirstLoad.value = false; | ||
142 | + } | ||
143 | + } | ||
144 | + | ||
145 | + function emitChange() { | ||
146 | + emit('options-change', unref(getOptions)); | ||
147 | + } | ||
148 | + | ||
149 | + function handleChange(value: string, ...args) { | ||
150 | + emitData.value = args; | ||
151 | + if (!value && props.queryEmptyDataAgin) fetch(); | ||
152 | + } | ||
153 | + | ||
154 | + const debounceSearchFunction = useDebounceFn(fetch, 300); | ||
155 | +</script> | ||
156 | + | ||
157 | +<template> | ||
158 | + <Select | ||
159 | + v-bind="$attrs" | ||
160 | + show-search | ||
161 | + @dropdownVisibleChange="handleFetch" | ||
162 | + @change="handleChange" | ||
163 | + :options="getOptions" | ||
164 | + :filter-option="false" | ||
165 | + @search="debounceSearchFunction" | ||
166 | + v-model:value="state" | ||
167 | + > | ||
168 | + <template #[item]="data" v-for="item in Object.keys($slots)"> | ||
169 | + <slot :name="item" v-bind="data || {}"></slot> | ||
170 | + </template> | ||
171 | + <template #suffixIcon v-if="loading"> | ||
172 | + <LoadingOutlined spin /> | ||
173 | + </template> | ||
174 | + <template #notFoundContent v-if="loading"> | ||
175 | + <span> | ||
176 | + <LoadingOutlined spin class="mr-1" /> | ||
177 | + {{ t('component.form.apiSelectNotFound') }} | ||
178 | + </span> | ||
179 | + </template> | ||
180 | + </Select> | ||
181 | +</template> |
src/enums/otaEnum.ts
0 → 100644
@@ -13,11 +13,16 @@ import { OrgTreeSelect } from '/@/views/common/OrgTreeSelect'; | @@ -13,11 +13,16 @@ import { OrgTreeSelect } from '/@/views/common/OrgTreeSelect'; | ||
13 | import { TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum'; | 13 | import { TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum'; |
14 | import { HexInput, InputTypeEnum } from '../../profiles/components/ObjectModelForm/HexInput'; | 14 | import { HexInput, InputTypeEnum } from '../../profiles/components/ObjectModelForm/HexInput'; |
15 | import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; | 15 | import { DeviceProfileDetail } from '/@/api/device/model/deviceConfigModel'; |
16 | +import ApiQuerySelectVue from '/@/components/Form/src/components/ApiQuerySelect.vue'; | ||
17 | +import { getDeviceProfileOtaPackages, getOtaPackageInfo } from '/@/api/ota'; | ||
18 | +import { QueryDeviceProfileOtaPackagesType } from '/@/api/ota/model'; | ||
19 | +import { OTAPackageType } from '/@/enums/otaEnum'; | ||
16 | 20 | ||
17 | useComponentRegister('JSONEditor', JSONEditor); | 21 | useComponentRegister('JSONEditor', JSONEditor); |
18 | useComponentRegister('LockControlGroup', LockControlGroup); | 22 | useComponentRegister('LockControlGroup', LockControlGroup); |
19 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); | 23 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); |
20 | useComponentRegister('HexInput', HexInput); | 24 | useComponentRegister('HexInput', HexInput); |
25 | +useComponentRegister('ApiQuerySelect', ApiQuerySelectVue); | ||
21 | 26 | ||
22 | export enum TypeEnum { | 27 | export enum TypeEnum { |
23 | IS_GATEWAY = 'GATEWAY', | 28 | IS_GATEWAY = 'GATEWAY', |
@@ -115,6 +120,7 @@ export const step1Schemas: FormSchema[] = [ | @@ -115,6 +120,7 @@ export const step1Schemas: FormSchema[] = [ | ||
115 | component: 'Input', | 120 | component: 'Input', |
116 | show: false, | 121 | show: false, |
117 | }, | 122 | }, |
123 | + | ||
118 | { | 124 | { |
119 | field: 'profileId', | 125 | field: 'profileId', |
120 | label: '所属产品', | 126 | label: '所属产品', |
@@ -156,7 +162,6 @@ export const step1Schemas: FormSchema[] = [ | @@ -156,7 +162,6 @@ export const step1Schemas: FormSchema[] = [ | ||
156 | const { profileId } = formModel; | 162 | const { profileId } = formModel; |
157 | if (profileId) { | 163 | if (profileId) { |
158 | const selectRecord = options.find((item) => item.value === profileId); | 164 | const selectRecord = options.find((item) => item.value === profileId); |
159 | - | ||
160 | selectRecord && | 165 | selectRecord && |
161 | setFieldsValue({ | 166 | setFieldsValue({ |
162 | transportType: selectRecord!.transportType, | 167 | transportType: selectRecord!.transportType, |
@@ -373,6 +378,65 @@ export const step1Schemas: FormSchema[] = [ | @@ -373,6 +378,65 @@ export const step1Schemas: FormSchema[] = [ | ||
373 | component: 'Input', | 378 | component: 'Input', |
374 | slot: 'deviceAddress', | 379 | slot: 'deviceAddress', |
375 | }, | 380 | }, |
381 | + | ||
382 | + { | ||
383 | + field: 'firmwareId', | ||
384 | + label: '分配的固件', | ||
385 | + component: 'ApiQuerySelect', | ||
386 | + ifShow: ({ model }) => model?.isUpdate, | ||
387 | + componentProps: ({ formModel }) => { | ||
388 | + return { | ||
389 | + placeholder: '请选择分配的固件', | ||
390 | + api: async (params: QueryDeviceProfileOtaPackagesType) => { | ||
391 | + if (!params.deviceProfileId) return []; | ||
392 | + const result = await getDeviceProfileOtaPackages(params); | ||
393 | + return result.data.map((item) => ({ label: item.title, value: item.id.id })); | ||
394 | + }, | ||
395 | + params: (textSearch: string) => { | ||
396 | + return { | ||
397 | + textSearch, | ||
398 | + page: 0, | ||
399 | + type: OTAPackageType.FIRMWARE, | ||
400 | + pageSize: 10, | ||
401 | + deviceProfileId: formModel?.profileId, | ||
402 | + }; | ||
403 | + }, | ||
404 | + queryApi: async (id: string) => { | ||
405 | + const result = await getOtaPackageInfo(id); | ||
406 | + return { label: result.title, value: result.id.id }; | ||
407 | + }, | ||
408 | + }; | ||
409 | + }, | ||
410 | + }, | ||
411 | + { | ||
412 | + field: 'softwareId', | ||
413 | + label: '分配的软件', | ||
414 | + component: 'ApiQuerySelect', | ||
415 | + ifShow: ({ model }) => model?.isUpdate, | ||
416 | + componentProps: ({ formModel }) => { | ||
417 | + return { | ||
418 | + placeholder: '请选择分配的软件', | ||
419 | + api: async (params: QueryDeviceProfileOtaPackagesType) => { | ||
420 | + if (!params.deviceProfileId) return []; | ||
421 | + const result = await getDeviceProfileOtaPackages(params); | ||
422 | + return result.data.map((item) => ({ label: item.title, value: item.id.id })); | ||
423 | + }, | ||
424 | + params: (textSearch: string) => { | ||
425 | + return { | ||
426 | + textSearch, | ||
427 | + page: 0, | ||
428 | + type: OTAPackageType.SOFTWARE, | ||
429 | + pageSize: 10, | ||
430 | + deviceProfileId: formModel?.profileId, | ||
431 | + }; | ||
432 | + }, | ||
433 | + queryApi: async (id: string) => { | ||
434 | + const result = await getOtaPackageInfo(id); | ||
435 | + return { label: result.title, value: result.id.id }; | ||
436 | + }, | ||
437 | + }; | ||
438 | + }, | ||
439 | + }, | ||
376 | { | 440 | { |
377 | field: 'description', | 441 | field: 'description', |
378 | label: '备注', | 442 | label: '备注', |
@@ -405,7 +405,6 @@ | @@ -405,7 +405,6 @@ | ||
405 | icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem], | 405 | icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem], |
406 | }); | 406 | }); |
407 | } | 407 | } |
408 | - | ||
409 | setFieldsValue({ | 408 | setFieldsValue({ |
410 | ...data, | 409 | ...data, |
411 | code: data?.code, | 410 | code: data?.code, |