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 | +} |