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