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, |