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 | GetDeviceProfileInfosParams, |
7 | 7 | GetOtaPackagesParams, |
8 | 8 | OtaRecordDatum, |
9 | + QueryDeviceProfileOtaPackagesType, | |
9 | 10 | TBResponse, |
10 | 11 | UploadOtaPackagesParams, |
11 | 12 | } from './model'; |
... | ... | @@ -137,3 +138,18 @@ export const getDeviceProfileInfoById = (id: string) => { |
137 | 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 | 2 | import { ALG, CheckSumWay } from '/@/views/operation/ota/config/packageDetail.config'; |
3 | +import { OrderByEnum } from '/@/views/rule/designer/enum/form'; | |
2 | 4 | |
3 | 5 | export interface GetOtaPackagesParams { |
4 | 6 | pageSize: number; |
... | ... | @@ -105,3 +107,13 @@ export interface DeviceProfileRecord { |
105 | 107 | type: string; |
106 | 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 | 13 | import { TCPProtocolTypeEnum, TransportTypeEnum } from '/@/enums/deviceEnum'; |
14 | 14 | import { HexInput, InputTypeEnum } from '../../profiles/components/ObjectModelForm/HexInput'; |
15 | 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 | 21 | useComponentRegister('JSONEditor', JSONEditor); |
18 | 22 | useComponentRegister('LockControlGroup', LockControlGroup); |
19 | 23 | useComponentRegister('OrgTreeSelect', OrgTreeSelect); |
20 | 24 | useComponentRegister('HexInput', HexInput); |
25 | +useComponentRegister('ApiQuerySelect', ApiQuerySelectVue); | |
21 | 26 | |
22 | 27 | export enum TypeEnum { |
23 | 28 | IS_GATEWAY = 'GATEWAY', |
... | ... | @@ -115,6 +120,7 @@ export const step1Schemas: FormSchema[] = [ |
115 | 120 | component: 'Input', |
116 | 121 | show: false, |
117 | 122 | }, |
123 | + | |
118 | 124 | { |
119 | 125 | field: 'profileId', |
120 | 126 | label: '所属产品', |
... | ... | @@ -156,7 +162,6 @@ export const step1Schemas: FormSchema[] = [ |
156 | 162 | const { profileId } = formModel; |
157 | 163 | if (profileId) { |
158 | 164 | const selectRecord = options.find((item) => item.value === profileId); |
159 | - | |
160 | 165 | selectRecord && |
161 | 166 | setFieldsValue({ |
162 | 167 | transportType: selectRecord!.transportType, |
... | ... | @@ -373,6 +378,65 @@ export const step1Schemas: FormSchema[] = [ |
373 | 378 | component: 'Input', |
374 | 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 | 441 | field: 'description', |
378 | 442 | label: '备注', | ... | ... |