Commit c6c9460dfba2afc0a24b3cbe0b22e4cc1e01d7fa

Authored by ww
1 parent faad3adc

feat: basic implement operation/ota page

  1 +import {
  2 + CreateOtaPackageParams,
  3 + DefaultDeviceProfileInfo,
  4 + DeviceProfileRecord,
  5 + GetDeviceProfileInfosParams,
  6 + GetOtaPackagesParams,
  7 + OtaRecordDatum,
  8 + TBResponse,
  9 + UploadOtaPackagesParams,
  10 +} from './model';
  11 +import { defHttp } from '/@/utils/http/axios';
  12 +
  13 +enum Api {
  14 + GET_OTA_PACKAGES = '/otaPackages',
  15 + CREATE_OTA_PACKAGES = '/otaPackage',
  16 + UPLOAD_OTA_PACKAGES = '/otaPackage',
  17 + DELETE_OTA_PACKAGES = '/otaPackage',
  18 + GET_DEVICE_PROFILE_INFO_DEFAULT = '/deviceProfileInfo/default',
  19 + GET_OTA_PACKAGE_INFO = '/otaPackage/info',
  20 + DOWNLOAD_PACKAGE = '/otaPackage',
  21 +
  22 + GET_DEVICE_PROFILE_INFO_BY_ID = '/deviceProfileInfo',
  23 + GET_DEVICE_PROFILE_INFOS = '/deviceProfileInfos',
  24 +}
  25 +
  26 +/**
  27 + * @description 获取ota包列表
  28 + * @param params
  29 + * @returns
  30 + */
  31 +export const getOtaPackagesList = (params: GetOtaPackagesParams) => {
  32 + return defHttp.get<TBResponse<OtaRecordDatum>>(
  33 + {
  34 + url: Api.GET_OTA_PACKAGES,
  35 + params,
  36 + },
  37 + {
  38 + joinPrefix: false,
  39 + }
  40 + );
  41 +};
  42 +
  43 +/**
  44 + * @description 创建ota包
  45 + * @param params
  46 + * @returns
  47 + */
  48 +export const createOtaPackages = (params: CreateOtaPackageParams) => {
  49 + return defHttp.post<OtaRecordDatum>(
  50 + {
  51 + url: Api.CREATE_OTA_PACKAGES,
  52 + params,
  53 + },
  54 + { joinPrefix: false }
  55 + );
  56 +};
  57 +
  58 +/**
  59 + * @description 上传ota包
  60 + * @param param
  61 + * @returns
  62 + */
  63 +export const uploadOtaPackages = (params: UploadOtaPackagesParams) => {
  64 + return defHttp.post(
  65 + {
  66 + url: `${Api.UPLOAD_OTA_PACKAGES}/${params.otaPackageId}?checksumAlgorithm=${
  67 + params.checksumAlgorithm
  68 + }${params.checksum ? `&checksum=${params.checksum}` : ''}`,
  69 + params: params.file,
  70 + },
  71 + { joinPrefix: false }
  72 + );
  73 +};
  74 +
  75 +/**
  76 + * @description 获取设备默认信息
  77 + * @returns
  78 + */
  79 +export const getDevicePRofileInfo = () => {
  80 + return defHttp.get<DefaultDeviceProfileInfo>(
  81 + {
  82 + url: Api.GET_DEVICE_PROFILE_INFO_DEFAULT,
  83 + },
  84 + {
  85 + joinPrefix: false,
  86 + }
  87 + );
  88 +};
  89 +
  90 +export const deleteOtaPackage = (id: string) => {
  91 + return defHttp.delete(
  92 + {
  93 + url: `${Api.DELETE_OTA_PACKAGES}/${id}`,
  94 + },
  95 + { joinPrefix: false }
  96 + );
  97 +};
  98 +
  99 +export const getOtaPackageInfo = (id: string) => {
  100 + return defHttp.get<OtaRecordDatum>(
  101 + {
  102 + url: `${Api.GET_OTA_PACKAGE_INFO}/${id}`,
  103 + },
  104 + {
  105 + joinPrefix: false,
  106 + }
  107 + );
  108 +};
  109 +
  110 +export const downloadPackage = (id: string) => {
  111 + return defHttp.get({ url: `${Api.DOWNLOAD_PACKAGE}/${id}/download` }, { joinPrefix: false });
  112 +};
  113 +
  114 +export const getDeviceProfileInfos = (params: GetDeviceProfileInfosParams) => {
  115 + const { page = 0, pageSize = 10, sortOrder = 'ASC', sortProperty = 'name', textSearch } = params;
  116 + return defHttp.get<TBResponse<DeviceProfileRecord>>(
  117 + {
  118 + url: `${Api.GET_DEVICE_PROFILE_INFOS}`,
  119 + params: {
  120 + page,
  121 + pageSize,
  122 + sortOrder,
  123 + sortProperty,
  124 + textSearch,
  125 + },
  126 + },
  127 + { joinPrefix: false }
  128 + );
  129 +};
  130 +
  131 +export const getDeviceProfileInfoById = (id: string) => {
  132 + return defHttp.get<DeviceProfileRecord>(
  133 + {
  134 + url: `${Api.GET_DEVICE_PROFILE_INFO_BY_ID}/${id}`,
  135 + },
  136 + { joinPrefix: false }
  137 + );
  138 +};
