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,6 +34,7 @@ import { JEasyCron } from './externalCompns/components/JEasyCron'; | ||
34 | import ColorPicker from './components/ColorPicker.vue'; | 34 | import ColorPicker from './components/ColorPicker.vue'; |
35 | import IconDrawer from './components/IconDrawer.vue'; | 35 | import IconDrawer from './components/IconDrawer.vue'; |
36 | import ApiUpload from './components/ApiUpload.vue'; | 36 | import ApiUpload from './components/ApiUpload.vue'; |
37 | +import ApiSearchSelect from './components/ApiSearchSelect.vue'; | ||
37 | 38 | ||
38 | const componentMap = new Map<ComponentType, Component>(); | 39 | const componentMap = new Map<ComponentType, Component>(); |
39 | 40 | ||
@@ -75,6 +76,7 @@ componentMap.set('JEasyCron', JEasyCron); | @@ -75,6 +76,7 @@ componentMap.set('JEasyCron', JEasyCron); | ||
75 | componentMap.set('ColorPicker', ColorPicker); | 76 | componentMap.set('ColorPicker', ColorPicker); |
76 | componentMap.set('IconDrawer', IconDrawer); | 77 | componentMap.set('IconDrawer', IconDrawer); |
77 | componentMap.set('ApiUpload', ApiUpload); | 78 | componentMap.set('ApiUpload', ApiUpload); |
79 | +componentMap.set('ApiSearchSelect', ApiSearchSelect); | ||
78 | 80 | ||
79 | export function add(compName: ComponentType, component: Component) { | 81 | export function add(compName: ComponentType, component: Component) { |
80 | componentMap.set(compName, component); | 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 | <script lang="ts" setup> | 1 | <script lang="ts" setup> |
2 | import { BasicModal, useModalInner } from '/@/components/Modal'; | 2 | import { BasicModal, useModalInner } from '/@/components/Modal'; |
3 | import { BasicForm, useForm } from '/@/components/Form'; | 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 | schemas: formSchema, | 27 | schemas: formSchema, |
11 | showActionButtonGroup: false, | 28 | showActionButtonGroup: false, |
12 | // labelCol: { span: 8 }, | 29 | // labelCol: { span: 8 }, |
13 | labelWidth: 100, | 30 | labelWidth: 100, |
14 | wrapperCol: { span: 16 }, | 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 | </script> | 81 | </script> |
17 | 82 | ||
18 | <template> | 83 | <template> |
19 | - <BasicModal title="包管理" @register="registerModal"> | 84 | + <BasicModal title="包管理" destroy-on-close @register="registerModal" @ok="handleSubmit"> |
20 | <BasicForm @register="registerForm" /> | 85 | <BasicForm @register="registerForm" /> |
21 | </BasicModal> | 86 | </BasicModal> |
22 | </template> | 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 | import { BasicColumn, FormSchema } from '/@/components/Table'; | 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 | export const columns: BasicColumn[] = [ | 11 | export const columns: BasicColumn[] = [ |
4 | { | 12 | { |
5 | title: '创建时间', | 13 | title: '创建时间', |
6 | dataIndex: PackageField.CREATE_TIME, | 14 | dataIndex: PackageField.CREATE_TIME, |
15 | + format(text) { | ||
16 | + return dateUtil(text).format(DEFAULT_DATE_FORMAT); | ||
17 | + }, | ||
7 | width: 120, | 18 | width: 120, |
8 | }, | 19 | }, |
9 | { | 20 | { |
@@ -18,29 +29,54 @@ export const columns: BasicColumn[] = [ | @@ -18,29 +29,54 @@ export const columns: BasicColumn[] = [ | ||
18 | }, | 29 | }, |
19 | { | 30 | { |
20 | title: '版本标签', | 31 | title: '版本标签', |
21 | - dataIndex: PackageField.VERSION_LABEL, | 32 | + dataIndex: PackageField.VERSION_TAG, |
22 | width: 120, | 33 | width: 120, |
23 | }, | 34 | }, |
24 | { | 35 | { |
25 | title: '包类型', | 36 | title: '包类型', |
26 | dataIndex: PackageField.PACKAGE_TYPE, | 37 | dataIndex: PackageField.PACKAGE_TYPE, |
38 | + format: (text) => { | ||
39 | + return text === PackageType.FIRMWARE ? '固件' : '软件'; | ||
40 | + }, | ||
27 | width: 120, | 41 | width: 120, |
28 | }, | 42 | }, |
29 | { | 43 | { |
30 | title: '直接URL', | 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 | width: 120, | 51 | width: 120, |
33 | }, | 52 | }, |
34 | { | 53 | { |
35 | title: '文件大小', | 54 | title: '文件大小', |
36 | dataIndex: PackageField.FILE_SIZE, | 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 | width: 120, | 61 | width: 120, |
38 | }, | 62 | }, |
39 | { | 63 | { |
40 | title: '校验和', | 64 | title: '校验和', |
41 | dataIndex: PackageField.CHECK_SUM, | 65 | dataIndex: PackageField.CHECK_SUM, |
66 | + format(text, record) { | ||
67 | + return text ? `${record[PackageField.CHECK_SUM_ALG]}: ${text.slice(0, 11)}` : ''; | ||
68 | + }, | ||
42 | width: 120, | 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 | export const searchFormSchema: FormSchema[] = [ | 82 | export const searchFormSchema: FormSchema[] = [ |
1 | +import { getDevicePRofileInfo, getDeviceProfileInfos } from '/@/api/ota'; | ||
2 | +import { Id } from '/@/api/ota/model'; | ||
1 | import { FormSchema } from '/@/components/Form'; | 3 | import { FormSchema } from '/@/components/Form'; |
4 | +import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue'; | ||
2 | 5 | ||
3 | export enum PackageField { | 6 | export enum PackageField { |
4 | TITLE = 'title', | 7 | TITLE = 'title', |
5 | VERSION = 'version', | 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 | DESCRIPTION = 'description', | 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 | export enum PackageType { | 28 | export enum PackageType { |
27 | - FIRMWARE = 'firmware', | ||
28 | - SOFTWARE = 'software', | 29 | + FIRMWARE = 'FIRMWARE', |
30 | + SOFTWARE = 'SOFTWARE', | ||
29 | } | 31 | } |
30 | 32 | ||
31 | export enum CheckSumWay { | 33 | export enum CheckSumWay { |
@@ -34,13 +36,13 @@ export enum CheckSumWay { | @@ -34,13 +36,13 @@ export enum CheckSumWay { | ||
34 | } | 36 | } |
35 | 37 | ||
36 | export enum ALG { | 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 | export const formSchema: FormSchema[] = [ | 48 | export const formSchema: FormSchema[] = [ |
@@ -63,25 +65,51 @@ 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 | label: '版本标签', | 69 | label: '版本标签', |
68 | component: 'Input', | 70 | component: 'Input', |
69 | helpMessage: ['自定义标签应与您设备报告的软件包版本相匹配'], | 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 | label: '设备配置', | 86 | label: '设备配置', |
77 | - component: 'Select', | 87 | + component: 'ApiSearchSelect', |
78 | helpMessage: ['上传的包仅适用于具有所选配置文件的设备'], | 88 | helpMessage: ['上传的包仅适用于具有所选配置文件的设备'], |
79 | defaultValue: 'default', | 89 | defaultValue: 'default', |
80 | rules: [{ required: true, message: '设备配置为必填项' }], | 90 | rules: [{ required: true, message: '设备配置为必填项' }], |
81 | - componentProps: () => { | 91 | + componentProps: ({ formActionType }) => { |
92 | + const { setFieldsValue } = formActionType; | ||
82 | return { | 93 | return { |
83 | - options: [{ label: 'default', value: 'default' }], | ||
84 | placeholder: '请选择设备配置', | 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,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 | label: '上传方式', | 135 | label: '上传方式', |
108 | component: 'RadioGroup', | 136 | component: 'RadioGroup', |
109 | - defaultValue: PackageUpdateType.BINARY_FILE, | 137 | + defaultValue: false, |
110 | componentProps: () => { | 138 | componentProps: () => { |
111 | return { | 139 | return { |
140 | + defaultValue: false, | ||
112 | options: [ | 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,25 +149,25 @@ export const formSchema: FormSchema[] = [ | ||
120 | field: PackageField.PACKAGE_BINARY_FILE, | 149 | field: PackageField.PACKAGE_BINARY_FILE, |
121 | label: '二进制文件', | 150 | label: '二进制文件', |
122 | ifShow: ({ model }) => { | 151 | ifShow: ({ model }) => { |
123 | - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.BINARY_FILE; | 152 | + return !model[PackageField.IS_URL]; |
124 | }, | 153 | }, |
125 | component: 'ApiUpload', | 154 | component: 'ApiUpload', |
126 | valueField: PackageField.PACKAGE_BINARY_FILE, | 155 | valueField: PackageField.PACKAGE_BINARY_FILE, |
127 | changeEvent: `update:${PackageField.PACKAGE_BINARY_FILE}`, | 156 | changeEvent: `update:${PackageField.PACKAGE_BINARY_FILE}`, |
157 | + rules: [{ required: true, message: '请上传二进制文件', type: 'array' }], | ||
128 | componentProps: { | 158 | componentProps: { |
129 | maxFileLimit: 1, | 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 | label: '外部URL', | 167 | label: '外部URL', |
139 | component: 'Input', | 168 | component: 'Input', |
140 | ifShow: ({ model }) => { | 169 | ifShow: ({ model }) => { |
141 | - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.EXTERNAL_URL; | 170 | + return model[PackageField.IS_URL]; |
142 | }, | 171 | }, |
143 | rules: [{ required: true, message: '外部URL为必填项' }], | 172 | rules: [{ required: true, message: '外部URL为必填项' }], |
144 | componentProps: { | 173 | componentProps: { |
@@ -146,10 +175,13 @@ export const formSchema: FormSchema[] = [ | @@ -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 | label: '校验和方式', | 179 | label: '校验和方式', |
151 | component: 'RadioGroup', | 180 | component: 'RadioGroup', |
152 | defaultValue: CheckSumWay.AUTO, | 181 | defaultValue: CheckSumWay.AUTO, |
182 | + ifShow: ({ model }) => { | ||
183 | + return !model[PackageField.IS_URL]; | ||
184 | + }, | ||
153 | componentProps: () => { | 185 | componentProps: () => { |
154 | return { | 186 | return { |
155 | options: [ | 187 | options: [ |
@@ -160,12 +192,13 @@ export const formSchema: FormSchema[] = [ | @@ -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 | label: '校验和算法', | 196 | label: '校验和算法', |
165 | component: 'Select', | 197 | component: 'Select', |
166 | ifShow: ({ model }) => { | 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 | componentProps: { | 202 | componentProps: { |
170 | placeholder: '请选择校验和算法', | 203 | placeholder: '请选择校验和算法', |
171 | options: Object.keys(ALG).map((key) => { | 204 | options: Object.keys(ALG).map((key) => { |
@@ -181,7 +214,7 @@ export const formSchema: FormSchema[] = [ | @@ -181,7 +214,7 @@ export const formSchema: FormSchema[] = [ | ||
181 | label: '校验和', | 214 | label: '校验和', |
182 | component: 'Input', | 215 | component: 'Input', |
183 | ifShow: ({ model }) => { | 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 | helpMessage: ['如果校验和为空,会自动生成'], | 219 | helpMessage: ['如果校验和为空,会自动生成'], |
187 | componentProps: { | 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 | <script lang="ts" setup> | 1 | <script lang="ts" setup> |
2 | import { Button } from 'ant-design-vue'; | 2 | import { Button } from 'ant-design-vue'; |
3 | - import { columns, searchFormSchema } from './config/config'; | 3 | + import { columns, ModalPassRecord, searchFormSchema } from './config/config'; |
4 | import { PageWrapper } from '/@/components/Page'; | 4 | import { PageWrapper } from '/@/components/Page'; |
5 | - import { BasicTable, useTable } from '/@/components/Table'; | 5 | + import { BasicTable, useTable, TableAction } from '/@/components/Table'; |
6 | import PackageDetailModal from './components/PackageDetailModal.vue'; | 6 | import PackageDetailModal from './components/PackageDetailModal.vue'; |
7 | import { useModal } from '/@/components/Modal'; | 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 | columns, | 16 | columns, |
11 | title: '包仓库', | 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 | formConfig: { | 34 | formConfig: { |
13 | labelWidth: 120, | 35 | labelWidth: 120, |
14 | schemas: searchFormSchema, | 36 | schemas: searchFormSchema, |
15 | }, | 37 | }, |
38 | + showIndexColumn: false, | ||
16 | useSearchForm: true, | 39 | useSearchForm: true, |
17 | showTableSetting: true, | 40 | showTableSetting: true, |
18 | }); | 41 | }); |
19 | 42 | ||
43 | + const { createConfirm, createMessage } = useMessage(); | ||
44 | + | ||
20 | const [registerModal, { openModal }] = useModal(); | 45 | const [registerModal, { openModal }] = useModal(); |
21 | 46 | ||
47 | + const [registerDrawer, { openDrawer }] = useDrawer(); | ||
48 | + | ||
22 | const handleCreatePackage = () => { | 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 | </script> | 74 | </script> |
26 | 75 | ||
27 | <template> | 76 | <template> |
28 | <PageWrapper dense contentFullHeight contentClass="flex flex-col"> | 77 | <PageWrapper dense contentFullHeight contentClass="flex flex-col"> |
29 | - <BasicTable @register="register"> | 78 | + <BasicTable @register="register" @row-click="handleOpenDetailDrawer" class="ota-list"> |
30 | <template #toolbar> | 79 | <template #toolbar> |
31 | <Button @click="handleCreatePackage" type="primary">新增包</Button> | 80 | <Button @click="handleCreatePackage" type="primary">新增包</Button> |
32 | </template> | 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 | </BasicTable> | 103 | </BasicTable> |
34 | - <PackageDetailModal @register="registerModal" /> | 104 | + <PackageDetailModal @register="registerModal" @update:list="reload" /> |
105 | + <PackagesDetailDrawer @register="registerDrawer" @update:list="reload" /> | ||
35 | </PageWrapper> | 106 | </PageWrapper> |
36 | </template> | 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> |