Commit be64163857f9003c7ce0a7dbe49f5205dfe3b8cd

Authored by xp.Huang
2 parents 5180d8fb 8c3860d7

Merge branch 'feat/device-ota-package' into 'main_dev'

feat: 设备编辑时新增ota升级包

See merge request yunteng/thingskit-front!1220
... ... @@ -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>
... ...
  1 +export enum OTAPackageType {
  2 + FIRMWARE = 'FIRMWARE',
  3 + SOFTWARE = 'SOFTWARE',
  4 +}
... ...
... ... @@ -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: '备注',
... ...
... ... @@ -405,7 +405,6 @@
405 405 icon: [{ uid: buildUUID(), name: 'name', url: deviceInfo.avatar } as FileItem],
406 406 });
407 407 }
408   -
409 408 setFieldsValue({
410 409 ...data,
411 410 code: data?.code,
... ...