... ...
  1 +import { ALG, CheckSumWay } from '/@/views/operation/ota/config/packageDetail.config';
  2 +
  3 +export interface GetOtaPackagesParams {
  4 + pageSize: number;
  5 + page: number;
  6 + textSearch?: string;
  7 + title?: string;
  8 +}
  9 +
  10 +export interface CreateOtaPackagesParams {
  11 + additionalInfo?: { description?: string };
  12 + checksum?: Nullable<number>;
  13 + checksumAlgorithm?: ALG;
  14 + deviceProfileId?: {
  15 + entityType?: string;
  16 + id?: string;
  17 + };
  18 + isURL: boolean;
  19 + tag?: string;
  20 + title?: string;
  21 + type?: CheckSumWay;
  22 + url?: string;
  23 + version?: string;
  24 +}
  25 +
  26 +export interface UploadOtaPackagesParams {
  27 + otaPackageId?: string;
  28 + checksum?: string;
  29 + checksumAlgorithm?: ALG;
  30 + file?: FormData;
  31 +}
  32 +
  33 +export interface Id {
  34 + entityType: string;
  35 + id: string;
  36 +}
  37 +
  38 +export interface AdditionalInfo {
  39 + description: string;
  40 +}
  41 +
  42 +export interface IdRecord {
  43 + entityType: string;
  44 + id: string;
  45 +}
  46 +
  47 +export interface OtaRecordDatum {
  48 + id: Id;
  49 + createdTime: any;
  50 + additionalInfo: AdditionalInfo;
  51 + tenantId: IdRecord;
  52 + deviceProfileId: IdRecord;
  53 + type: string;
  54 + title: string;
  55 + version: string;
  56 + tag: string;
  57 + url: string;
  58 + hasData: boolean;
  59 + fileName: string;
  60 + contentType: string;
  61 + checksumAlgorithm: string;
  62 + checksum: string;
  63 + dataSize?: number;
  64 +}
  65 +
  66 +export interface TBResponse<T> {
  67 + data: T[];
  68 + totalPages: number;
  69 + totalElements: number;
  70 + hasNext: boolean;
  71 +}
  72 +
  73 +export interface DefaultDeviceProfileInfo {
  74 + id: {
  75 + entityType: string;
  76 + id: string;
  77 + };
  78 +}
  79 +
  80 +export interface CreateOtaPackageParams {
  81 + title: string;
  82 + version: string;
  83 + tag: string;
  84 + type: string;
  85 + deviceProfileId: IdRecord;
  86 + isURL: boolean;
  87 + additionalInfo: AdditionalInfo;
  88 +}
  89 +
  90 +export interface GetDeviceProfileInfosParams {
  91 + pageSize?: number;
  92 + page?: number;
  93 + textSearch?: string;
  94 + sortProperty?: string;
  95 + sortOrder?: 'ASC' | 'DESC';
  96 +}
  97 +
  98 +export interface DeviceProfileRecord {
  99 + id: Id;
  100 + name: string;
  101 + image: string;
  102 + defaultDashboardId?: any;
  103 + type: string;
  104 + transportType: string;
  105 +}
... ...
... ... @@ -34,6 +34,7 @@ import { JEasyCron } from './externalCompns/components/JEasyCron';
34 34 import ColorPicker from './components/ColorPicker.vue';
35 35 import IconDrawer from './components/IconDrawer.vue';
36 36 import ApiUpload from './components/ApiUpload.vue';
  37 +import ApiSearchSelect from './components/ApiSearchSelect.vue';
37 38
38 39 const componentMap = new Map<ComponentType, Component>();
39 40
... ... @@ -75,6 +76,7 @@ componentMap.set('JEasyCron', JEasyCron);
75 76 componentMap.set('ColorPicker', ColorPicker);
76 77 componentMap.set('IconDrawer', IconDrawer);
77 78 componentMap.set('ApiUpload', ApiUpload);
  79 +componentMap.set('ApiSearchSelect', ApiSearchSelect);
