Commit c6c9460dfba2afc0a24b3cbe0b22e4cc1e01d7fa
1 parent
faad3adc
feat: basic implement operation/ota page
Showing
12 changed files
with
917 additions
and
60 deletions
src/api/ota/index.ts
0 → 100644
| 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 | +}; | ... | ... |
src/api/ota/model/index.ts
0 → 100644
| 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> | ... | ... |
| 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 | +]; | ... | ... |
src/views/operation/ota/hook/useDownload.ts
0 → 100644
| 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> | ... | ... |