Commit f518f914fe26467a22641a2d2f6eb23428b7370c
Merge branch 'ww' into 'main'
feat: implement ota update page See merge request huang/yun-teng-iot-front!367
Showing
31 changed files
with
1672 additions
and
158 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 createOtaPackage = (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 getDefaultDeviceProfile = () => { | ||
| 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"> | ||
| 2 | + export type OptionsItem = { label: string; value: string; disabled?: boolean }; | ||
| 3 | + export interface OnChangeHookParams { | ||
| 4 | + options: Ref<OptionsItem[]>; | ||
| 5 | + } | ||
| 6 | +</script> | ||
| 7 | + | ||
| 8 | +<script lang="ts" setup> | ||
| 9 | + import { ref, watchEffect, computed, unref, watch, Ref } from 'vue'; | ||
| 10 | + import { Select } from 'ant-design-vue'; | ||
| 11 | + import { isFunction } from '/@/utils/is'; | ||
| 12 | + import { useRuleFormItem } from '/@/hooks/component/useFormItem'; | ||
| 13 | + import { useAttrs } from '/@/hooks/core/useAttrs'; | ||
| 14 | + import { get, omit } from 'lodash-es'; | ||
| 15 | + import { LoadingOutlined } from '@ant-design/icons-vue'; | ||
| 16 | + import { useI18n } from '/@/hooks/web/useI18n'; | ||
| 17 | + | ||
| 18 | + const emit = defineEmits(['options-change', 'change']); | ||
| 19 | + const props = withDefaults( | ||
| 20 | + defineProps<{ | ||
| 21 | + value?: Recordable | number | string; | ||
| 22 | + numberToString?: boolean; | ||
| 23 | + api?: (arg?: Recordable) => Promise<OptionsItem[]>; | ||
| 24 | + searchApi?: (arg?: Recordable) => Promise<OptionsItem[]>; | ||
| 25 | + params?: Recordable; | ||
| 26 | + resultField?: string; | ||
| 27 | + labelField?: string; | ||
| 28 | + valueField?: string; | ||
| 29 | + immediate?: boolean; | ||
| 30 | + queryEmptyDataAgin?: boolean; | ||
| 31 | + onChangeHook?: ({ options }: OnChangeHookParams) => void; | ||
| 32 | + dropdownVisibleChangeHook?: ({ options }: OnChangeHookParams) => void; | ||
| 33 | + }>(), | ||
| 34 | + { | ||
| 35 | + resultField: '', | ||
| 36 | + labelField: 'label', | ||
| 37 | + valueField: 'value', | ||
| 38 | + immediate: true, | ||
| 39 | + queryEmptyDataAgin: true, | ||
| 40 | + } | ||
| 41 | + ); | ||
| 42 | + const options = ref<OptionsItem[]>([]); | ||
| 43 | + const loading = ref(false); | ||
| 44 | + const isFirstLoad = ref(true); | ||
| 45 | + const emitData = ref<any[]>([]); | ||
| 46 | + const attrs = useAttrs(); | ||
| 47 | + const { t } = useI18n(); | ||
| 48 | + | ||
| 49 | + // Embedded in the form, just use the hook binding to perform form verification | ||
| 50 | + const [state] = useRuleFormItem(props, 'value', 'change', emitData); | ||
| 51 | + | ||
| 52 | + const getOptions = computed(() => { | ||
| 53 | + const { labelField, valueField = 'value', numberToString } = props; | ||
| 54 | + return unref(options).reduce((prev, next: Recordable) => { | ||
| 55 | + if (next) { | ||
| 56 | + const value = next[valueField]; | ||
| 57 | + prev.push({ | ||
| 58 | + label: next[labelField], | ||
| 59 | + value: numberToString ? `${value}` : value, | ||
| 60 | + ...omit(next, [labelField, valueField]), | ||
| 61 | + }); | ||
| 62 | + } | ||
| 63 | + return prev; | ||
| 64 | + }, [] as OptionsItem[]); | ||
| 65 | + }); | ||
| 66 | + | ||
| 67 | + watchEffect(() => { | ||
| 68 | + props.immediate && fetch(); | ||
| 69 | + }); | ||
| 70 | + | ||
| 71 | + watch( | ||
| 72 | + () => props.params, | ||
| 73 | + () => { | ||
| 74 | + !unref(isFirstLoad) && fetch(); | ||
| 75 | + }, | ||
| 76 | + { deep: true } | ||
| 77 | + ); | ||
| 78 | + | ||
| 79 | + async function fetch() { | ||
| 80 | + const api = props.api; | ||
| 81 | + if (!api || !isFunction(api)) return; | ||
| 82 | + options.value = []; | ||
| 83 | + try { | ||
| 84 | + loading.value = true; | ||
| 85 | + const res = await api(props.params); | ||
| 86 | + if (Array.isArray(res)) { | ||
| 87 | + options.value = res; | ||
| 88 | + emitChange(); | ||
| 89 | + return; | ||
| 90 | + } | ||
| 91 | + if (props.resultField) { | ||
| 92 | + options.value = get(res, props.resultField) || []; | ||
| 93 | + } | ||
| 94 | + emitChange(); | ||
| 95 | + } catch (error) { | ||
| 96 | + console.warn(error); | ||
| 97 | + } finally { | ||
| 98 | + loading.value = false; | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + async function handleFetch() { | ||
| 103 | + const { immediate, dropdownVisibleChangeHook } = props; | ||
| 104 | + if (!immediate && unref(isFirstLoad)) { | ||
| 105 | + await fetch(); | ||
| 106 | + isFirstLoad.value = false; | ||
| 107 | + } | ||
| 108 | + if (dropdownVisibleChangeHook && isFunction(dropdownVisibleChangeHook)) { | ||
| 109 | + dropdownVisibleChangeHook({ options }); | ||
| 110 | + } | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + function emitChange() { | ||
| 114 | + emit('options-change', unref(getOptions)); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + function handleChange(value: string, ...args) { | ||
| 118 | + emitData.value = args; | ||
| 119 | + if (!value && props.queryEmptyDataAgin) handleSearch(); | ||
| 120 | + const { onChangeHook } = props; | ||
| 121 | + if (!onChangeHook && !isFunction(onChangeHook)) return; | ||
| 122 | + onChangeHook({ options }); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + async function handleSearch(params?: string) { | ||
| 126 | + let { searchApi, api } = props; | ||
| 127 | + if (!searchApi || !isFunction(searchApi)) { | ||
| 128 | + if (!api || !isFunction(api)) return; | ||
| 129 | + searchApi = api; | ||
| 130 | + } | ||
| 131 | + options.value = []; | ||
| 132 | + try { | ||
| 133 | + loading.value = true; | ||
| 134 | + const res = await searchApi({ ...props.params, text: params }); | ||
| 135 | + if (Array.isArray(res)) { | ||
| 136 | + options.value = res; | ||
| 137 | + emitChange(); | ||
| 138 | + return; | ||
| 139 | + } | ||
| 140 | + if (props.resultField) { | ||
| 141 | + options.value = get(res, props.resultField) || []; | ||
| 142 | + } | ||
| 143 | + emitChange(); | ||
| 144 | + } catch (error) { | ||
| 145 | + console.warn(error); | ||
| 146 | + } finally { | ||
| 147 | + loading.value = false; | ||
| 148 | + } | ||
| 149 | + } | ||
| 150 | +</script> | ||
| 151 | + | ||
| 152 | +<template> | ||
| 153 | + <Select | ||
| 154 | + @dropdownVisibleChange="handleFetch" | ||
| 155 | + v-bind="attrs" | ||
| 156 | + show-search | ||
| 157 | + @change="handleChange" | ||
| 158 | + :options="getOptions" | ||
| 159 | + @search="handleSearch" | ||
| 160 | + v-model:value="state" | ||
| 161 | + > | ||
| 162 | + <template #[item]="data" v-for="item in Object.keys($slots)"> | ||
| 163 | + <slot :name="item" v-bind="data || {}"></slot> | ||
| 164 | + </template> | ||
| 165 | + <template #suffixIcon v-if="loading"> | ||
| 166 | + <LoadingOutlined spin /> | ||
| 167 | + </template> | ||
| 168 | + <template #notFoundContent v-if="loading"> | ||
| 169 | + <span> | ||
| 170 | + <LoadingOutlined spin class="mr-1" /> | ||
| 171 | + {{ t('component.form.apiSelectNotFound') }} | ||
| 172 | + </span> | ||
| 173 | + </template> | ||
| 174 | + </Select> | ||
| 175 | +</template> | 
src/hooks/component/useSyncConfirm.ts
0 → 100644
| 1 | +import { ModalOptionsEx, useMessage } from '../web/useMessage'; | ||
| 2 | + | ||
| 3 | +export function useSyncConfirm() { | ||
| 4 | + const { createConfirm } = useMessage(); | ||
| 5 | + | ||
| 6 | + const createSyncConfirm = (options: ModalOptionsEx): Promise<boolean> => { | ||
| 7 | + return new Promise((resolve, reject) => { | ||
| 8 | + createConfirm({ | ||
| 9 | + ...options, | ||
| 10 | + onOk: () => { | ||
| 11 | + resolve(true); | ||
| 12 | + }, | ||
| 13 | + onCancel: () => { | ||
| 14 | + reject(false); | ||
| 15 | + }, | ||
| 16 | + }); | ||
| 17 | + }); | ||
| 18 | + }; | ||
| 19 | + | ||
| 20 | + return { createSyncConfirm }; | ||
| 21 | +} | 
| @@ -37,11 +37,22 @@ export const copyTransTreeFun = (arr: any[]) => { | @@ -37,11 +37,22 @@ export const copyTransTreeFun = (arr: any[]) => { | ||
| 37 | }; | 37 | }; | 
| 38 | 38 | ||
| 39 | // 百度地图url | 39 | // 百度地图url | 
| 40 | +const ak = '7uOPPyAHn2Y2ZryeQqHtcRqtIY374vKa'; | ||
| 41 | + | ||
| 40 | const register_BAI_DU_MAP_URL = (ak: string) => { | 42 | const register_BAI_DU_MAP_URL = (ak: string) => { | 
| 41 | return `https://api.map.baidu.com/getscript?v=3.0&ak=${ak}`; | 43 | return `https://api.map.baidu.com/getscript?v=3.0&ak=${ak}`; | 
| 42 | }; | 44 | }; | 
| 43 | 45 | ||
| 44 | -export const BAI_DU_MAP_URL = register_BAI_DU_MAP_URL('7uOPPyAHn2Y2ZryeQqHtcRqtIY374vKa'); | 46 | +const registerBaiDuMapGlLib = (ak: string) => { | 
| 47 | + return `//api.map.baidu.com/getscript?type=webgl&v=1.0&ak=${ak}&services=&t=${Date.now()}`; | ||
| 48 | +}; | ||
| 49 | + | ||
| 50 | +export const BAI_DU_MAP_TRACK_ANIMATION = | ||
| 51 | + '//mapopen.bj.bcebos.com/github/BMapGLLib/TrackAnimation/src/TrackAnimation.min.js'; | ||
| 52 | + | ||
| 53 | +export const BAI_DU_MAP_GL_LIB = registerBaiDuMapGlLib(ak); | ||
| 54 | + | ||
| 55 | +export const BAI_DU_MAP_URL = register_BAI_DU_MAP_URL(ak); | ||
| 45 | 56 | ||
| 46 | // 数字加上每三位加上逗号 | 57 | // 数字加上每三位加上逗号 | 
| 47 | export function toThousands(num) { | 58 | export function toThousands(num) { | 
| @@ -2,7 +2,7 @@ | @@ -2,7 +2,7 @@ | ||
| 2 | import moment from 'moment'; | 2 | import moment from 'moment'; | 
| 3 | import { nextTick, onMounted, onUnmounted, Ref, ref, unref } from 'vue'; | 3 | import { nextTick, onMounted, onUnmounted, Ref, ref, unref } from 'vue'; | 
| 4 | import { getDeviceDataKeys, getDeviceHistoryInfo } from '/@/api/alarm/position'; | 4 | import { getDeviceDataKeys, getDeviceHistoryInfo } from '/@/api/alarm/position'; | 
| 5 | - import { Empty } from 'ant-design-vue'; | 5 | + import { Empty, Spin } from 'ant-design-vue'; | 
| 6 | import { useECharts } from '/@/hooks/web/useECharts'; | 6 | import { useECharts } from '/@/hooks/web/useECharts'; | 
| 7 | import { dateUtil } from '/@/utils/dateUtil'; | 7 | import { dateUtil } from '/@/utils/dateUtil'; | 
| 8 | import { | 8 | import { | 
| @@ -175,7 +175,7 @@ | @@ -175,7 +175,7 @@ | ||
| 175 | </section> | 175 | </section> | 
| 176 | <section class="bg-white p-3"> | 176 | <section class="bg-white p-3"> | 
| 177 | <div v-show="isNull" ref="chartRef" :style="{ height: '550px', width: '100%' }"> | 177 | <div v-show="isNull" ref="chartRef" :style="{ height: '550px', width: '100%' }"> | 
| 178 | - <Loading :loading="loading" :absolute="true" /> | 178 | + <Spin :spinning="loading" :absolute="true" /> | 
| 179 | </div> | 179 | </div> | 
| 180 | <Empty v-show="!isNull" /> | 180 | <Empty v-show="!isNull" /> | 
| 181 | </section> | 181 | </section> | 
| 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 { createOtaPackage, 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 createOtaPackage(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"> | ||
| 20 | - <BasicForm @register="registerForm" /> | 84 | + <BasicModal | 
| 85 | + title="包管理" | ||
| 86 | + destroy-on-close | ||
| 87 | + :ok-button-props="{ loading }" | ||
| 88 | + @register="registerModal" | ||
| 89 | + @ok="handleSubmit" | ||
| 90 | + > | ||
| 91 | + <BasicForm @register="registerForm" class="package-manage-form" /> | ||
| 21 | </BasicModal> | 92 | </BasicModal> | 
| 22 | </template> | 93 | </template> | 
| 94 | + | ||
| 95 | +<style scoped lang="less"> | ||
| 96 | + .package-manage-form { | ||
| 97 | + :deep(.ant-form-item-control-input-content > div > div) { | ||
| 98 | + width: 100% !important; | ||
| 99 | + } | ||
| 100 | + } | ||
| 101 | +</style> | 
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { DeviceProfileRecord, 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 { | ||
| 11 | + createOtaPackage, | ||
| 12 | + deleteOtaPackage, | ||
| 13 | + getDeviceProfileInfoById, | ||
| 14 | + getOtaPackageInfo, | ||
| 15 | + } from '/@/api/ota'; | ||
| 16 | + import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard'; | ||
| 17 | + import { useDownload } from '../hook/useDownload'; | ||
| 18 | + import { Authority } from '/@/components/Authority'; | ||
| 19 | + import { OtaPermissionKey } from '../config/config'; | ||
| 20 | + // import DeviceDetailDrawer from '/@/views/device/list/cpns/modal/DeviceDetailDrawer.vue'; | ||
| 21 | + | ||
| 22 | + const emit = defineEmits(['register', 'update:list']); | ||
| 23 | + | ||
| 24 | + const loading = ref(false); | ||
| 25 | + | ||
| 26 | + const otaRecord = ref<OtaRecordDatum>({} as unknown as OtaRecordDatum); | ||
| 27 | + | ||
| 28 | + const deviceProfileInfo = ref<DeviceProfileRecord>({} as unknown as DeviceProfileRecord); | ||
| 29 | + | ||
| 30 | + const { createConfirm, createMessage } = useMessage(); | ||
| 31 | + | ||
| 32 | + const [registerForm, { setFieldsValue, getFieldsValue }] = useForm({ | ||
| 33 | + schemas: formSchema, | ||
| 34 | + showActionButtonGroup: false, | ||
| 35 | + disabled: true, | ||
| 36 | + }); | ||
| 37 | + | ||
| 38 | + const [register, { closeDrawer, changeLoading }] = useDrawerInner(async (id: string) => { | ||
| 39 | + try { | ||
| 40 | + const record = await getOtaPackageInfo(id); | ||
| 41 | + const deviceInfo = await getDeviceProfileInfoById(record.deviceProfileId.id); | ||
| 42 | + setFieldsValue({ | ||
| 43 | + ...record, | ||
| 44 | + [PackageField.DESCRIPTION]: record.additionalInfo.description, | ||
| 45 | + [PackageField.DEVICE_PROFILE_INFO]: deviceInfo.name, | ||
| 46 | + }); | ||
| 47 | + deviceProfileInfo.value = deviceInfo; | ||
| 48 | + otaRecord.value = record; | ||
| 49 | + } catch (error) {} | ||
| 50 | + }); | ||
| 51 | + | ||
| 52 | + // const [registerTBDrawer, TBDrawerMethod] = useDrawer(); | ||
| 53 | + | ||
| 54 | + // const openDetailPage = () => { | ||
| 55 | + // TBDrawerMethod.openDrawer({ | ||
| 56 | + // id: otaRecord.value.id.id, | ||
| 57 | + // tbDeviceId: otaRecord.value.deviceProfileId.id, | ||
| 58 | + // }); | ||
| 59 | + // }; | ||
| 60 | + | ||
| 61 | + const downloadPackage = async () => { | ||
| 62 | + await useDownload(unref(otaRecord)); | ||
| 63 | + }; | ||
| 64 | + | ||
| 65 | + const deletePackage = () => { | ||
| 66 | + createConfirm({ | ||
| 67 | + iconType: 'warning', | ||
| 68 | + content: '是否确认删除操作?', | ||
| 69 | + onOk: async () => { | ||
| 70 | + try { | ||
| 71 | + await deleteOtaPackage(otaRecord.value.id.id); | ||
| 72 | + closeDrawer(); | ||
| 73 | + emit('update:list'); | ||
| 74 | + createMessage.success('删除成功'); | ||
| 75 | + } catch (error) {} | ||
| 76 | + }, | ||
| 77 | + }); | ||
| 78 | + }; | ||
| 79 | + | ||
| 80 | + const { clipboardRef, isSuccessRef } = useCopyToClipboard(''); | ||
| 81 | + const copyPackageId = () => { | ||
| 82 | + clipboardRef.value = otaRecord.value.id.id; | ||
| 83 | + if (unref(isSuccessRef)) createMessage.success('复制成功'); | ||
| 84 | + }; | ||
| 85 | + | ||
| 86 | + const copyUrl = () => { | ||
| 87 | + if (!unref(otaRecord).url) { | ||
| 88 | + createMessage.warning('无直接URL'); | ||
| 89 | + return; | ||
| 90 | + } | ||
| 91 | + clipboardRef.value = otaRecord.value.url; | ||
| 92 | + if (unref(isSuccessRef)) createMessage.success('复制成功'); | ||
| 93 | + }; | ||
| 94 | + | ||
| 95 | + const setLoading = (status: boolean) => { | ||
| 96 | + changeLoading(status); | ||
| 97 | + loading.value = status; | ||
| 98 | + }; | ||
| 99 | + | ||
| 100 | + const handleSubmit = async () => { | ||
| 101 | + const value = getFieldsValue(); | ||
| 102 | + try { | ||
| 103 | + setLoading(true); | ||
| 104 | + await createOtaPackage({ | ||
| 105 | + ...unref(otaRecord), | ||
| 106 | + additionalInfo: { description: value[PackageField.DESCRIPTION] }, | ||
| 107 | + } as any); | ||
| 108 | + createMessage.success('修改成功'); | ||
| 109 | + } catch (error) { | ||
| 110 | + } finally { | ||
| 111 | + setLoading(false); | ||
| 112 | + closeDrawer(); | ||
| 113 | + } | ||
| 114 | + }; | ||
| 115 | +</script> | ||
| 116 | + | ||
| 117 | +<template> | ||
| 118 | + <BasicDrawer | ||
| 119 | + :title="otaRecord.title" | ||
| 120 | + width="40%" | ||
| 121 | + class="relative" | ||
| 122 | + @register="register" | ||
| 123 | + @ok="handleSubmit" | ||
| 124 | + > | ||
| 125 | + <Tabs> | ||
| 126 | + <Tabs.TabPane tab="详情" key="detail"> | ||
| 127 | + <Space> | ||
| 128 | + <!-- <Button type="primary" @click="openDetailPage">打开详情页</Button> --> | ||
| 129 | + <Button type="primary" @click="downloadPackage" :disabled="!!otaRecord.url"> | ||
| 130 | + 下载包 | ||
| 131 | + </Button> | ||
| 132 | + <Button type="primary" @click="deletePackage" danger>删除包</Button> | ||
| 133 | + </Space> | ||
| 134 | + <div class="mt-3"> | ||
| 135 | + <Space> | ||
| 136 | + <Button type="primary" @click="copyPackageId">复制包ID</Button> | ||
| 137 | + <Button type="primary" @click="copyUrl">复制直接URL</Button> | ||
| 138 | + </Space> | ||
| 139 | + </div> | ||
| 140 | + <BasicForm @register="registerForm" /> | ||
| 141 | + </Tabs.TabPane> | ||
| 142 | + </Tabs> | ||
| 143 | + <template #footer> | ||
| 144 | + <div | ||
| 145 | + class="absolute right-0 bottom-0 w-full border-t bg-light-50 border-t-gray-100 py-2 px-4 text-right" | ||
| 146 | + > | ||
| 147 | + <Button class="mr-2" @click="closeDrawer">取消</Button> | ||
| 148 | + <Authority :value="OtaPermissionKey.UPDATE"> | ||
| 149 | + <Button type="primary" :loading="loading" @click="handleSubmit">保存</Button> | ||
| 150 | + </Authority> | ||
| 151 | + </div> | ||
| 152 | + </template> | ||
| 153 | + <!-- <DeviceDetailDrawer @register="registerTBDrawer" /> --> | ||
| 154 | + </BasicDrawer> | ||
| 155 | +</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 | + | ||
| 11 | +export enum OtaPermissionKey { | ||
| 12 | + CREATE = 'api:operation:ota:post', | ||
| 13 | + UPDATE = 'api:operation:ota:update', | ||
| 14 | + DELETE = 'api:operation:ota:delete', | ||
| 15 | + DOWNLOAD = 'api:operation:ota:download', | ||
| 16 | +} | ||
| 17 | + | ||
| 3 | export const columns: BasicColumn[] = [ | 18 | export const columns: BasicColumn[] = [ | 
| 4 | { | 19 | { | 
| 5 | title: '创建时间', | 20 | title: '创建时间', | 
| 6 | dataIndex: PackageField.CREATE_TIME, | 21 | dataIndex: PackageField.CREATE_TIME, | 
| 22 | + format(text) { | ||
| 23 | + return dateUtil(text).format(DEFAULT_DATE_FORMAT); | ||
| 24 | + }, | ||
| 7 | width: 120, | 25 | width: 120, | 
| 8 | }, | 26 | }, | 
| 9 | { | 27 | { | 
| @@ -18,29 +36,54 @@ export const columns: BasicColumn[] = [ | @@ -18,29 +36,54 @@ export const columns: BasicColumn[] = [ | ||
| 18 | }, | 36 | }, | 
| 19 | { | 37 | { | 
| 20 | title: '版本标签', | 38 | title: '版本标签', | 
| 21 | - dataIndex: PackageField.VERSION_LABEL, | 39 | + dataIndex: PackageField.VERSION_TAG, | 
| 22 | width: 120, | 40 | width: 120, | 
| 23 | }, | 41 | }, | 
| 24 | { | 42 | { | 
| 25 | title: '包类型', | 43 | title: '包类型', | 
| 26 | dataIndex: PackageField.PACKAGE_TYPE, | 44 | dataIndex: PackageField.PACKAGE_TYPE, | 
| 45 | + format: (text) => { | ||
| 46 | + return text === PackageType.FIRMWARE ? '固件' : '软件'; | ||
| 47 | + }, | ||
| 27 | width: 120, | 48 | width: 120, | 
| 28 | }, | 49 | }, | 
| 29 | { | 50 | { | 
| 30 | title: '直接URL', | 51 | title: '直接URL', | 
| 31 | - dataIndex: PackageField.PACKAGE_EXTERNAL_URL, | 52 | + dataIndex: PackageField.URL, | 
| 53 | + width: 120, | ||
| 54 | + }, | ||
| 55 | + { | ||
| 56 | + title: '文件名', | ||
| 57 | + dataIndex: PackageField.FILE_NAME, | ||
| 32 | width: 120, | 58 | width: 120, | 
| 33 | }, | 59 | }, | 
| 34 | { | 60 | { | 
| 35 | title: '文件大小', | 61 | title: '文件大小', | 
| 36 | dataIndex: PackageField.FILE_SIZE, | 62 | dataIndex: PackageField.FILE_SIZE, | 
| 63 | + format(text, record) { | ||
| 64 | + return record[PackageField.FILE_SIZE] | ||
| 65 | + ? `${Math.ceil(((text as unknown as number) / 1024) * 100) / 100}kb` | ||
| 66 | + : ''; | ||
| 67 | + }, | ||
| 37 | width: 120, | 68 | width: 120, | 
| 38 | }, | 69 | }, | 
| 39 | { | 70 | { | 
| 40 | title: '校验和', | 71 | title: '校验和', | 
| 41 | dataIndex: PackageField.CHECK_SUM, | 72 | dataIndex: PackageField.CHECK_SUM, | 
| 73 | + format(text, record) { | ||
| 74 | + return text ? `${record[PackageField.CHECK_SUM_ALG]}: ${text.slice(0, 11)}` : ''; | ||
| 75 | + }, | ||
| 42 | width: 120, | 76 | width: 120, | 
| 43 | }, | 77 | }, | 
| 78 | + { | ||
| 79 | + title: '操作', | ||
| 80 | + dataIndex: 'action', | ||
| 81 | + flag: 'ACTION', | ||
| 82 | + fixed: 'right', | ||
| 83 | + slots: { | ||
| 84 | + customRender: 'action', | ||
| 85 | + }, | ||
| 86 | + }, | ||
| 44 | ]; | 87 | ]; | 
| 45 | 88 | ||
| 46 | export const searchFormSchema: FormSchema[] = [ | 89 | export const searchFormSchema: FormSchema[] = [ | 
| 1 | +import { getDefaultDeviceProfile, 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,23 +36,38 @@ export enum CheckSumWay { | @@ -34,23 +36,38 @@ 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 | ||
| 48 | +const getVersionTag = (title: string, version: string) => { | ||
| 49 | + return `${title ?? ''} ${version ?? ''}`; | ||
| 50 | +}; | ||
| 51 | + | ||
| 46 | export const formSchema: FormSchema[] = [ | 52 | export const formSchema: FormSchema[] = [ | 
| 47 | { | 53 | { | 
| 48 | field: PackageField.TITLE, | 54 | field: PackageField.TITLE, | 
| 49 | label: '标题', | 55 | label: '标题', | 
| 50 | component: 'Input', | 56 | component: 'Input', | 
| 51 | rules: [{ required: true, message: '标题为必填项' }], | 57 | rules: [{ required: true, message: '标题为必填项' }], | 
| 52 | - componentProps: { | ||
| 53 | - placeholder: '请输入标题', | 58 | + componentProps: ({ formActionType, formModel }) => { | 
| 59 | + const { setFieldsValue } = formActionType; | ||
| 60 | + return { | ||
| 61 | + placeholder: '请输入标题', | ||
| 62 | + onChange: (value: Event) => { | ||
| 63 | + setFieldsValue({ | ||
| 64 | + [PackageField.VERSION_TAG]: getVersionTag( | ||
| 65 | + (value.target as HTMLInputElement).value, | ||
| 66 | + formModel[PackageField.VERSION] | ||
| 67 | + ), | ||
| 68 | + }); | ||
| 69 | + }, | ||
| 70 | + }; | ||
| 54 | }, | 71 | }, | 
| 55 | }, | 72 | }, | 
| 56 | { | 73 | { | 
| @@ -58,30 +75,61 @@ export const formSchema: FormSchema[] = [ | @@ -58,30 +75,61 @@ export const formSchema: FormSchema[] = [ | ||
| 58 | label: '版本', | 75 | label: '版本', | 
| 59 | component: 'Input', | 76 | component: 'Input', | 
| 60 | rules: [{ required: true, message: '版本为必填项' }], | 77 | rules: [{ required: true, message: '版本为必填项' }], | 
| 61 | - componentProps: { | ||
| 62 | - placeholder: '请输入版本', | 78 | + componentProps: ({ formActionType, formModel }) => { | 
| 79 | + const { setFieldsValue } = formActionType; | ||
| 80 | + return { | ||
| 81 | + placeholder: '请输入版本', | ||
| 82 | + onChange: (value: Event) => { | ||
| 83 | + setFieldsValue({ | ||
| 84 | + [PackageField.VERSION_TAG]: getVersionTag( | ||
| 85 | + formModel[PackageField.TITLE], | ||
| 86 | + (value.target as HTMLInputElement).value | ||
| 87 | + ), | ||
| 88 | + }); | ||
| 89 | + }, | ||
| 90 | + }; | ||
| 63 | }, | 91 | }, | 
| 64 | }, | 92 | }, | 
| 65 | { | 93 | { | 
| 66 | - field: PackageField.VERSION_LABEL, | 94 | + field: PackageField.VERSION_TAG, | 
| 67 | label: '版本标签', | 95 | label: '版本标签', | 
| 68 | component: 'Input', | 96 | component: 'Input', | 
| 69 | helpMessage: ['自定义标签应与您设备报告的软件包版本相匹配'], | 97 | helpMessage: ['自定义标签应与您设备报告的软件包版本相匹配'], | 
| 70 | - componentProps: { | ||
| 71 | - placeholder: '请输入版本标签', | 98 | + componentProps: () => { | 
| 99 | + return { | ||
| 100 | + placeholder: '请输入版本标签', | ||
| 101 | + }; | ||
| 72 | }, | 102 | }, | 
| 73 | }, | 103 | }, | 
| 74 | { | 104 | { | 
| 75 | - field: PackageField.DEVICE_CONFIGURATION, | 105 | + field: PackageField.DEVICE_PROFILE_INFO, | 
| 76 | label: '设备配置', | 106 | label: '设备配置', | 
| 77 | - component: 'Select', | 107 | + component: 'ApiSearchSelect', | 
| 78 | helpMessage: ['上传的包仅适用于具有所选配置文件的设备'], | 108 | helpMessage: ['上传的包仅适用于具有所选配置文件的设备'], | 
| 79 | defaultValue: 'default', | 109 | defaultValue: 'default', | 
| 80 | rules: [{ required: true, message: '设备配置为必填项' }], | 110 | rules: [{ required: true, message: '设备配置为必填项' }], | 
| 81 | - componentProps: () => { | 111 | + componentProps: ({ formActionType }) => { | 
| 112 | + const { setFieldsValue } = formActionType; | ||
| 82 | return { | 113 | return { | 
| 83 | - options: [{ label: 'default', value: 'default' }], | ||
| 84 | placeholder: '请选择设备配置', | 114 | placeholder: '请选择设备配置', | 
| 115 | + showSearch: true, | ||
| 116 | + resultField: 'data', | ||
| 117 | + labelField: 'name', | ||
| 118 | + valueField: 'id', | ||
| 119 | + api: async () => { | ||
| 120 | + const data = await getDefaultDeviceProfile(); | ||
| 121 | + data.id = JSON.stringify(data.id) as unknown as Id; | ||
| 122 | + setFieldsValue({ [PackageField.DEVICE_PROFILE_INFO]: data.id }); | ||
| 123 | + return { data: [data] }; | ||
| 124 | + }, | ||
| 125 | + searchApi: async (params: Recordable) => { | ||
| 126 | + const data = await getDeviceProfileInfos({ textSearch: params.text }); | ||
| 127 | + data.data = data.data.map((item) => ({ | ||
| 128 | + ...item, | ||
| 129 | + id: JSON.stringify(item.id) as unknown as Id, | ||
| 130 | + })); | ||
| 131 | + return data; | ||
| 132 | + }, | ||
| 85 | }; | 133 | }; | 
| 86 | }, | 134 | }, | 
| 87 | }, | 135 | }, | 
| @@ -103,15 +151,16 @@ export const formSchema: FormSchema[] = [ | @@ -103,15 +151,16 @@ export const formSchema: FormSchema[] = [ | ||
| 103 | }, | 151 | }, | 
| 104 | }, | 152 | }, | 
| 105 | { | 153 | { | 
| 106 | - field: PackageField.PACKAGE_UPDATE_TYPE, | 154 | + field: PackageField.IS_URL, | 
| 107 | label: '上传方式', | 155 | label: '上传方式', | 
| 108 | component: 'RadioGroup', | 156 | component: 'RadioGroup', | 
| 109 | - defaultValue: PackageUpdateType.BINARY_FILE, | 157 | + defaultValue: false, | 
| 110 | componentProps: () => { | 158 | componentProps: () => { | 
| 111 | return { | 159 | return { | 
| 160 | + defaultValue: false, | ||
| 112 | options: [ | 161 | options: [ | 
| 113 | - { label: '上传二进制文件', value: PackageUpdateType.BINARY_FILE }, | ||
| 114 | - { label: '使用外部URL', value: PackageUpdateType.EXTERNAL_URL }, | 162 | + { label: '上传二进制文件', value: false }, | 
| 163 | + { label: '使用外部URL', value: true }, | ||
| 115 | ], | 164 | ], | 
| 116 | }; | 165 | }; | 
| 117 | }, | 166 | }, | 
| @@ -120,25 +169,25 @@ export const formSchema: FormSchema[] = [ | @@ -120,25 +169,25 @@ export const formSchema: FormSchema[] = [ | ||
| 120 | field: PackageField.PACKAGE_BINARY_FILE, | 169 | field: PackageField.PACKAGE_BINARY_FILE, | 
| 121 | label: '二进制文件', | 170 | label: '二进制文件', | 
| 122 | ifShow: ({ model }) => { | 171 | ifShow: ({ model }) => { | 
| 123 | - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.BINARY_FILE; | 172 | + return !model[PackageField.IS_URL]; | 
| 124 | }, | 173 | }, | 
| 125 | component: 'ApiUpload', | 174 | component: 'ApiUpload', | 
| 126 | valueField: PackageField.PACKAGE_BINARY_FILE, | 175 | valueField: PackageField.PACKAGE_BINARY_FILE, | 
| 127 | changeEvent: `update:${PackageField.PACKAGE_BINARY_FILE}`, | 176 | changeEvent: `update:${PackageField.PACKAGE_BINARY_FILE}`, | 
| 177 | + rules: [{ required: true, message: '请上传二进制文件', type: 'array' }], | ||
| 128 | componentProps: { | 178 | componentProps: { | 
| 129 | maxFileLimit: 1, | 179 | maxFileLimit: 1, | 
| 130 | - api: (_file: File) => { | ||
| 131 | - console.log({ _file }); | ||
| 132 | - return { uid: _file.uid, name: _file.name }; | 180 | + api: (file: FileItem) => { | 
| 181 | + return { uid: file.uid, name: file.name, file }; | ||
| 133 | }, | 182 | }, | 
| 134 | }, | 183 | }, | 
| 135 | }, | 184 | }, | 
| 136 | { | 185 | { | 
| 137 | - field: PackageField.PACKAGE_EXTERNAL_URL, | 186 | + field: PackageField.URL, | 
| 138 | label: '外部URL', | 187 | label: '外部URL', | 
| 139 | component: 'Input', | 188 | component: 'Input', | 
| 140 | ifShow: ({ model }) => { | 189 | ifShow: ({ model }) => { | 
| 141 | - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.EXTERNAL_URL; | 190 | + return model[PackageField.IS_URL]; | 
| 142 | }, | 191 | }, | 
| 143 | rules: [{ required: true, message: '外部URL为必填项' }], | 192 | rules: [{ required: true, message: '外部URL为必填项' }], | 
| 144 | componentProps: { | 193 | componentProps: { | 
| @@ -146,10 +195,13 @@ export const formSchema: FormSchema[] = [ | @@ -146,10 +195,13 @@ export const formSchema: FormSchema[] = [ | ||
| 146 | }, | 195 | }, | 
| 147 | }, | 196 | }, | 
| 148 | { | 197 | { | 
| 149 | - field: PackageField.CHECK_SUM_WAY, | 198 | + field: PackageField.VALIDATE_WAY, | 
| 150 | label: '校验和方式', | 199 | label: '校验和方式', | 
| 151 | component: 'RadioGroup', | 200 | component: 'RadioGroup', | 
| 152 | defaultValue: CheckSumWay.AUTO, | 201 | defaultValue: CheckSumWay.AUTO, | 
| 202 | + ifShow: ({ model }) => { | ||
| 203 | + return !model[PackageField.IS_URL]; | ||
| 204 | + }, | ||
| 153 | componentProps: () => { | 205 | componentProps: () => { | 
| 154 | return { | 206 | return { | 
| 155 | options: [ | 207 | options: [ | 
| @@ -160,12 +212,13 @@ export const formSchema: FormSchema[] = [ | @@ -160,12 +212,13 @@ export const formSchema: FormSchema[] = [ | ||
| 160 | }, | 212 | }, | 
| 161 | }, | 213 | }, | 
| 162 | { | 214 | { | 
| 163 | - field: PackageField.ALG, | 215 | + field: PackageField.CHECK_SUM_ALG, | 
| 164 | label: '校验和算法', | 216 | label: '校验和算法', | 
| 165 | component: 'Select', | 217 | component: 'Select', | 
| 166 | ifShow: ({ model }) => { | 218 | ifShow: ({ model }) => { | 
| 167 | - return model[PackageField.CHECK_SUM_WAY] === CheckSumWay.MANUAL; | 219 | + return model[PackageField.VALIDATE_WAY] === CheckSumWay.MANUAL && !model[PackageField.IS_URL]; | 
| 168 | }, | 220 | }, | 
| 221 | + defaultValue: ALG.SHA_256, | ||
| 169 | componentProps: { | 222 | componentProps: { | 
| 170 | placeholder: '请选择校验和算法', | 223 | placeholder: '请选择校验和算法', | 
| 171 | options: Object.keys(ALG).map((key) => { | 224 | options: Object.keys(ALG).map((key) => { | 
| @@ -181,7 +234,7 @@ export const formSchema: FormSchema[] = [ | @@ -181,7 +234,7 @@ export const formSchema: FormSchema[] = [ | ||
| 181 | label: '校验和', | 234 | label: '校验和', | 
| 182 | component: 'Input', | 235 | component: 'Input', | 
| 183 | ifShow: ({ model }) => { | 236 | ifShow: ({ model }) => { | 
| 184 | - return model[PackageField.CHECK_SUM_WAY] === CheckSumWay.MANUAL; | 237 | + return model[PackageField.VALIDATE_WAY] === CheckSumWay.MANUAL && !model[PackageField.IS_URL]; | 
| 185 | }, | 238 | }, | 
| 186 | helpMessage: ['如果校验和为空,会自动生成'], | 239 | helpMessage: ['如果校验和为空,会自动生成'], | 
| 187 | componentProps: { | 240 | 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, OtaPermissionKey, 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'; | ||
| 14 | + import { computed } from 'vue'; | ||
| 15 | + import { useSyncConfirm } from '/@/hooks/component/useSyncConfirm'; | ||
| 16 | + import { Authority } from '/@/components/Authority'; | ||
| 8 | 17 | ||
| 9 | - const [register] = useTable({ | 18 | + const [register, { reload, getSelectRowKeys, getRowSelection, setSelectedRowKeys }] = useTable({ | 
| 10 | columns, | 19 | columns, | 
| 11 | title: '包仓库', | 20 | title: '包仓库', | 
| 21 | + api: async (params: GetOtaPackagesParams) => { | ||
| 22 | + const data = await getOtaPackagesList({ | ||
| 23 | + ...params, | ||
| 24 | + page: params.page - 1, | ||
| 25 | + textSearch: params.title, | ||
| 26 | + }); | ||
| 27 | + return { ...data, page: params.page }; | ||
| 28 | + }, | ||
| 29 | + pagination: { | ||
| 30 | + showSizeChanger: true, | ||
| 31 | + pageSizeOptions: ['1', '2', '3', '4'], | ||
| 32 | + }, | ||
| 33 | + fetchSetting: { | ||
| 34 | + totalField: 'totalElements', | ||
| 35 | + listField: 'data', | ||
| 36 | + }, | ||
| 12 | formConfig: { | 37 | formConfig: { | 
| 13 | labelWidth: 120, | 38 | labelWidth: 120, | 
| 14 | schemas: searchFormSchema, | 39 | schemas: searchFormSchema, | 
| 15 | }, | 40 | }, | 
| 41 | + rowKey: (record: OtaRecordDatum) => record.id.id, | ||
| 42 | + showIndexColumn: false, | ||
| 16 | useSearchForm: true, | 43 | useSearchForm: true, | 
| 17 | showTableSetting: true, | 44 | showTableSetting: true, | 
| 45 | + rowSelection: { | ||
| 46 | + type: 'checkbox', | ||
| 47 | + }, | ||
| 18 | }); | 48 | }); | 
| 19 | 49 | ||
| 50 | + const { createConfirm, createMessage } = useMessage(); | ||
| 51 | + | ||
| 20 | const [registerModal, { openModal }] = useModal(); | 52 | const [registerModal, { openModal }] = useModal(); | 
| 21 | 53 | ||
| 54 | + const [registerDrawer, { openDrawer }] = useDrawer(); | ||
| 55 | + | ||
| 22 | const handleCreatePackage = () => { | 56 | const handleCreatePackage = () => { | 
| 23 | - openModal(true); | 57 | + openModal(true, { isUpdate: false } as ModalPassRecord); | 
| 58 | + }; | ||
| 59 | + | ||
| 60 | + const handleOpenDetailDrawer = (record: OtaRecordDatum) => { | ||
| 61 | + openDrawer(true, record.id.id); | ||
| 62 | + }; | ||
| 63 | + | ||
| 64 | + const downloadFile = async (record: OtaRecordDatum) => { | ||
| 65 | + await useDownload(record); | ||
| 66 | + }; | ||
| 67 | + | ||
| 68 | + const deletePackage = (record: OtaRecordDatum) => { | ||
| 69 | + createConfirm({ | ||
| 70 | + iconType: 'warning', | ||
| 71 | + content: '是否确认删除操作?', | ||
| 72 | + onOk: async () => { | ||
| 73 | + try { | ||
| 74 | + await deleteOtaPackage(record.id.id); | ||
| 75 | + createMessage.success('删除成功'); | ||
| 76 | + reload(); | ||
| 77 | + } catch (error) {} | ||
| 78 | + }, | ||
| 79 | + }); | ||
| 80 | + }; | ||
| 81 | + | ||
| 82 | + const canDelete = computed(() => { | ||
| 83 | + const rowSelection = getRowSelection(); | ||
| 84 | + return !rowSelection.selectedRowKeys?.length; | ||
| 85 | + }); | ||
| 86 | + | ||
| 87 | + const { createSyncConfirm } = useSyncConfirm(); | ||
| 88 | + const handleBatchDelete = async () => { | ||
| 89 | + const rowKeys = getSelectRowKeys(); | ||
| 90 | + try { | ||
| 91 | + await createSyncConfirm({ iconType: 'warning', content: '确认后所有选中的OTA升级将被删除' }); | ||
| 92 | + for (const key of rowKeys) { | ||
| 93 | + await deleteOtaPackage(key); | ||
| 94 | + } | ||
| 95 | + createMessage.success('批量删除成功'); | ||
| 96 | + setSelectedRowKeys([]); | ||
| 97 | + reload(); | ||
| 98 | + } catch (error) {} | ||
| 24 | }; | 99 | }; | 
| 25 | </script> | 100 | </script> | 
| 26 | 101 | ||
| 27 | <template> | 102 | <template> | 
| 28 | <PageWrapper dense contentFullHeight contentClass="flex flex-col"> | 103 | <PageWrapper dense contentFullHeight contentClass="flex flex-col"> | 
| 29 | - <BasicTable @register="register"> | 104 | + <BasicTable @register="register" @row-click="handleOpenDetailDrawer" class="ota-list"> | 
| 30 | <template #toolbar> | 105 | <template #toolbar> | 
| 31 | - <Button @click="handleCreatePackage" type="primary">新增包</Button> | 106 | + <Authority :value="OtaPermissionKey.CREATE"> | 
| 107 | + <Button @click="handleCreatePackage" type="primary">新增包</Button> | ||
| 108 | + </Authority> | ||
| 109 | + <Authority :value="OtaPermissionKey.DELETE"> | ||
| 110 | + <Button @click="handleBatchDelete" :disabled="canDelete" type="primary" danger> | ||
| 111 | + 批量删除 | ||
| 112 | + </Button> | ||
| 113 | + </Authority> | ||
| 114 | + </template> | ||
| 115 | + <template #action="{ record }"> | ||
| 116 | + <TableAction | ||
| 117 | + @click.stop | ||
| 118 | + :actions="[ | ||
| 119 | + { | ||
| 120 | + label: '下载', | ||
| 121 | + icon: 'ant-design:download-outlined', | ||
| 122 | + auth: OtaPermissionKey.DOWNLOAD, | ||
| 123 | + onClick: downloadFile.bind(null, record), | ||
| 124 | + }, | ||
| 125 | + { | ||
| 126 | + label: '删除', | ||
| 127 | + icon: 'ant-design:delete-outlined', | ||
| 128 | + color: 'error', | ||
| 129 | + auth: OtaPermissionKey.DELETE, | ||
| 130 | + popConfirm: { | ||
| 131 | + title: '是否确认删除', | ||
| 132 | + confirm: deletePackage.bind(null, record), | ||
| 133 | + }, | ||
| 134 | + }, | ||
| 135 | + ]" | ||
| 136 | + /> | ||
| 32 | </template> | 137 | </template> | 
| 33 | </BasicTable> | 138 | </BasicTable> | 
| 34 | - <PackageDetailModal @register="registerModal" /> | 139 | + <PackageDetailModal @register="registerModal" @update:list="reload" /> | 
| 140 | + <PackagesDetailDrawer @register="registerDrawer" @update:list="reload" /> | ||
| 35 | </PageWrapper> | 141 | </PageWrapper> | 
| 36 | </template> | 142 | </template> | 
| 143 | + | ||
| 144 | +<style scoped lang="less"> | ||
| 145 | + .ota-list:deep(.ant-table-tbody > tr > td:last-of-type) { | ||
| 146 | + width: 100%; | ||
| 147 | + height: 100%; | ||
| 148 | + padding: 0 !important; | ||
| 149 | + } | ||
| 150 | +</style> | 
| @@ -5,7 +5,7 @@ | @@ -5,7 +5,7 @@ | ||
| 5 | </script> | 5 | </script> | 
| 6 | <script lang="ts" setup> | 6 | <script lang="ts" setup> | 
| 7 | import type { ECharts, EChartsOption } from 'echarts'; | 7 | import type { ECharts, EChartsOption } from 'echarts'; | 
| 8 | - import { PropType, watch } from 'vue'; | 8 | + import { watch } from 'vue'; | 
| 9 | import { nextTick, onMounted, onUnmounted, ref, unref, computed } from 'vue'; | 9 | import { nextTick, onMounted, onUnmounted, ref, unref, computed } from 'vue'; | 
| 10 | import { init } from 'echarts'; | 10 | import { init } from 'echarts'; | 
| 11 | import { | 11 | import { | 
| @@ -26,29 +26,23 @@ | @@ -26,29 +26,23 @@ | ||
| 26 | import { Tooltip } from 'ant-design-vue'; | 26 | import { Tooltip } from 'ant-design-vue'; | 
| 27 | import { useThrottleFn } from '@vueuse/shared'; | 27 | import { useThrottleFn } from '@vueuse/shared'; | 
| 28 | import { buildUUID } from '/@/utils/uuid'; | 28 | import { buildUUID } from '/@/utils/uuid'; | 
| 29 | - import { FrontComponent } from '../help'; | ||
| 30 | - | ||
| 31 | - const props = defineProps({ | ||
| 32 | - add: { | ||
| 33 | - type: Function, | ||
| 34 | - }, | ||
| 35 | - layout: { | ||
| 36 | - type: Object as PropType<DashboardComponentLayout>, | ||
| 37 | - default: () => ({}), | ||
| 38 | - }, | ||
| 39 | - value: { | ||
| 40 | - type: Object as PropType<DashBoardValue>, | ||
| 41 | - default: () => ({ id: buildUUID() }), | ||
| 42 | - }, | ||
| 43 | - radio: { | ||
| 44 | - type: Object as PropType<RadioRecord>, | ||
| 45 | - default: () => DEFAULT_RADIO_RECORD, | ||
| 46 | - }, | ||
| 47 | - random: { | ||
| 48 | - type: Boolean, | ||
| 49 | - default: true, | ||
| 50 | - }, | ||
| 51 | - }); | 29 | + import { FrontComponent } from '../../const/const'; | 
| 30 | + | ||
| 31 | + const props = withDefaults( | ||
| 32 | + defineProps<{ | ||
| 33 | + add?: Function; | ||
| 34 | + layout?: DashboardComponentLayout; | ||
| 35 | + value?: DashBoardValue; | ||
| 36 | + radio?: RadioRecord; | ||
| 37 | + random?: boolean; | ||
| 38 | + }>(), | ||
| 39 | + { | ||
| 40 | + layout: () => ({} as unknown as DashboardComponentLayout), | ||
| 41 | + value: () => ({ id: buildUUID() }), | ||
| 42 | + radio: () => DEFAULT_RADIO_RECORD, | ||
| 43 | + random: true, | ||
| 44 | + } | ||
| 45 | + ); | ||
| 52 | 46 | ||
| 53 | const getControlsWidgetId = () => `widget-chart-${props.value.id}`; | 47 | const getControlsWidgetId = () => `widget-chart-${props.value.id}`; | 
| 54 | 48 | 
| @@ -261,7 +261,15 @@ const handleValue = (value: any) => { | @@ -261,7 +261,15 @@ const handleValue = (value: any) => { | ||
| 261 | 261 | ||
| 262 | export const update_instrument_1_value = (params: DashBoardValue) => { | 262 | export const update_instrument_1_value = (params: DashBoardValue) => { | 
| 263 | const { value = 0, unit = '°C', fontColor } = params; | 263 | const { value = 0, unit = '°C', fontColor } = params; | 
| 264 | - let max = value > 1 ? Number(1 + Array(String(value).length).fill(0).join('')) / 2 : 100 / 2; | 264 | + let max = | 
| 265 | + value > 1 | ||
| 266 | + ? Number( | ||
| 267 | + 1 + | ||
| 268 | + Array(String(Math.floor(value)).length) | ||
| 269 | + .fill(0) | ||
| 270 | + .join('') | ||
| 271 | + ) / 2 | ||
| 272 | + : 100 / 2; | ||
| 265 | max = value > max ? max * 2 : max; | 273 | max = value > max ? max * 2 : max; | 
| 266 | return { | 274 | return { | 
| 267 | series: [ | 275 | series: [ | 
| @@ -288,8 +296,22 @@ export const update_instrument_2_value = (params: DashBoardValue) => { | @@ -288,8 +296,22 @@ export const update_instrument_2_value = (params: DashBoardValue) => { | ||
| 288 | const thirdRecord = getGradient(Gradient.THIRD, gradientInfo); | 296 | const thirdRecord = getGradient(Gradient.THIRD, gradientInfo); | 
| 289 | 297 | ||
| 290 | let max = thirdRecord?.value || secondRecord?.value || firstRecord?.value || 70; | 298 | let max = thirdRecord?.value || secondRecord?.value || firstRecord?.value || 70; | 
| 291 | - max = Number(1 + Array(String(max).length).fill(0).join('')); | ||
| 292 | - max = value > 1 ? Number(1 + Array(String(value).length).fill(0).join('')) / 2 : 100 / 2; | 299 | + max = Number( | 
| 300 | + 1 + | ||
| 301 | + Array(String(Math.floor(max)).length) | ||
| 302 | + .fill(0) | ||
| 303 | + .join('') | ||
| 304 | + ); | ||
| 305 | + | ||
| 306 | + max = | ||
| 307 | + value > 1 | ||
| 308 | + ? Number( | ||
| 309 | + 1 + | ||
| 310 | + Array(String(Math.floor(value)).length) | ||
| 311 | + .fill(0) | ||
| 312 | + .join('') | ||
| 313 | + ) / 2 | ||
| 314 | + : 100 / 2; | ||
| 293 | max = value > max ? max * 2 : max; | 315 | max = value > max ? max * 2 : max; | 
| 294 | 316 | ||
| 295 | const firstGradient = firstRecord?.value ? firstRecord.value / max : 0.3; | 317 | const firstGradient = firstRecord?.value ? firstRecord.value / max : 0.3; | 
| @@ -4,32 +4,162 @@ | @@ -4,32 +4,162 @@ | ||
| 4 | }; | 4 | }; | 
| 5 | </script> | 5 | </script> | 
| 6 | <script lang="ts" setup> | 6 | <script lang="ts" setup> | 
| 7 | - import { nextTick, onMounted, ref, unref } from 'vue'; | ||
| 8 | - import { useScript } from '/@/hooks/web/useScript'; | ||
| 9 | - import { BAI_DU_MAP_URL } from '/@/utils/fnUtils'; | 7 | + import { computed, onMounted, ref, unref } from 'vue'; | 
| 8 | + import { RadioRecord } from '../../detail/config/util'; | ||
| 9 | + import { MapComponentLayout, MapComponentValue } from './map.config'; | ||
| 10 | + import { | ||
| 11 | + ClockCircleOutlined, | ||
| 12 | + PlayCircleOutlined, | ||
| 13 | + PauseCircleOutlined, | ||
| 14 | + } from '@ant-design/icons-vue'; | ||
| 15 | + import { Button, Tooltip } from 'ant-design-vue'; | ||
| 16 | + import { FrontComponent } from '../../const/const'; | ||
| 17 | + import { buildUUID } from '/@/utils/uuid'; | ||
| 18 | + | ||
| 19 | + // useVisualBoardContext(); | ||
| 20 | + | ||
| 21 | + const startMethodName = `trackPlayMethod_${buildUUID()}`; | ||
| 22 | + | ||
| 23 | + const wrapId = `bai-map-${buildUUID()}`; | ||
| 24 | + | ||
| 25 | + enum TrackAnimationStatus { | ||
| 26 | + PLAY = 1, | ||
| 27 | + DONE = 2, | ||
| 28 | + PAUSE = 3, | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + const props = withDefaults( | ||
| 32 | + defineProps<{ | ||
| 33 | + value?: MapComponentValue; | ||
| 34 | + layout?: MapComponentLayout; | ||
| 35 | + radio?: RadioRecord; | ||
| 36 | + random?: boolean; | ||
| 37 | + }>(), | ||
| 38 | + { | ||
| 39 | + random: true, | ||
| 40 | + } | ||
| 41 | + ); | ||
| 10 | 42 | ||
| 11 | const wrapRef = ref<HTMLDivElement | null>(null); | 43 | const wrapRef = ref<HTMLDivElement | null>(null); | 
| 12 | - const { toPromise } = useScript({ src: BAI_DU_MAP_URL }); | 44 | + const trackAni = ref<Nullable<any>>(null); | 
| 45 | + let mapInstance: Nullable<Recordable> = null; | ||
| 13 | 46 | ||
| 14 | async function initMap() { | 47 | async function initMap() { | 
| 15 | - await toPromise(); | ||
| 16 | - await nextTick(); | ||
| 17 | const wrapEl = unref(wrapRef); | 48 | const wrapEl = unref(wrapRef); | 
| 18 | if (!wrapEl) return; | 49 | if (!wrapEl) return; | 
| 19 | - const BMap = (window as any).BMap; | ||
| 20 | - const map = new BMap.Map(wrapEl); | ||
| 21 | - const point = new BMap.Point(116.404, 39.915); | ||
| 22 | - map.centerAndZoom(point, 15); | ||
| 23 | - map.enableScrollWheelZoom(true); | 50 | + const BMapGL = (window as any).BMapGL; | 
| 51 | + mapInstance = new BMapGL.Map(wrapId); | ||
| 52 | + const point = new BMapGL.Point(116.404, 39.915); | ||
| 53 | + mapInstance!.centerAndZoom(point, 15); | ||
| 54 | + mapInstance!.enableScrollWheelZoom(true); | ||
| 55 | + props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY && randomAnimation(); | ||
| 24 | } | 56 | } | 
| 25 | 57 | ||
| 58 | + const randomAnimation = () => { | ||
| 59 | + const path = [ | ||
| 60 | + { | ||
| 61 | + lng: 116.297611, | ||
| 62 | + lat: 40.047363, | ||
| 63 | + }, | ||
| 64 | + { | ||
| 65 | + lng: 116.302839, | ||
| 66 | + lat: 40.048219, | ||
| 67 | + }, | ||
| 68 | + { | ||
| 69 | + lng: 116.308301, | ||
| 70 | + lat: 40.050566, | ||
| 71 | + }, | ||
| 72 | + { | ||
| 73 | + lng: 116.305732, | ||
| 74 | + lat: 40.054957, | ||
| 75 | + }, | ||
| 76 | + { | ||
| 77 | + lng: 116.304754, | ||
| 78 | + lat: 40.057953, | ||
| 79 | + }, | ||
| 80 | + { | ||
| 81 | + lng: 116.306487, | ||
| 82 | + lat: 40.058312, | ||
| 83 | + }, | ||
| 84 | + { | ||
| 85 | + lng: 116.307223, | ||
| 86 | + lat: 40.056379, | ||
| 87 | + }, | ||
| 88 | + ]; | ||
| 89 | + | ||
| 90 | + const point: any[] = []; | ||
| 91 | + const BMapGL = (window as any).BMapGL; | ||
| 92 | + | ||
| 93 | + for (const { lng, lat } of path) { | ||
| 94 | + point.push(new BMapGL.Point(lng, lat)); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + const pl = new BMapGL.Polyline(point); | ||
| 98 | + const BMapGLLib = (window as any).BMapGLLib; | ||
| 99 | + | ||
| 100 | + const dynamicPlayMethod = { | ||
| 101 | + [startMethodName]() { | ||
| 102 | + trackAni.value = new BMapGLLib.TrackAnimation(mapInstance, pl, { | ||
| 103 | + overallView: true, | ||
| 104 | + tilt: 30, | ||
| 105 | + duration: 20000, | ||
| 106 | + delay: 300, | ||
| 107 | + }); | ||
| 108 | + trackAni.value!.start(); | ||
| 109 | + }, | ||
| 110 | + }; | ||
| 111 | + | ||
| 112 | + (window as any)[startMethodName] = dynamicPlayMethod[startMethodName]; | ||
| 113 | + | ||
| 114 | + setTimeout(`${startMethodName}()`); | ||
| 115 | + }; | ||
| 116 | + | ||
| 26 | onMounted(() => { | 117 | onMounted(() => { | 
| 27 | initMap(); | 118 | initMap(); | 
| 28 | }); | 119 | }); | 
| 120 | + | ||
| 121 | + const getTimeRange = computed(() => { | ||
| 122 | + return ` - 从 ${'2020-10-20 10:10:10'} 到 ${'2020-10-20 10:10:10'}`; | ||
| 123 | + }); | ||
| 124 | + | ||
| 125 | + const handleTrackSwitch = () => {}; | ||
| 126 | + | ||
| 127 | + const getTrackPlayStatus = computed(() => { | ||
| 128 | + return (trackAni.value || {})._status; | ||
| 129 | + }); | ||
| 130 | + | ||
| 131 | + const handlePlay = () => { | ||
| 132 | + if (unref(getTrackPlayStatus) === TrackAnimationStatus.DONE) unref(trackAni).start(); | ||
| 133 | + else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PLAY) unref(trackAni).pause(); | ||
| 134 | + else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PAUSE) unref(trackAni).continue(); | ||
| 135 | + }; | ||
| 29 | </script> | 136 | </script> | 
| 30 | 137 | ||
| 31 | <template> | 138 | <template> | 
| 32 | - <div class="w-full h-full flex justify-center items-center"> | ||
| 33 | - <div ref="wrapRef" class="w-[95%] h-[95%]"></div> | 139 | + <div class="w-full h-full flex justify-center items-center flex-col"> | 
| 140 | + <div | ||
| 141 | + class="w-full flex" | ||
| 142 | + v-if="props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY" | ||
| 143 | + > | ||
| 144 | + <Button type="text" class="!px-2 flex-auto !text-left truncate" @click="handleTrackSwitch"> | ||
| 145 | + <div class="w-full truncate text-gray-500 flex items-center"> | ||
| 146 | + <ClockCircleOutlined /> | ||
| 147 | + <span class="mx-1">实时</span> | ||
| 148 | + <Tooltip :title="getTimeRange.replace('-', '')"> | ||
| 149 | + <span class="truncate"> | ||
| 150 | + {{ getTimeRange }} | ||
| 151 | + </span> | ||
| 152 | + </Tooltip> | ||
| 153 | + </div> | ||
| 154 | + </Button> | ||
| 155 | + <Button type="text" class="!px-2 !text-gray-500" @click="handlePlay"> | ||
| 156 | + <PlayCircleOutlined v-show="getTrackPlayStatus !== TrackAnimationStatus.PLAY" /> | ||
| 157 | + <PauseCircleOutlined v-show="getTrackPlayStatus === TrackAnimationStatus.PLAY" /> | ||
| 158 | + <span> | ||
| 159 | + {{ getTrackPlayStatus !== TrackAnimationStatus.PLAY ? '播放轨迹' : '暂停播放' }} | ||
| 160 | + </span> | ||
| 161 | + </Button> | ||
| 162 | + </div> | ||
| 163 | + <div ref="wrapRef" :id="wrapId" class="w-full h-full"></div> | ||
| 34 | </div> | 164 | </div> | 
| 35 | </template> | 165 | </template> | 
| 1 | -import { ComponentConfig } from '../help'; | 1 | +import { FrontComponent } from '../../const/const'; | 
| 2 | +import { ComponentConfig } from '../../types/type'; | ||
| 3 | + | ||
| 4 | +export interface MapComponentLayout { | ||
| 5 | + componentType?: FrontComponent; | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +export interface MapComponentValue { | ||
| 9 | + icon?: string; | ||
| 10 | + track?: Recordable[]; | ||
| 11 | +} | ||
| 12 | + | ||
| 13 | +interface Config { | ||
| 14 | + componentType?: FrontComponent; | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +export const MaphistoryTrackConfig: Config = { | ||
| 18 | + componentType: FrontComponent.MAP_COMPONENT_TRACK_HISTORY, | ||
| 19 | +}; | ||
| 20 | + | ||
| 21 | +export const MapRealTrackConfig: Config = { | ||
| 22 | + componentType: FrontComponent.MAP_COMPONENT_TRACK_REAL, | ||
| 23 | +}; | ||
| 2 | 24 | ||
| 3 | export const transfromMapComponentConfig: ComponentConfig['transformConfig'] = ( | 25 | export const transfromMapComponentConfig: ComponentConfig['transformConfig'] = ( | 
| 4 | - _componentConfig, | 26 | + componentConfig: Config, | 
| 5 | _record, | 27 | _record, | 
| 6 | _dataSourceRecord | 28 | _dataSourceRecord | 
| 7 | ) => { | 29 | ) => { | 
| 8 | - return {}; | 30 | + return { | 
| 31 | + layout: { | ||
| 32 | + ...componentConfig, | ||
| 33 | + }, | ||
| 34 | + }; | ||
| 9 | }; | 35 | }; | 
| 1 | <script lang="ts" setup> | 1 | <script lang="ts" setup> | 
| 2 | import { useUpdateCenter } from '../../hook/useUpdateCenter'; | 2 | import { useUpdateCenter } from '../../hook/useUpdateCenter'; | 
| 3 | import { FrontDataSourceRecord } from '../../types/type'; | 3 | import { FrontDataSourceRecord } from '../../types/type'; | 
| 4 | + import { createVisualBoardContext } from '../../hook/useVisualBoardContext'; | ||
| 4 | 5 | ||
| 5 | const props = defineProps<{ | 6 | const props = defineProps<{ | 
| 6 | dataSource: FrontDataSourceRecord[]; | 7 | dataSource: FrontDataSourceRecord[]; | 
| @@ -8,6 +9,8 @@ | @@ -8,6 +9,8 @@ | ||
| 8 | 9 | ||
| 9 | const { update, add, remove } = useUpdateCenter(); | 10 | const { update, add, remove } = useUpdateCenter(); | 
| 10 | 11 | ||
| 12 | + createVisualBoardContext({ update, add, remove }); | ||
| 13 | + | ||
| 11 | defineExpose({ update }); | 14 | defineExpose({ update }); | 
| 12 | </script> | 15 | </script> | 
| 13 | 16 | 
| @@ -27,7 +27,11 @@ import ToggleSwitch from './ControlComponent/ToggleSwitch.vue'; | @@ -27,7 +27,11 @@ import ToggleSwitch from './ControlComponent/ToggleSwitch.vue'; | ||
| 27 | import SlidingSwitch from './ControlComponent/SlidingSwitch.vue'; | 27 | import SlidingSwitch from './ControlComponent/SlidingSwitch.vue'; | 
| 28 | import SwitchWithIcon from './ControlComponent/SwitchWithIcon.vue'; | 28 | import SwitchWithIcon from './ControlComponent/SwitchWithIcon.vue'; | 
| 29 | import MapComponent from './MapComponent/MapComponent.vue'; | 29 | import MapComponent from './MapComponent/MapComponent.vue'; | 
| 30 | -import { transfromMapComponentConfig } from './MapComponent/map.config'; | 30 | +import { | 
| 31 | + MaphistoryTrackConfig, | ||
| 32 | + MapRealTrackConfig, | ||
| 33 | + transfromMapComponentConfig, | ||
| 34 | +} from './MapComponent/map.config'; | ||
| 31 | import { ComponentConfig } from '../types/type'; | 35 | import { ComponentConfig } from '../types/type'; | 
| 32 | import { FrontComponent, FrontComponentCategory } from '../const/const'; | 36 | import { FrontComponent, FrontComponentCategory } from '../const/const'; | 
| 33 | 37 | ||
| @@ -135,10 +139,20 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, { | @@ -135,10 +139,20 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, { | ||
| 135 | transformConfig: transformControlConfig, | 139 | transformConfig: transformControlConfig, | 
| 136 | }); | 140 | }); | 
| 137 | 141 | ||
| 138 | -frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK, { | 142 | +frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_REAL, { | 
| 139 | Component: MapComponent, | 143 | Component: MapComponent, | 
| 140 | ComponentName: '实时轨迹', | 144 | ComponentName: '实时轨迹', | 
| 141 | - ComponentKey: FrontComponent.MAP_COMPONENT_TRACK, | 145 | + ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_REAL, | 
| 146 | + ComponentConfig: MapRealTrackConfig, | ||
| 147 | + ComponentCategory: FrontComponentCategory.MAP, | ||
| 148 | + transformConfig: transfromMapComponentConfig, | ||
| 149 | +}); | ||
| 150 | + | ||
| 151 | +frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_HISTORY, { | ||
| 152 | + Component: MapComponent, | ||
| 153 | + ComponentName: '历史轨迹', | ||
| 154 | + ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_HISTORY, | ||
| 155 | + ComponentConfig: MaphistoryTrackConfig, | ||
| 142 | ComponentCategory: FrontComponentCategory.MAP, | 156 | ComponentCategory: FrontComponentCategory.MAP, | 
| 143 | transformConfig: transfromMapComponentConfig, | 157 | transformConfig: transfromMapComponentConfig, | 
| 144 | }); | 158 | }); | 
| @@ -27,7 +27,8 @@ export enum FrontComponent { | @@ -27,7 +27,8 @@ export enum FrontComponent { | ||
| 27 | CONTROL_COMPONENT_TOGGLE_SWITCH = 'control-component-toggle-switch', | 27 | CONTROL_COMPONENT_TOGGLE_SWITCH = 'control-component-toggle-switch', | 
| 28 | CONTROL_COMPONENT_SWITCH_WITH_ICON = 'control-component-switch-with-icon', | 28 | CONTROL_COMPONENT_SWITCH_WITH_ICON = 'control-component-switch-with-icon', | 
| 29 | CONTROL_COMPONENT_SLIDING_SWITCH = 'control-component-sliding-switch', | 29 | CONTROL_COMPONENT_SLIDING_SWITCH = 'control-component-sliding-switch', | 
| 30 | - MAP_COMPONENT_TRACK = 'map-component-track', | 30 | + MAP_COMPONENT_TRACK_REAL = 'map-component-track-real', | 
| 31 | + MAP_COMPONENT_TRACK_HISTORY = 'map-component-track-history', | ||
| 31 | } | 32 | } | 
| 32 | 33 | ||
| 33 | export enum Gradient { | 34 | export enum Gradient { | 
| 1 | +<script lang="ts" setup> | ||
| 2 | + import { ref, unref } from 'vue'; | ||
| 3 | + import { BasicForm, FormActionType } from '/@/components/Form'; | ||
| 4 | + import { mapFormSchema } from '../../config/basicConfiguration'; | ||
| 5 | + | ||
| 6 | + const formEl = ref<Nullable<FormActionType>>(); | ||
| 7 | + | ||
| 8 | + const setFormEl = (el: any) => { | ||
| 9 | + formEl.value = el; | ||
| 10 | + }; | ||
| 11 | + | ||
| 12 | + const getFieldsValue = () => { | ||
| 13 | + return unref(formEl)!.getFieldsValue(); | ||
| 14 | + }; | ||
| 15 | + | ||
| 16 | + const validate = async () => { | ||
| 17 | + await unref(formEl)!.validate(); | ||
| 18 | + }; | ||
| 19 | + | ||
| 20 | + const setFieldsValue = async (record: Recordable) => { | ||
| 21 | + await unref(formEl)!.setFieldsValue(record); | ||
| 22 | + }; | ||
| 23 | + | ||
| 24 | + const clearValidate = async (name?: string | string[]) => { | ||
| 25 | + await unref(formEl)!.clearValidate(name); | ||
| 26 | + }; | ||
| 27 | + defineExpose({ | ||
| 28 | + formActionType: { getFieldsValue, validate, setFieldsValue, clearValidate }, | ||
| 29 | + }); | ||
| 30 | +</script> | ||
| 31 | + | ||
| 32 | +<template> | ||
| 33 | + <div class="w-full flex-1"> | ||
| 34 | + <BasicForm | ||
| 35 | + :ref="(el) => setFormEl(el)" | ||
| 36 | + :schemas="mapFormSchema" | ||
| 37 | + class="w-full flex-1 data-source-form" | ||
| 38 | + :show-action-button-group="false" | ||
| 39 | + :row-props="{ | ||
| 40 | + gutter: 10, | ||
| 41 | + }" | ||
| 42 | + layout="horizontal" | ||
| 43 | + :label-col="{ span: 0 }" | ||
| 44 | + /> | ||
| 45 | + </div> | ||
| 46 | +</template> | 
| @@ -2,10 +2,13 @@ import { Component } from 'vue'; | @@ -2,10 +2,13 @@ import { Component } from 'vue'; | ||
| 2 | import { FrontComponent } from '../../../const/const'; | 2 | import { FrontComponent } from '../../../const/const'; | 
| 3 | import BasicDataSourceForm from './BasicDataSourceForm.vue'; | 3 | import BasicDataSourceForm from './BasicDataSourceForm.vue'; | 
| 4 | import ControlDataSourceForm from './ControlDataSourceForm.vue'; | 4 | import ControlDataSourceForm from './ControlDataSourceForm.vue'; | 
| 5 | +import MapDataSourceForm from './MapDataSourceForm.vue'; | ||
| 5 | 6 | ||
| 6 | const dataSourceComponentMap = new Map<FrontComponent, Component>(); | 7 | const dataSourceComponentMap = new Map<FrontComponent, Component>(); | 
| 7 | 8 | ||
| 8 | dataSourceComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, ControlDataSourceForm); | 9 | dataSourceComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, ControlDataSourceForm); | 
| 10 | +dataSourceComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_REAL, MapDataSourceForm); | ||
| 11 | +dataSourceComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_HISTORY, MapDataSourceForm); | ||
| 9 | 12 | ||
| 10 | export const getDataSourceComponent = (frontId: FrontComponent) => { | 13 | export const getDataSourceComponent = (frontId: FrontComponent) => { | 
| 11 | if (dataSourceComponentMap.has(frontId)) return dataSourceComponentMap.get(frontId)!; | 14 | if (dataSourceComponentMap.has(frontId)) return dataSourceComponentMap.get(frontId)!; | 
| @@ -2,6 +2,8 @@ import { getAllDeviceByOrg, getDeviceAttributes, getGatewaySlaveDevice } from '/ | @@ -2,6 +2,8 @@ import { getAllDeviceByOrg, getDeviceAttributes, getGatewaySlaveDevice } from '/ | ||
| 2 | import { getOrganizationList } from '/@/api/system/system'; | 2 | import { getOrganizationList } from '/@/api/system/system'; | 
| 3 | import { FormSchema } from '/@/components/Form'; | 3 | import { FormSchema } from '/@/components/Form'; | 
| 4 | import { copyTransFun } from '/@/utils/fnUtils'; | 4 | import { copyTransFun } from '/@/utils/fnUtils'; | 
| 5 | +import { OnChangeHookParams } from '/@/components/Form/src/components/ApiSearchSelect.vue'; | ||
| 6 | +import { unref } from 'vue'; | ||
| 5 | 7 | ||
| 6 | export enum BasicConfigField { | 8 | export enum BasicConfigField { | 
| 7 | NAME = 'name', | 9 | NAME = 'name', | 
| @@ -25,6 +27,8 @@ export enum DataSourceField { | @@ -25,6 +27,8 @@ export enum DataSourceField { | ||
| 25 | ATTRIBUTE_RENAME = 'attributeRename', | 27 | ATTRIBUTE_RENAME = 'attributeRename', | 
| 26 | DEVICE_NAME = 'deviceName', | 28 | DEVICE_NAME = 'deviceName', | 
| 27 | DEVICE_RENAME = 'deviceRename', | 29 | DEVICE_RENAME = 'deviceRename', | 
| 30 | + LONGITUDE_ATTRIBUTE = 'longitudeAttribute', | ||
| 31 | + LATITUDE_ATTRIBUTE = 'latitudeAttribute', | ||
| 28 | } | 32 | } | 
| 29 | 33 | ||
| 30 | export const basicSchema: FormSchema[] = [ | 34 | export const basicSchema: FormSchema[] = [ | 
| @@ -238,3 +242,198 @@ export const controlFormSchema: FormSchema[] = [ | @@ -238,3 +242,198 @@ export const controlFormSchema: FormSchema[] = [ | ||
| 238 | }, | 242 | }, | 
| 239 | }, | 243 | }, | 
| 240 | ]; | 244 | ]; | 
| 245 | + | ||
| 246 | +export const mapFormSchema: FormSchema[] = [ | ||
| 247 | + { | ||
| 248 | + field: DataSourceField.IS_GATEWAY_DEVICE, | ||
| 249 | + component: 'Switch', | ||
| 250 | + label: '是否是网关设备', | ||
| 251 | + show: false, | ||
| 252 | + }, | ||
| 253 | + { | ||
| 254 | + field: DataSourceField.DEVICE_NAME, | ||
| 255 | + component: 'Input', | ||
| 256 | + label: '设备名', | ||
| 257 | + show: false, | ||
| 258 | + }, | ||
| 259 | + { | ||
| 260 | + field: DataSourceField.ORIGINATION_ID, | ||
| 261 | + component: 'ApiTreeSelect', | ||
| 262 | + label: '组织', | ||
| 263 | + colProps: { span: 8 }, | ||
| 264 | + rules: [{ required: true, message: '组织为必填项' }], | ||
| 265 | + componentProps({ formActionType }) { | ||
| 266 | + const { setFieldsValue } = formActionType; | ||
| 267 | + return { | ||
| 268 | + placeholder: '请选择组织', | ||
| 269 | + api: async () => { | ||
| 270 | + const data = await getOrganizationList(); | ||
| 271 | + copyTransFun(data as any as any[]); | ||
| 272 | + return data; | ||
| 273 | + }, | ||
| 274 | + onChange() { | ||
| 275 | + setFieldsValue({ | ||
| 276 | + [DataSourceField.DEVICE_ID]: null, | ||
| 277 | + [DataSourceField.LATITUDE_ATTRIBUTE]: null, | ||
| 278 | + [DataSourceField.LONGITUDE_ATTRIBUTE]: null, | ||
| 279 | + [DataSourceField.SLAVE_DEVICE_ID]: null, | ||
| 280 | + [DataSourceField.IS_GATEWAY_DEVICE]: false, | ||
| 281 | + }); | ||
| 282 | + }, | ||
| 283 | + getPopupContainer: () => document.body, | ||
| 284 | + }; | ||
| 285 | + }, | ||
| 286 | + }, | ||
| 287 | + { | ||
| 288 | + field: DataSourceField.DEVICE_ID, | ||
| 289 | + component: 'ApiSelect', | ||
| 290 | + label: '设备', | ||
| 291 | + colProps: { span: 8 }, | ||
| 292 | + rules: [{ required: true, message: '设备名称为必填项' }], | ||
| 293 | + componentProps({ formModel, formActionType }) { | ||
| 294 | + const { setFieldsValue } = formActionType; | ||
| 295 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | ||
| 296 | + return { | ||
| 297 | + api: async () => { | ||
| 298 | + if (organizationId) { | ||
| 299 | + try { | ||
| 300 | + const data = await getAllDeviceByOrg(organizationId); | ||
| 301 | + if (data) | ||
| 302 | + return data.map((item) => ({ | ||
| 303 | + label: item.name, | ||
| 304 | + value: item.id, | ||
| 305 | + deviceType: item.deviceType, | ||
| 306 | + })); | ||
| 307 | + } catch (error) {} | ||
| 308 | + } | ||
| 309 | + return []; | ||
| 310 | + }, | ||
| 311 | + onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) { | ||
| 312 | + setFieldsValue({ | ||
| 313 | + [DataSourceField.LONGITUDE_ATTRIBUTE]: null, | ||
| 314 | + [DataSourceField.LATITUDE_ATTRIBUTE]: null, | ||
| 315 | + [DataSourceField.IS_GATEWAY_DEVICE]: record?.deviceType === 'GATEWAY', | ||
| 316 | + [DataSourceField.SLAVE_DEVICE_ID]: null, | ||
| 317 | + [DataSourceField.DEVICE_NAME]: record?.label, | ||
| 318 | + }); | ||
| 319 | + }, | ||
| 320 | + placeholder: '请选择设备', | ||
| 321 | + getPopupContainer: () => document.body, | ||
| 322 | + }; | ||
| 323 | + }, | ||
| 324 | + }, | ||
| 325 | + { | ||
| 326 | + field: DataSourceField.SLAVE_DEVICE_ID, | ||
| 327 | + label: '网关子设备', | ||
| 328 | + component: 'ApiSelect', | ||
| 329 | + colProps: { span: 8 }, | ||
| 330 | + rules: [{ required: true, message: '网关子设备为必填项' }], | ||
| 331 | + ifShow({ model }) { | ||
| 332 | + return model[DataSourceField.IS_GATEWAY_DEVICE]; | ||
| 333 | + }, | ||
| 334 | + dynamicRules({ model }) { | ||
| 335 | + return [{ required: model[DataSourceField.IS_GATEWAY_DEVICE], message: '请选择网关子设备' }]; | ||
| 336 | + }, | ||
| 337 | + componentProps({ formModel, formActionType }) { | ||
| 338 | + const { setFieldsValue } = formActionType; | ||
| 339 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | ||
| 340 | + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE]; | ||
| 341 | + const deviceId = formModel[DataSourceField.DEVICE_ID]; | ||
| 342 | + return { | ||
| 343 | + api: async () => { | ||
| 344 | + if (organizationId && isGatewayDevice) { | ||
| 345 | + try { | ||
| 346 | + const data = await getGatewaySlaveDevice({ organizationId, masterId: deviceId }); | ||
| 347 | + if (data) | ||
| 348 | + return data.map((item) => ({ | ||
| 349 | + label: item.name, | ||
| 350 | + value: item.id, | ||
| 351 | + deviceType: item.deviceType, | ||
| 352 | + })); | ||
| 353 | + } catch (error) {} | ||
| 354 | + } | ||
| 355 | + return []; | ||
| 356 | + }, | ||
| 357 | + onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) { | ||
| 358 | + setFieldsValue({ | ||
| 359 | + [DataSourceField.LATITUDE_ATTRIBUTE]: null, | ||
| 360 | + [DataSourceField.LONGITUDE_ATTRIBUTE]: null, | ||
| 361 | + [DataSourceField.DEVICE_NAME]: record?.label, | ||
| 362 | + }); | ||
| 363 | + }, | ||
| 364 | + placeholder: '请选择网关子设备', | ||
| 365 | + getPopupContainer: () => document.body, | ||
| 366 | + }; | ||
| 367 | + }, | ||
| 368 | + }, | ||
| 369 | + { | ||
| 370 | + field: DataSourceField.LONGITUDE_ATTRIBUTE, | ||
| 371 | + component: 'ApiSearchSelect', | ||
| 372 | + label: '经度属性', | ||
| 373 | + colProps: { span: 8 }, | ||
| 374 | + rules: [{ required: true, message: '属性为必填项' }], | ||
| 375 | + componentProps({ formModel }) { | ||
| 376 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | ||
| 377 | + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE]; | ||
| 378 | + const deviceId = formModel[DataSourceField.DEVICE_ID]; | ||
| 379 | + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID]; | ||
| 380 | + return { | ||
| 381 | + api: async () => { | ||
| 382 | + if (organizationId && deviceId) { | ||
| 383 | + try { | ||
| 384 | + if (isGatewayDevice && slaveDeviceId) { | ||
| 385 | + return await getDeviceAttribute(slaveDeviceId); | ||
| 386 | + } | ||
| 387 | + if (!isGatewayDevice) { | ||
| 388 | + return await getDeviceAttribute(deviceId); | ||
| 389 | + } | ||
| 390 | + } catch (error) {} | ||
| 391 | + } | ||
| 392 | + return []; | ||
| 393 | + }, | ||
| 394 | + placeholder: '请选择经度属性', | ||
| 395 | + dropdownVisibleChangeHook: ({ options }: OnChangeHookParams) => { | ||
| 396 | + options.value = unref(options).filter( | ||
| 397 | + (item) => item.value !== formModel[DataSourceField.LATITUDE_ATTRIBUTE] | ||
| 398 | + ); | ||
| 399 | + }, | ||
| 400 | + getPopupContainer: () => document.body, | ||
| 401 | + }; | ||
| 402 | + }, | ||
| 403 | + }, | ||
| 404 | + { | ||
| 405 | + field: DataSourceField.LATITUDE_ATTRIBUTE, | ||
| 406 | + component: 'ApiSearchSelect', | ||
| 407 | + label: '纬度属性', | ||
| 408 | + colProps: { span: 8 }, | ||
| 409 | + rules: [{ required: true, message: '属性为必填项' }], | ||
| 410 | + componentProps({ formModel }) { | ||
| 411 | + const organizationId = formModel[DataSourceField.ORIGINATION_ID]; | ||
| 412 | + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE]; | ||
| 413 | + const deviceId = formModel[DataSourceField.DEVICE_ID]; | ||
| 414 | + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID]; | ||
| 415 | + return { | ||
| 416 | + api: async () => { | ||
| 417 | + if (organizationId && deviceId) { | ||
| 418 | + try { | ||
| 419 | + if (isGatewayDevice && slaveDeviceId) { | ||
| 420 | + return getDeviceAttribute(slaveDeviceId); | ||
| 421 | + } | ||
| 422 | + if (!isGatewayDevice) { | ||
| 423 | + return await getDeviceAttribute(deviceId); | ||
| 424 | + } | ||
| 425 | + } catch (error) {} | ||
| 426 | + } | ||
| 427 | + return []; | ||
| 428 | + }, | ||
| 429 | + dropdownVisibleChangeHook: ({ options }: OnChangeHookParams) => { | ||
| 430 | + options.value = unref(options).filter( | ||
| 431 | + (item) => item.value !== formModel[DataSourceField.LONGITUDE_ATTRIBUTE] | ||
| 432 | + ); | ||
| 433 | + }, | ||
| 434 | + placeholder: '请选择纬度属性', | ||
| 435 | + getPopupContainer: () => document.body, | ||
| 436 | + }; | ||
| 437 | + }, | ||
| 438 | + }, | ||
| 439 | +]; | 
| @@ -45,12 +45,17 @@ | @@ -45,12 +45,17 @@ | ||
| 45 | import backIcon from '/@/assets/images/back.png'; | 45 | import backIcon from '/@/assets/images/back.png'; | 
| 46 | import { useCalcGridLayout } from '../hook/useCalcGridLayout'; | 46 | import { useCalcGridLayout } from '../hook/useCalcGridLayout'; | 
| 47 | import { FrontComponent } from '../const/const'; | 47 | import { FrontComponent } from '../const/const'; | 
| 48 | + import { useScript } from '/@/hooks/web/useScript'; | ||
| 49 | + import { BAI_DU_MAP_GL_LIB, BAI_DU_MAP_TRACK_ANIMATION } from '/@/utils/fnUtils'; | ||
| 48 | 50 | ||
| 49 | const ROUTE = useRoute(); | 51 | const ROUTE = useRoute(); | 
| 50 | 52 | ||
| 51 | const ROUTER = useRouter(); | 53 | const ROUTER = useRouter(); | 
| 52 | 54 | ||
| 53 | - // unref(ROUTE).name = unref(ROUTE).fullPath; | 55 | + const { toPromise: injectBaiDuMapLib } = useScript({ src: BAI_DU_MAP_GL_LIB }); | 
| 56 | + const { toPromise: injectBaiDuMapTrackAniMationLib } = useScript({ | ||
| 57 | + src: BAI_DU_MAP_TRACK_ANIMATION, | ||
| 58 | + }); | ||
| 54 | 59 | ||
| 55 | const { createMessage, createConfirm } = useMessage(); | 60 | const { createMessage, createConfirm } = useMessage(); | 
| 56 | 61 | ||
| @@ -370,7 +375,9 @@ | @@ -370,7 +375,9 @@ | ||
| 370 | historyDataModalMethod.openModal(true, record); | 375 | historyDataModalMethod.openModal(true, record); | 
| 371 | }; | 376 | }; | 
| 372 | 377 | ||
| 373 | - onMounted(() => { | 378 | + onMounted(async () => { | 
| 379 | + await injectBaiDuMapLib(); | ||
| 380 | + await injectBaiDuMapTrackAniMationLib(); | ||
| 374 | getDataBoardComponent(); | 381 | getDataBoardComponent(); | 
| 375 | }); | 382 | }); | 
| 376 | </script> | 383 | </script> | 
| @@ -566,4 +573,8 @@ | @@ -566,4 +573,8 @@ | ||
| 566 | .board-detail:deep(.ant-page-header-content) { | 573 | .board-detail:deep(.ant-page-header-content) { | 
| 567 | padding-top: 20px; | 574 | padding-top: 20px; | 
| 568 | } | 575 | } | 
| 576 | + | ||
| 577 | + :deep(.vue-resizable-handle) { | ||
| 578 | + z-index: 99; | ||
| 579 | + } | ||
| 569 | </style> | 580 | </style> | 
| @@ -17,12 +17,13 @@ interface SocketMessageItem { | @@ -17,12 +17,13 @@ interface SocketMessageItem { | ||
| 17 | keys: string; | 17 | keys: string; | 
| 18 | } | 18 | } | 
| 19 | 19 | ||
| 20 | -interface CmdMapping { | ||
| 21 | - componentId: string; | ||
| 22 | - deviceId: string; | 20 | +interface GroupMappingRecord { | 
| 21 | + id: string; | ||
| 23 | recordIndex: number; | 22 | recordIndex: number; | 
| 24 | dataSourceIndex: number; | 23 | dataSourceIndex: number; | 
| 25 | attribute: string; | 24 | attribute: string; | 
| 25 | + deviceId: string; | ||
| 26 | + slaveDeviceId: string; | ||
| 26 | } | 27 | } | 
| 27 | 28 | ||
| 28 | interface ResponseMessage { | 29 | interface ResponseMessage { | 
| @@ -50,7 +51,9 @@ const generateMessage = (deviceId: string, cmdId: number, attr: string): SocketM | @@ -50,7 +51,9 @@ const generateMessage = (deviceId: string, cmdId: number, attr: string): SocketM | ||
| 50 | export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | 51 | export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | 
| 51 | const token = getAuthCache(JWT_TOKEN_KEY); | 52 | const token = getAuthCache(JWT_TOKEN_KEY); | 
| 52 | 53 | ||
| 53 | - const cmdIdMapping = new Map<number, CmdMapping>(); | 54 | + const cmdIdMapping = new Map<number, GroupMappingRecord[]>(); | 
| 55 | + | ||
| 56 | + const groupMapping = new Map<string, GroupMappingRecord[]>(); | ||
| 54 | 57 | ||
| 55 | const waitSendQueue: string[] = []; | 58 | const waitSendQueue: string[] = []; | 
| 56 | 59 | ||
| @@ -64,6 +67,44 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | @@ -64,6 +67,44 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | ||
| 64 | return unref(dataSourceRef)[recordIndex].record.dataSource[dataSourceIndex]; | 67 | return unref(dataSourceRef)[recordIndex].record.dataSource[dataSourceIndex]; | 
| 65 | }; | 68 | }; | 
| 66 | 69 | ||
| 70 | + const mergeGroup = (dataSourceRef: Ref<DataBoardLayoutInfo[]>) => { | ||
| 71 | + for (let recordIndex = 0; recordIndex < unref(dataSourceRef).length; recordIndex++) { | ||
| 72 | + const record = unref(dataSourceRef).at(recordIndex)!; | ||
| 73 | + const dataSource = record?.record.dataSource; | ||
| 74 | + for (let dataSourceIndex = 0; dataSourceIndex < dataSource.length; dataSourceIndex++) { | ||
| 75 | + const dataDatum = dataSource.at(dataSourceIndex)!; | ||
| 76 | + const { deviceId, slaveDeviceId, attribute } = dataDatum; | ||
| 77 | + const groupMappingRecord: GroupMappingRecord = { | ||
| 78 | + id: record.record.id, | ||
| 79 | + recordIndex, | ||
| 80 | + dataSourceIndex, | ||
| 81 | + attribute, | ||
| 82 | + deviceId, | ||
| 83 | + slaveDeviceId, | ||
| 84 | + }; | ||
| 85 | + if (groupMapping.has(slaveDeviceId || deviceId)) { | ||
| 86 | + const group = groupMapping.get(slaveDeviceId || deviceId); | ||
| 87 | + group?.push(groupMappingRecord); | ||
| 88 | + } else { | ||
| 89 | + groupMapping.set(slaveDeviceId || deviceId, [groupMappingRecord]); | ||
| 90 | + } | ||
| 91 | + } | ||
| 92 | + } | ||
| 93 | + }; | ||
| 94 | + | ||
| 95 | + function generateGroupMessage() { | ||
| 96 | + const messageList: SocketMessageItem[] = []; | ||
| 97 | + let cmdId = 0; | ||
| 98 | + groupMapping.forEach((value, key) => { | ||
| 99 | + const message = generateMessage(key, cmdId, value.map((item) => item.attribute).join(',')); | ||
| 100 | + messageList.push(message); | ||
| 101 | + setCmdId(cmdId, value); | ||
| 102 | + cmdId++; | ||
| 103 | + }); | ||
| 104 | + console.log(cmdIdMapping); | ||
| 105 | + return messageList; | ||
| 106 | + } | ||
| 107 | + | ||
| 67 | const { close, send, open, status } = useWebSocket(config.server, { | 108 | const { close, send, open, status } = useWebSocket(config.server, { | 
| 68 | onConnected() { | 109 | onConnected() { | 
| 69 | if (waitSendQueue.length) { | 110 | if (waitSendQueue.length) { | 
| @@ -80,11 +121,14 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | @@ -80,11 +121,14 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | ||
| 80 | if (isNullAndUnDef(subscriptionId)) return; | 121 | if (isNullAndUnDef(subscriptionId)) return; | 
| 81 | const mappingRecord = cmdIdMapping.get(subscriptionId); | 122 | const mappingRecord = cmdIdMapping.get(subscriptionId); | 
| 82 | if (!mappingRecord) return; | 123 | if (!mappingRecord) return; | 
| 83 | - const { attribute, recordIndex, dataSourceIndex } = mappingRecord; | ||
| 84 | - const [[timespan, value]] = data[attribute]; | ||
| 85 | - const record = getNeedUpdateValueByIndex(recordIndex, dataSourceIndex); | ||
| 86 | - record.componentInfo.value = value; | ||
| 87 | - record.componentInfo.updateTime = timespan; | 124 | + mappingRecord.forEach((item) => { | 
| 125 | + const { attribute, recordIndex, dataSourceIndex } = item; | ||
| 126 | + const [[timespan, value]] = data[attribute]; | ||
| 127 | + const record = getNeedUpdateValueByIndex(recordIndex, dataSourceIndex); | ||
| 128 | + record.componentInfo.value = value; | ||
| 129 | + record.componentInfo.updateTime = timespan; | ||
| 130 | + }); | ||
| 131 | + return; | ||
| 88 | } catch (error) { | 132 | } catch (error) { | 
| 89 | throw Error(error as string); | 133 | throw Error(error as string); | 
| 90 | } | 134 | } | 
| @@ -94,40 +138,14 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | @@ -94,40 +138,14 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) { | ||
| 94 | // }, | 138 | // }, | 
| 95 | }); | 139 | }); | 
| 96 | 140 | ||
| 97 | - const setCmdId = (cmdId: number, record: CmdMapping) => { | 141 | + const setCmdId = (cmdId: number, record: GroupMappingRecord[]) => { | 
| 98 | cmdIdMapping.set(cmdId, record); | 142 | cmdIdMapping.set(cmdId, record); | 
| 99 | }; | 143 | }; | 
| 100 | 144 | ||
| 101 | const transformSocketMessageItem = () => { | 145 | const transformSocketMessageItem = () => { | 
| 102 | - const messageList: SocketMessageItem[] = []; | ||
| 103 | - let index = 0; | ||
| 104 | - unref(dataSourceRef).forEach((record, recordIndex) => { | ||
| 105 | - const componentId = record.record.id; | ||
| 106 | - for ( | ||
| 107 | - let dataSourceIndex = 0; | ||
| 108 | - dataSourceIndex < record.record.dataSource.length; | ||
| 109 | - dataSourceIndex++ | ||
| 110 | - ) { | ||
| 111 | - const dataSource = record.record.dataSource[dataSourceIndex]; | ||
| 112 | - const { deviceId, attribute, slaveDeviceId, gatewayDevice } = dataSource; | ||
| 113 | - if (!attribute) continue; | ||
| 114 | - const cmdId = index; | ||
| 115 | - index++; | ||
| 116 | - setCmdId(cmdId, { | ||
| 117 | - componentId, | ||
| 118 | - deviceId: gatewayDevice ? deviceId : slaveDeviceId, | ||
| 119 | - recordIndex, | ||
| 120 | - dataSourceIndex, | ||
| 121 | - attribute, | ||
| 122 | - }); | ||
| 123 | - | ||
| 124 | - messageList.push( | ||
| 125 | - generateMessage(gatewayDevice ? slaveDeviceId : deviceId, cmdId, attribute) | ||
| 126 | - ); | ||
| 127 | - } | ||
| 128 | - }); | 146 | + mergeGroup(dataSourceRef); | 
| 129 | return { | 147 | return { | 
| 130 | - tsSubCmds: messageList, | 148 | + tsSubCmds: generateGroupMessage(), | 
| 131 | } as SocketMessage; | 149 | } as SocketMessage; | 
| 132 | }; | 150 | }; | 
| 133 | 151 | 
| 1 | +import { inject, provide } from 'vue'; | ||
| 2 | +import { UpdateCenter } from './useUpdateCenter'; | ||
| 3 | + | ||
| 4 | +const key = Symbol('visual-board-content'); | ||
| 5 | + | ||
| 6 | +type Instance = UpdateCenter; | ||
| 7 | + | ||
| 8 | +export function createVisualBoardContext(instance: Instance) { | ||
| 9 | + provide(key, instance); | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +export function useVisualBoardContext() { | ||
| 13 | + return inject(key) as Instance; | ||
| 14 | +} |