78 80
79 81 export function add(compName: ComponentType, component: Component) {
80 82 componentMap.set(compName, component);
... ...
  1 +<script lang="ts" setup>
  2 + import { ref, watchEffect, computed, unref, watch } from 'vue';
  3 + import { Select } from 'ant-design-vue';
  4 + import { isFunction } from '/@/utils/is';
  5 + import { useRuleFormItem } from '/@/hooks/component/useFormItem';
  6 + import { useAttrs } from '/@/hooks/core/useAttrs';
  7 + import { get, omit } from 'lodash-es';
  8 + import { LoadingOutlined } from '@ant-design/icons-vue';
  9 + import { useI18n } from '/@/hooks/web/useI18n';
  10 +
  11 + type OptionsItem = { label: string; value: string; disabled?: boolean };
  12 +
  13 + const emit = defineEmits(['options-change', 'change']);
  14 + const props = withDefaults(
  15 + defineProps<{
  16 + value?: Recordable | number | string;
  17 + numberToString?: boolean;
  18 + api?: (arg?: Recordable) => Promise<OptionsItem[]>;
  19 + searchApi?: (arg?: Recordable) => Promise<OptionsItem[]>;
  20 + params?: Recordable;
  21 + resultField?: string;
  22 + labelField?: string;
  23 + valueField?: string;
  24 + immediate?: boolean;
  25 + }>(),
  26 + {
  27 + resultField: '',
  28 + labelField: 'label',
  29 + value: 'value',
  30 + immediate: true,
  31 + }
  32 + );
  33 + const options = ref<OptionsItem[]>([]);
  34 + const loading = ref(false);
  35 + const isFirstLoad = ref(true);
  36 + const emitData = ref<any[]>([]);
  37 + const attrs = useAttrs();
  38 + const { t } = useI18n();
  39 +
  40 + // Embedded in the form, just use the hook binding to perform form verification
  41 + const [state] = useRuleFormItem(props, 'value', 'change', emitData);
  42 +
  43 + const getOptions = computed(() => {
  44 + const { labelField, valueField = 'value', numberToString } = props;
  45 +
  46 + return unref(options).reduce((prev, next: Recordable) => {
  47 + if (next) {
  48 + const value = next[valueField];
  49 + prev.push({
  50 + label: next[labelField],
  51 + value: numberToString ? `${value}` : value,
  52 + ...omit(next, [labelField, valueField]),
  53 + });
  54 + }
  55 + return prev;
  56 + }, [] as OptionsItem[]);
  57 + });
  58 +
  59 + watchEffect(() => {
  60 + props.immediate && fetch();
  61 + });
  62 +
  63 + watch(
  64 + () => props.params,
  65 + () => {
  66 + !unref(isFirstLoad) && fetch();
  67 + },
  68 + { deep: true }
  69 + );
  70 +
  71 + async function fetch() {
  72 + const api = props.api;
  73 + if (!api || !isFunction(api)) return;
  74 + options.value = [];
  75 + try {
  76 + loading.value = true;
  77 + const res = await api(props.params);
  78 + if (Array.isArray(res)) {
  79 + options.value = res;
  80 + emitChange();
  81 + return;
  82 + }
  83 + if (props.resultField) {
  84 + options.value = get(res, props.resultField) || [];
  85 + }
  86 + console.log('fetch', unref(options));
  87 + emitChange();
  88 + } catch (error) {
  89 + console.warn(error);
  90 + } finally {
  91 + loading.value = false;
  92 + }
  93 + }
  94 +
  95 + async function handleFetch() {
  96 + if (!props.immediate && unref(isFirstLoad)) {
  97 + await fetch();
  98 + isFirstLoad.value = false;
  99 + }
  100 + }
  101 +
  102 + function emitChange() {
  103 + emit('options-change', unref(getOptions));
  104 + }
  105 +
  106 + function handleChange(_, ...args) {
  107 + emitData.value = args;
  108 + if (!_) handleSearch();
  109 + }
  110 +
  111 + async function handleSearch(params?: string) {
  112 + const searchApi = props.searchApi;
  113 + if (!searchApi || !isFunction(searchApi)) return;
  114 + options.value = [];
  115 + try {
  116 + loading.value = true;
  117 + const res = await searchApi({ ...props.params, text: params });
  118 + if (Array.isArray(res)) {
  119 + options.value = res;
  120 + emitChange();
  121 + return;
  122 + }
  123 + if (props.resultField) {
  124 + options.value = get(res, props.resultField) || [];
  125 + }
  126 + emitChange();
  127 + } catch (error) {
  128 + console.warn(error);
  129 + } finally {
  130 + loading.value = false;
  131 + }
  132 + }
  133 +</script>
  134 +
  135 +<template>
  136 + <Select
  137 + @dropdownVisibleChange="handleFetch"
  138 + v-bind="attrs"
  139 + @change="handleChange"
  140 + :options="getOptions"
  141 + @search="handleSearch"
  142 + v-model:value="state"
  143 + >
  144 + <template #[item]="data" v-for="item in Object.keys($slots)">
  145 + <slot :name="item" v-bind="data || {}"></slot>
  146 + </template>
  147 + <template #suffixIcon v-if="loading">
  148 + <LoadingOutlined spin />
  149 + </template>
  150 + <template #notFoundContent v-if="loading">
  151 + <span>
  152 + <LoadingOutlined spin class="mr-1" />
  153 + {{ t('component.form.apiSelectNotFound') }}
  154 + </span>
  155 + </template>
  156 + </Select>
  157 +</template>
... ...
... ... @@ -115,4 +115,5 @@ export type ComponentType =
115 115 | 'Rate'
116 116 | 'ColorPicker'
117 117 | 'IconDrawer'
118   - | 'ApiUpload';
  118 + | 'ApiUpload'
  119 + | 'ApiSearchSelect';
... ...
1 1 <script lang="ts" setup>
2 2 import { BasicModal, useModalInner } from '/@/components/Modal';
3 3 import { BasicForm, useForm } from '/@/components/Form';
4   - import { formSchema } from '../config/packageDetail.config';
5   - defineEmits(['register']);
  4 + import { ALG, formSchema, PackageField } from '../config/packageDetail.config';
  5 + import { ref } from 'vue';
  6 + import { createOtaPackages, uploadOtaPackages, deleteOtaPackage } from '/@/api/ota';
  7 + import { CreateOtaPackageParams } from '/@/api/ota/model';
6 8
7   - const [registerModal] = useModalInner((_record: Recordable) => {});
  9 + interface FieldsValue extends CreateOtaPackageParams {
  10 + fileList: { file: File }[];
  11 + checksum?: string;
  12 + checksumAlgorithm?: ALG;
  13 + }
8 14
9   - const [registerForm] = useForm({
  15 + const emit = defineEmits(['register', 'update:list']);
  16 +
  17 + const loading = ref(false);
  18 +
  19 + const [registerModal, { changeLoading, closeModal }] = useModalInner();
  20 +
  21 + const setLoading = (status: boolean) => {
  22 + changeLoading(status);
  23 + loading.value = status;
  24 + };
  25 +
  26 + const [registerForm, { getFieldsValue, validate }] = useForm({
10 27 schemas: formSchema,
11 28 showActionButtonGroup: false,
12 29 // labelCol: { span: 8 },
13 30 labelWidth: 100,
14 31 wrapperCol: { span: 16 },
15 32 });
  33 +
  34 + const handleUploadFile = async (value: FieldsValue, id: string) => {
  35 + try {
  36 + const file = new FormData();
  37 + file.append('file', value.fileList.at(0)!.file);
  38 + await uploadOtaPackages({
  39 + otaPackageId: id,
  40 + file,
  41 + checksumAlgorithm: value.checksumAlgorithm,
  42 + });
  43 + } catch (error) {
  44 + console.log(error);
  45 + if ((error as { status: number }).status) {
  46 + await deleteOtaPackage(id);
  47 + }
  48 + } finally {
  49 + closeModal();
  50 + emit('update:list');
  51 + }
  52 + };
  53 +
  54 + const handleCreatePackages = async (value: FieldsValue) => {
  55 + try {
  56 + setLoading(true);
  57 + const { id } = await createOtaPackages(value);
  58 + const { isURL } = value;
  59 + if (!isURL) {
  60 + await handleUploadFile(value, id.id);
  61 + }
  62 + } catch (error) {
  63 + } finally {
  64 + setLoading(false);
  65 + }
  66 + };
  67 +
  68 + const handleSubmit = async () => {
  69 + try {
  70 + await validate();
  71 + const value = getFieldsValue();
  72 + value[PackageField.DEVICE_PROFILE_INFO] = JSON.parse(value[PackageField.DEVICE_PROFILE_INFO]);
  73 + value[PackageField.ADDITIONAL_INFO] = {
  74 + [PackageField.DESCRIPTION]: value[PackageField.DESCRIPTION],
  75 + };
  76 + handleCreatePackages(value as unknown as FieldsValue);
  77 + } catch (error) {
  78 + window.console.error(error);
  79 + }
  80 + };
16 81 </script>
17 82
18 83 <template>
19   - <BasicModal title="包管理" @register="registerModal">
  84 + <BasicModal title="包管理" destroy-on-close @register="registerModal" @ok="handleSubmit">
20 85 <BasicForm @register="registerForm" />
21 86 </BasicModal>
22 87 </template>
... ...
  1 +<script lang="ts" setup>
  2 + import { OtaRecordDatum } from '/@/api/ota/model';
  3 + import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
  4 + import { Tabs, Space, Button } from 'ant-design-vue';
  5 + import { useForm, BasicForm } from '/@/components/Form';
  6 + import { formSchema } from '../config/packageDrawer.config';
  7 + import { ref, unref } from 'vue';
  8 + import { PackageField } from '../config/packageDetail.config';
  9 + import { useMessage } from '/@/hooks/web/useMessage';
  10 + import { deleteOtaPackage, getDeviceProfileInfoById, getOtaPackageInfo } from '/@/api/ota';
  11 + import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
  12 + import { useDownload } from '../hook/useDownload';
  13 +
  14 + const emit = defineEmits(['register', 'update:list']);
  15 +
  16 + const otaRecord = ref<OtaRecordDatum>({} as unknown as OtaRecordDatum);
  17 +
  18 + const { createConfirm, createMessage } = useMessage();
  19 +
  20 + const [registerForm, { setFieldsValue, getFieldsValue }] = useForm({
  21 + schemas: formSchema,
  22 + showActionButtonGroup: false,
  23 + disabled: true,
  24 + });
  25 +
  26 + const [register, { closeDrawer }] = useDrawerInner(async (id: string) => {
  27 + try {
  28 + const record = await getOtaPackageInfo(id);
  29 + const deviceInfo = await getDeviceProfileInfoById(record.deviceProfileId.id);
  30 + setFieldsValue({
  31 + ...record,
  32 + [PackageField.DESCRIPTION]: record.additionalInfo.description,
  33 + [PackageField.DEVICE_PROFILE_INFO]: deviceInfo.name,
  34 + });
  35 + otaRecord.value = record;
  36 + } catch (error) {}
  37 + });
  38 +
  39 + const openDetailPage = () => {};
  40 +
  41 + const downloadPackage = async () => {
  42 + await useDownload(unref(otaRecord));
  43 + };
  44 +
  45 + const deletePackage = () => {
  46 + createConfirm({
  47 + iconType: 'warning',
  48 + content: '是否确认删除操作?',
  49 + onOk: async () => {
  50 + try {
  51 + await deleteOtaPackage(otaRecord.value.id.id);
  52 + closeDrawer();
  53 + emit('update:list');
  54 + createMessage.success('删除成功');
  55 + } catch (error) {}
  56 + },
  57 + });
  58 + };
  59 +
  60 + const { clipboardRef, isSuccessRef } = useCopyToClipboard('');
  61 + const copyPackageId = () => {
  62 + clipboardRef.value = otaRecord.value.id.id;
  63 + if (unref(isSuccessRef)) createMessage.success('复制成功');
  64 + };
  65 +
  66 + const copyUrl = () => {
  67 + if (!unref(otaRecord).url) {
  68 + createMessage.warning('无直接URL');
  69 + return;
  70 + }
  71 + clipboardRef.value = otaRecord.value.url;
  72 + if (unref(isSuccessRef)) createMessage.success('复制成功');
  73 + };
  74 +
  75 + const handleSubmit = async () => {
  76 + getFieldsValue();
  77 + };
  78 +</script>
  79 +
  80 +<template>
  81 + <BasicDrawer width="40%" class="relative" @register="register" @ok="handleSubmit">
  82 + <Tabs>
  83 + <Tabs.TabPane tab="详情" key="detail">
  84 + <Space>
  85 + <Button type="primary" @click="openDetailPage">打开详情页</Button>
  86 + <Button type="primary" @click="downloadPackage" :disabled="!!otaRecord.url">
  87 + 下载包
  88 + </Button>
  89 + <Button type="primary" @click="deletePackage" danger>删除包</Button>
  90 + </Space>
  91 + <div class="mt-3">
  92 + <Space>
  93 + <Button type="primary" @click="copyPackageId">复制包ID</Button>
  94 + <Button type="primary" @click="copyUrl">复制直接URL</Button>
  95 + </Space>
  96 + </div>
  97 + <BasicForm @register="registerForm" />
  98 + </Tabs.TabPane>
  99 + </Tabs>
  100 + <div
  101 + class="absolute right-0 bottom-0 w-full border-t bg-light-50 border-t-gray-100 py-2 px-4 text-right"
  102 + >
  103 + <Button class="mr-2">取消</Button>
  104 + <Button type="primary">保存</Button>
  105 + </div>
  106 + </BasicDrawer>
  107 +</template>
... ...
1 1 import { BasicColumn, FormSchema } from '/@/components/Table';
2   -import { PackageField } from './packageDetail.config';
  2 +import { PackageField, PackageType } from './packageDetail.config';
  3 +import { dateUtil } from '/@/utils/dateUtil';
  4 +import { DEFAULT_DATE_FORMAT } from '/@/views/visual/board/detail/config/util';
  5 +
  6 +export interface ModalPassRecord {
  7 + isUpdate: boolean;
  8 + record?: Recordable;
  9 +}
  10 +
3 11 export const columns: BasicColumn[] = [
4 12 {
5 13 title: '创建时间',
6 14 dataIndex: PackageField.CREATE_TIME,
  15 + format(text) {
  16 + return dateUtil(text).format(DEFAULT_DATE_FORMAT);
  17 + },
7 18 width: 120,
8 19 },
9 20 {
... ... @@ -18,29 +29,54 @@ export const columns: BasicColumn[] = [
18 29 },
19 30 {
20 31 title: '版本标签',
21   - dataIndex: PackageField.VERSION_LABEL,
  32 + dataIndex: PackageField.VERSION_TAG,
22 33 width: 120,
23 34 },
24 35 {
25 36 title: '包类型',
26 37 dataIndex: PackageField.PACKAGE_TYPE,
  38 + format: (text) => {
  39 + return text === PackageType.FIRMWARE ? '固件' : '软件';
  40 + },
27 41 width: 120,
28 42 },
29 43 {
30 44 title: '直接URL',
31   - dataIndex: PackageField.PACKAGE_EXTERNAL_URL,
  45 + dataIndex: PackageField.URL,
  46 + width: 120,
  47 + },
  48 + {
  49 + title: '文件名',
  50 + dataIndex: PackageField.FILE_NAME,
32 51 width: 120,
33 52 },
34 53 {
35 54 title: '文件大小',
36 55 dataIndex: PackageField.FILE_SIZE,
  56 + format(text, record) {
  57 + return record[PackageField.FILE_SIZE]
  58 + ? `${Math.ceil(((text as unknown as number) / 1024) * 100) / 100}kb`
  59 + : '';
  60 + },
37 61 width: 120,
38 62 },
39 63 {
40 64 title: '校验和',
41 65 dataIndex: PackageField.CHECK_SUM,
  66 + format(text, record) {
  67 + return text ? `${record[PackageField.CHECK_SUM_ALG]}: ${text.slice(0, 11)}` : '';
  68 + },
42 69 width: 120,
43 70 },
  71 + {
  72 + title: '操作',
  73 + dataIndex: 'action',
  74 + flag: 'ACTION',
  75 + fixed: 'right',
  76 + slots: {
  77 + customRender: 'action',
  78 + },
  79 + },
44 80 ];
45 81
46 82 export const searchFormSchema: FormSchema[] = [
... ...
  1 +import { getDevicePRofileInfo, getDeviceProfileInfos } from '/@/api/ota';
  2 +import { Id } from '/@/api/ota/model';
1 3 import { FormSchema } from '/@/components/Form';
  4 +import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
2 5
3 6 export enum PackageField {
4 7 TITLE = 'title',
5 8 VERSION = 'version',
6   - VERSION_LABEL = 'versionLabel',
7   - DEVICE_CONFIGURATION = 'deviceConfiguration',
8   - PACKAGE_TYPE = 'packageType',
9   - PACKAGE_UPDATE_TYPE = 'PackageUpdateType',
10   - PACKAGE_BINARY_FILE = 'fileList',
11   - PACKAGE_EXTERNAL_URL = 'packageEexternalUrl',
12   - CHECK_SUM_WAY = 'checkSumWay',
13   - ALG = 'alg',
14   - CHECK_SUM = 'checkSum',
  9 + VERSION_TAG = 'tag',
  10 + PACKAGE_TYPE = 'type',
  11 + URL = 'url',
  12 + IS_URL = 'isUrl',
  13 + CHECK_SUM_ALG = 'checksumAlgorithm',
  14 + CHECK_SUM = 'checksum',
15 15 DESCRIPTION = 'description',
  16 + DEVICE_PROFILE_INFO = 'deviceProfileId',
16 17
17   - CREATE_TIME = 'createTIme',
18   - FILE_SIZE = 'fileSize',
19   -}
  18 + CREATE_TIME = 'createdTime',
  19 + FILE_SIZE = 'dataSize',
  20 + FILE_NAME = 'fileName',
  21 + ADDITIONAL_INFO = 'additionalInfo',
  22 + FILE_TYPE = 'contentType',
20 23
21   -export enum PackageUpdateType {
22   - BINARY_FILE = 'binaryFile',
23   - EXTERNAL_URL = 'externalUrl',
  24 + PACKAGE_BINARY_FILE = 'fileList',
  25 + VALIDATE_WAY = 'validateWay',
24 26 }
25 27
26 28 export enum PackageType {
27   - FIRMWARE = 'firmware',
28   - SOFTWARE = 'software',
  29 + FIRMWARE = 'FIRMWARE',
  30 + SOFTWARE = 'SOFTWARE',
29 31 }
30 32
31 33 export enum CheckSumWay {
... ... @@ -34,13 +36,13 @@ export enum CheckSumWay {
34 36 }
35 37
36 38 export enum ALG {
37   - MD5 = 'md5',
38   - SHA_256 = 'sha-256',
39   - SHA_384 = 'sha-384',
40   - SHA_512 = 'sha-512',
41   - CRC_32 = 'crc-32',
42   - MURMUR3_32 = 'murmur3-32',
43   - MURMUR3_128 = 'murmur3-128',
  39 + MD5 = 'MD$',
  40 + SHA_256 = 'SHA256',
  41 + SHA_384 = 'SHA384',
  42 + SHA_512 = 'SHA512',
  43 + CRC_32 = 'CRC32',
  44 + MURMUR3_32 = 'MURMUR332',
  45 + MURMUR3_128 = 'MURMUR3128',
44 46 }
45 47
46 48 export const formSchema: FormSchema[] = [
... ... @@ -63,25 +65,51 @@ export const formSchema: FormSchema[] = [
63 65 },
64 66 },
65 67 {
66   - field: PackageField.VERSION_LABEL,
  68 + field: PackageField.VERSION_TAG,
67 69 label: '版本标签',
68 70 component: 'Input',
69 71 helpMessage: ['自定义标签应与您设备报告的软件包版本相匹配'],
70   - componentProps: {
71   - placeholder: '请输入版本标签',
  72 + componentProps: () => {
  73 + return {
  74 + placeholder: '请输入版本标签',
  75 + };
72 76 },
73 77 },
74 78 {
75   - field: PackageField.DEVICE_CONFIGURATION,
  79 + field: PackageField.DEVICE_PROFILE_INFO,
  80 + label: '',
  81 + component: 'Input',
  82 + show: false,
  83 + },
  84 + {
  85 + field: PackageField.DEVICE_PROFILE_INFO,
76 86 label: '设备配置',
77   - component: 'Select',
  87 + component: 'ApiSearchSelect',
78 88 helpMessage: ['上传的包仅适用于具有所选配置文件的设备'],
79 89 defaultValue: 'default',
80 90 rules: [{ required: true, message: '设备配置为必填项' }],
81   - componentProps: () => {
  91 + componentProps: ({ formActionType }) => {
  92 + const { setFieldsValue } = formActionType;
82 93 return {
83   - options: [{ label: 'default', value: 'default' }],
84 94 placeholder: '请选择设备配置',
  95 + showSearch: true,
  96 + resultField: 'data',
  97 + labelField: 'name',
  98 + valueField: 'id',
  99 + api: async () => {
  100 + const data = await getDevicePRofileInfo();
  101 + data.id = JSON.stringify(data.id) as unknown as Id;
  102 + setFieldsValue({ [PackageField.DEVICE_PROFILE_INFO]: data.id });
  103 + return { data: [data] };
  104 + },
  105 + searchApi: async (params: Recordable) => {
  106 + const data = await getDeviceProfileInfos({ textSearch: params.text });
  107 + data.data = data.data.map((item) => ({
  108 + ...item,
  109 + id: JSON.stringify(item.id) as unknown as Id,
  110 + }));
  111 + return data;
  112 + },
85 113 };
86 114 },
87 115 },
... ... @@ -103,15 +131,16 @@ export const formSchema: FormSchema[] = [
103 131 },
104 132 },
105 133 {
106   - field: PackageField.PACKAGE_UPDATE_TYPE,
  134 + field: PackageField.IS_URL,
107 135 label: '上传方式',
108 136 component: 'RadioGroup',
109   - defaultValue: PackageUpdateType.BINARY_FILE,
  137 + defaultValue: false,
110 138 componentProps: () => {
111 139 return {
  140 + defaultValue: false,
112 141 options: [
113   - { label: '上传二进制文件', value: PackageUpdateType.BINARY_FILE },
114   - { label: '使用外部URL', value: PackageUpdateType.EXTERNAL_URL },
  142 + { label: '上传二进制文件', value: false },
  143 + { label: '使用外部URL', value: true },
115 144 ],
116 145 };
117 146 },
... ... @@ -120,25 +149,25 @@ export const formSchema: FormSchema[] = [
120 149 field: PackageField.PACKAGE_BINARY_FILE,
121 150 label: '二进制文件',
122 151 ifShow: ({ model }) => {
123   - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.BINARY_FILE;
  152 + return !model[PackageField.IS_URL];
124 153 },
125 154 component: 'ApiUpload',
126 155 valueField: PackageField.PACKAGE_BINARY_FILE,
127 156 changeEvent: `update:${PackageField.PACKAGE_BINARY_FILE}`,
  157 + rules: [{ required: true, message: '请上传二进制文件', type: 'array' }],
128 158 componentProps: {
129 159 maxFileLimit: 1,
130   - api: (_file: File) => {
131   - console.log({ _file });
132   - return { uid: _file.uid, name: _file.name };
  160 + api: (file: FileItem) => {
  161 + return { uid: file.uid, name: file.name, file };
133 162 },
134 163 },
135 164 },
136 165 {
137   - field: PackageField.PACKAGE_EXTERNAL_URL,
  166 + field: PackageField.URL,
138 167 label: '外部URL',
139 168 component: 'Input',
140 169 ifShow: ({ model }) => {
141   - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.EXTERNAL_URL;
  170 + return model[PackageField.IS_URL];
142 171 },
143 172 rules: [{ required: true, message: '外部URL为必填项' }],
144 173 componentProps: {
... ... @@ -146,10 +175,13 @@ export const formSchema: FormSchema[] = [
146 175 },
147 176 },
148 177 {
149   - field: PackageField.CHECK_SUM_WAY,
  178 + field: PackageField.VALIDATE_WAY,
150 179 label: '校验和方式',
151 180 component: 'RadioGroup',
152 181 defaultValue: CheckSumWay.AUTO,
  182 + ifShow: ({ model }) => {
  183 + return !model[PackageField.IS_URL];
  184 + },
153 185 componentProps: () => {
154 186 return {
155 187 options: [
... ... @@ -160,12 +192,13 @@ export const formSchema: FormSchema[] = [
160 192 },
161 193 },
162 194 {
163   - field: PackageField.ALG,
  195 + field: PackageField.CHECK_SUM_ALG,
164 196 label: '校验和算法',
165 197 component: 'Select',
166 198 ifShow: ({ model }) => {
167   - return model[PackageField.CHECK_SUM_WAY] === CheckSumWay.MANUAL;
  199 + return model[PackageField.VALIDATE_WAY] === CheckSumWay.MANUAL && !model[PackageField.IS_URL];
168 200 },
  201 + defaultValue: ALG.SHA_256,
169 202 componentProps: {
170 203 placeholder: '请选择校验和算法',
171 204 options: Object.keys(ALG).map((key) => {
... ... @@ -181,7 +214,7 @@ export const formSchema: FormSchema[] = [
181 214 label: '校验和',
182 215 component: 'Input',
183 216 ifShow: ({ model }) => {
184   - return model[PackageField.CHECK_SUM_WAY] === CheckSumWay.MANUAL;
  217 + return model[PackageField.VALIDATE_WAY] === CheckSumWay.MANUAL && !model[PackageField.IS_URL];
185 218 },
186 219 helpMessage: ['如果校验和为空,会自动生成'],
187 220 componentProps: {
... ...
  1 +import { ALG, PackageField, PackageType } from './packageDetail.config';
  2 +import { FormSchema } from '/@/components/Form';
  3 +
  4 +export const formSchema: FormSchema[] = [
  5 + {
  6 + field: PackageField.TITLE,
  7 + label: '标题',
  8 + component: 'Input',
  9 + colProps: { span: 12 },
  10 + },
  11 + {
  12 + field: PackageField.VERSION,
  13 + label: '版本',
  14 + component: 'Input',
  15 + colProps: { span: 12 },
  16 + },
  17 + {
  18 + field: PackageField.VERSION_TAG,
  19 + label: '版本标签',
  20 + component: 'Input',
  21 + },
  22 + {
  23 + field: PackageField.DEVICE_PROFILE_INFO,
  24 + label: '设备配置',
  25 + component: 'Input',
  26 + },
  27 + {
  28 + field: PackageField.PACKAGE_TYPE,
  29 + label: '包类型',
  30 + component: 'Select',
  31 + rules: [{ required: true, message: '包类型为必填项' }],
  32 + componentProps: () => {
  33 + return {
  34 + options: [
  35 + { label: '固件', value: PackageType.FIRMWARE },
  36 + { label: '软件', value: PackageType.SOFTWARE },
  37 + ],
  38 + placeholder: '请选择设备配置',
  39 + };
  40 + },
  41 + },
  42 + {
  43 + field: PackageField.URL,
  44 + label: '直接URL',
  45 + component: 'Input',
  46 + ifShow: ({ model }) => {
  47 + return model[PackageField.URL];
  48 + },
  49 + },
  50 + {
  51 + field: PackageField.CHECK_SUM_ALG,
  52 + label: '校验和算法',
  53 + component: 'Select',
  54 + colProps: { span: 12 },
  55 + ifShow: ({ model }) => {
  56 + return !model[PackageField.URL];
  57 + },
  58 + componentProps: {
  59 + placeholder: '请选择校验和算法',
  60 + options: Object.keys(ALG).map((key) => {
  61 + return {
  62 + label: String(ALG[key]).toUpperCase(),
  63 + value: ALG[key],
  64 + };
  65 + }),
  66 + },
  67 + },
  68 + {
  69 + field: PackageField.CHECK_SUM,
  70 + label: '校验和',
  71 + component: 'Input',
  72 + ifShow: ({ model }) => {
  73 + return !model[PackageField.URL];
  74 + },
  75 + colProps: { span: 12 },
  76 + },
  77 + {
  78 + field: PackageField.FILE_NAME,
  79 + label: '文件名',
  80 + component: 'Input',
  81 + ifShow: ({ model }) => {
  82 + return !model[PackageField.URL];
  83 + },
  84 + colProps: { span: 8 },
  85 + },
  86 + {
  87 + field: PackageField.FILE_SIZE,
  88 + label: '文件大小(以字节为单位)',
  89 + component: 'Input',
  90 + ifShow: ({ model }) => {
  91 + return !model[PackageField.URL];
  92 + },
  93 + colProps: { span: 8 },
  94 + },
  95 + {
  96 + field: PackageField.FILE_NAME,
  97 + label: '文件名',
  98 + component: 'Input',
  99 + ifShow: ({ model }) => {
  100 + return !model[PackageField.URL];
  101 + },
  102 + colProps: { span: 8 },
  103 + },
  104 + {
  105 + field: PackageField.FILE_TYPE,
  106 + label: '内容类型',
  107 + ifShow: ({ model }) => {
  108 + return !model[PackageField.URL];
  109 + },
  110 + component: 'Input',
  111 + },
  112 + {
  113 + field: PackageField.DESCRIPTION,
  114 + label: '描述',
  115 + component: 'Input',
  116 + dynamicDisabled: false,
  117 + },
  118 +];
... ...
  1 +import { downloadPackage } from '/@/api/ota';
  2 +import { OtaRecordDatum } from '/@/api/ota/model';
  3 +
  4 +export async function useDownload(record: OtaRecordDatum) {
  5 + try {
  6 + if (record.url || !record.fileName) return;
  7 + const data = await downloadPackage(record.id.id);
  8 + const aEl = document.createElement('a');
  9 + const blob = new Blob([data], { type: record.contentType });
  10 + const objectUrl = URL.createObjectURL(blob);
  11 + aEl.href = objectUrl;
  12 + aEl.download = record.fileName;
  13 + aEl.click();
  14 + URL.revokeObjectURL(objectUrl);
  15 + } catch (error) {}
  16 +}
... ...
1 1 <script lang="ts" setup>
2 2 import { Button } from 'ant-design-vue';
3   - import { columns, searchFormSchema } from './config/config';
  3 + import { columns, ModalPassRecord, searchFormSchema } from './config/config';
4 4 import { PageWrapper } from '/@/components/Page';
5   - import { BasicTable, useTable } from '/@/components/Table';
  5 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
6 6 import PackageDetailModal from './components/PackageDetailModal.vue';
7 7 import { useModal } from '/@/components/Modal';
  8 + import { deleteOtaPackage, getOtaPackagesList } from '/@/api/ota';
  9 + import { GetOtaPackagesParams, OtaRecordDatum } from '/@/api/ota/model/index';
  10 + import PackagesDetailDrawer from './components/PackagesDetailDrawer.vue';
  11 + import { useDrawer } from '/@/components/Drawer';
  12 + import { useMessage } from '/@/hooks/web/useMessage';
  13 + import { useDownload } from './hook/useDownload';
8 14
9   - const [register] = useTable({
  15 + const [register, { reload }] = useTable({
10 16 columns,
11 17 title: '包仓库',
  18 + api: async (params: GetOtaPackagesParams) => {
  19 + const data = await getOtaPackagesList({
  20 + ...params,
  21 + page: params.page - 1,
  22 + textSearch: params.title,
  23 + });
  24 + return { ...data, page: params.page };
  25 + },
  26 + pagination: {
  27 + showSizeChanger: true,
  28 + pageSizeOptions: ['1', '2', '3', '4'],
  29 + },
  30 + fetchSetting: {
  31 + totalField: 'totalElements',
  32 + listField: 'data',
  33 + },
12 34 formConfig: {
13 35 labelWidth: 120,
14 36 schemas: searchFormSchema,
15 37 },
  38 + showIndexColumn: false,
16 39 useSearchForm: true,
17 40 showTableSetting: true,
18 41 });
19 42
  43 + const { createConfirm, createMessage } = useMessage();
  44 +
20 45 const [registerModal, { openModal }] = useModal();
21 46
  47 + const [registerDrawer, { openDrawer }] = useDrawer();
  48 +
22 49 const handleCreatePackage = () => {
23   - openModal(true);
  50 + openModal(true, { isUpdate: false } as ModalPassRecord);
  51 + };
  52 +
  53 + const handleOpenDetailDrawer = (record: OtaRecordDatum) => {
  54 + openDrawer(true, record.id.id);
  55 + };
  56 +
  57 + const downloadFile = async (record: OtaRecordDatum) => {
  58 + await useDownload(record);
  59 + };
  60 +
  61 + const deletePackage = (record: OtaRecordDatum) => {
  62 + createConfirm({
  63 + iconType: 'warning',
  64 + content: '是否确认删除操作?',
  65 + onOk: async () => {
  66 + try {
  67 + await deleteOtaPackage(record.id.id);
  68 + createMessage.success('删除成功');
  69 + reload();
  70 + } catch (error) {}
  71 + },
  72 + });
24 73 };
25 74 </script>
26 75
27 76 <template>
28 77 <PageWrapper dense contentFullHeight contentClass="flex flex-col">
29   - <BasicTable @register="register">
  78 + <BasicTable @register="register" @row-click="handleOpenDetailDrawer" class="ota-list">
30 79 <template #toolbar>
31 80 <Button @click="handleCreatePackage" type="primary">新增包</Button>
32 81 </template>
  82 + <template #action="{ record }">
  83 + <TableAction
  84 + @click.stop
  85 + :actions="[
  86 + {
  87 + label: '下载',
  88 + icon: 'ant-design:download-outlined',
  89 + onClick: downloadFile.bind(null, record),
  90 + },
  91 + {
  92 + label: '删除',
  93 + icon: 'ant-design:delete-outlined',
  94 + color: 'error',
  95 + popConfirm: {
  96 + title: '是否确认删除',
  97 + confirm: deletePackage.bind(null, record),
  98 + },
  99 + },
  100 + ]"
  101 + />
  102 + </template>
33 103 </BasicTable>
34   - <PackageDetailModal @register="registerModal" />
  104 + <PackageDetailModal @register="registerModal" @update:list="reload" />
  105 + <PackagesDetailDrawer @register="registerDrawer" @update:list="reload" />
35 106 </PageWrapper>
36 107 </template>
  108 +
  109 +<style scoped lang="less">
  110 + .ota-list:deep(.ant-table-tbody > tr > td:last-of-type) {
  111 + width: 100%;
  112 + height: 100%;
  113 + padding: 0 !important;
  114 + }
  115 +</style>
... ...