Commit ac9c92b5e7492f3e7ecfe59e9f71d815f58268c8

Authored by fengtao
2 parents 6d4ce14f f518f914

Merge branch 'main' into ft_local_dev

Showing 31 changed files with 1672 additions and 158 deletions
  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 +};
... ...
  1 +import { ALG, CheckSumWay } from '/@/views/operation/ota/config/packageDetail.config';
  2 +
  3 +export interface GetOtaPackagesParams {
  4 + pageSize: number;
  5 + page: number;
  6 + textSearch?: string;
  7 + title?: string;
  8 +}
  9 +
  10 +export interface CreateOtaPackagesParams {
  11 + additionalInfo?: { description?: string };
  12 + checksum?: Nullable<number>;
  13 + checksumAlgorithm?: ALG;
  14 + deviceProfileId?: {
  15 + entityType?: string;
  16 + id?: string;
  17 + };
  18 + isURL: boolean;
  19 + tag?: string;
  20 + title?: string;
  21 + type?: CheckSumWay;
  22 + url?: string;
  23 + version?: string;
  24 +}
  25 +
  26 +export interface UploadOtaPackagesParams {
  27 + otaPackageId?: string;
  28 + checksum?: string;
  29 + checksumAlgorithm?: ALG;
  30 + file?: FormData;
  31 +}
  32 +
  33 +export interface Id {
  34 + entityType: string;
  35 + id: string;
  36 +}
  37 +
  38 +export interface AdditionalInfo {
  39 + description: string;
  40 +}
  41 +
  42 +export interface IdRecord {
  43 + entityType: string;
  44 + id: string;
  45 +}
  46 +
  47 +export interface OtaRecordDatum {
  48 + id: Id;
  49 + createdTime: any;
  50 + additionalInfo: AdditionalInfo;
  51 + tenantId: IdRecord;
  52 + deviceProfileId: IdRecord;
  53 + type: string;
  54 + title: string;
  55 + version: string;
  56 + tag: string;
  57 + url: string;
  58 + hasData: boolean;
  59 + fileName: string;
  60 + contentType: string;
  61 + checksumAlgorithm: string;
  62 + checksum: string;
  63 + dataSize?: number;
  64 +}
  65 +
  66 +export interface TBResponse<T> {
  67 + data: T[];
  68 + totalPages: number;
  69 + totalElements: number;
  70 + hasNext: boolean;
  71 +}
  72 +
  73 +export interface DefaultDeviceProfileInfo {
  74 + id: {
  75 + entityType: string;
  76 + id: string;
  77 + };
  78 +}
  79 +
  80 +export interface CreateOtaPackageParams {
  81 + title: string;
  82 + version: string;
  83 + tag: string;
  84 + type: string;
  85 + deviceProfileId: IdRecord;
  86 + isURL: boolean;
  87 + additionalInfo: AdditionalInfo;
  88 +}
  89 +
  90 +export interface GetDeviceProfileInfosParams {
  91 + pageSize?: number;
  92 + page?: number;
  93 + textSearch?: string;
  94 + sortProperty?: string;
  95 + sortOrder?: 'ASC' | 'DESC';
  96 +}
  97 +
  98 +export interface DeviceProfileRecord {
  99 + id: Id;
  100 + name: string;
  101 + image: string;
  102 + defaultDashboardId?: any;
  103 + type: string;
  104 + transportType: string;
  105 +}
... ...
... ... @@ -34,6 +34,7 @@ import { JEasyCron } from './externalCompns/components/JEasyCron';
34 34 import ColorPicker from './components/ColorPicker.vue';
35 35 import IconDrawer from './components/IconDrawer.vue';
36 36 import ApiUpload from './components/ApiUpload.vue';
  37 +import ApiSearchSelect from './components/ApiSearchSelect.vue';
37 38
38 39 const componentMap = new Map<ComponentType, Component>();
39 40
... ... @@ -75,6 +76,7 @@ componentMap.set('JEasyCron', JEasyCron);
75 76 componentMap.set('ColorPicker', ColorPicker);
76 77 componentMap.set('IconDrawer', IconDrawer);
77 78 componentMap.set('ApiUpload', ApiUpload);
  79 +componentMap.set('ApiSearchSelect', ApiSearchSelect);
78 80
79 81 export function add(compName: ComponentType, component: Component) {
80 82 componentMap.set(compName, component);
... ...
  1 +<script lang="ts">
  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>
... ...
... ... @@ -115,4 +115,5 @@ export type ComponentType =
115 115 | 'Rate'
116 116 | 'ColorPicker'
117 117 | 'IconDrawer'
118   - | 'ApiUpload';
  118 + | 'ApiUpload'
  119 + | 'ApiSearchSelect';
... ...
  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 37 };
38 38
39 39 // 百度地图url
  40 +const ak = '7uOPPyAHn2Y2ZryeQqHtcRqtIY374vKa';
  41 +
40 42 const register_BAI_DU_MAP_URL = (ak: string) => {
41 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 58 export function toThousands(num) {
... ...
... ... @@ -2,7 +2,7 @@
2 2 import moment from 'moment';
3 3 import { nextTick, onMounted, onUnmounted, Ref, ref, unref } from 'vue';
4 4 import { getDeviceDataKeys, getDeviceHistoryInfo } from '/@/api/alarm/position';
5   - import { Empty } from 'ant-design-vue';
  5 + import { Empty, Spin } from 'ant-design-vue';
6 6 import { useECharts } from '/@/hooks/web/useECharts';
7 7 import { dateUtil } from '/@/utils/dateUtil';
8 8 import {
... ... @@ -175,7 +175,7 @@
175 175 </section>
176 176 <section class="bg-white p-3">
177 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 179 </div>
180 180 <Empty v-show="!isNull" />
181 181 </section>
... ...
1 1 <script lang="ts" setup>
2 2 import { BasicModal, useModalInner } from '/@/components/Modal';
3 3 import { BasicForm, useForm } from '/@/components/Form';
4   - import { formSchema } from '../config/packageDetail.config';
5   - defineEmits(['register']);
  4 + import { ALG, formSchema, PackageField } from '../config/packageDetail.config';
  5 + import { ref } from 'vue';
  6 + import { 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 27 schemas: formSchema,
11 28 showActionButtonGroup: false,
12 29 // labelCol: { span: 8 },
13 30 labelWidth: 100,
14 31 wrapperCol: { span: 16 },
15 32 });
  33 +
  34 + const handleUploadFile = async (value: FieldsValue, id: string) => {
  35 + try {
  36 + const file = new FormData();
  37 + file.append('file', value.fileList.at(0)!.file);
  38 + await uploadOtaPackages({
  39 + otaPackageId: id,
  40 + file,
  41 + checksumAlgorithm: value.checksumAlgorithm,
  42 + });
  43 + } catch (error) {
  44 + console.log(error);
  45 + if ((error as { status: number }).status) {
  46 + await deleteOtaPackage(id);
  47 + }
  48 + } finally {
  49 + closeModal();
  50 + emit('update:list');
  51 + }
  52 + };
  53 +
  54 + const handleCreatePackages = async (value: FieldsValue) => {
  55 + try {
  56 + setLoading(true);
  57 + const { id } = await 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 81 </script>
17 82
18 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 92 </BasicModal>
22 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 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 18 export const columns: BasicColumn[] = [
4 19 {
5 20 title: '创建时间',
6 21 dataIndex: PackageField.CREATE_TIME,
  22 + format(text) {
  23 + return dateUtil(text).format(DEFAULT_DATE_FORMAT);
  24 + },
7 25 width: 120,
8 26 },
9 27 {
... ... @@ -18,29 +36,54 @@ export const columns: BasicColumn[] = [
18 36 },
19 37 {
20 38 title: '版本标签',
21   - dataIndex: PackageField.VERSION_LABEL,
  39 + dataIndex: PackageField.VERSION_TAG,
22 40 width: 120,
23 41 },
24 42 {
25 43 title: '包类型',
26 44 dataIndex: PackageField.PACKAGE_TYPE,
  45 + format: (text) => {
  46 + return text === PackageType.FIRMWARE ? '固件' : '软件';
  47 + },
27 48 width: 120,
28 49 },
29 50 {
30 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 58 width: 120,
33 59 },
34 60 {
35 61 title: '文件大小',
36 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 68 width: 120,
38 69 },
39 70 {
40 71 title: '校验和',
41 72 dataIndex: PackageField.CHECK_SUM,
  73 + format(text, record) {
  74 + return text ? `${record[PackageField.CHECK_SUM_ALG]}: ${text.slice(0, 11)}` : '';
  75 + },
42 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 89 export const searchFormSchema: FormSchema[] = [
... ...
  1 +import { getDefaultDeviceProfile, getDeviceProfileInfos } from '/@/api/ota';
  2 +import { Id } from '/@/api/ota/model';
1 3 import { FormSchema } from '/@/components/Form';
  4 +import { FileItem } from '/@/components/Form/src/components/ApiUpload.vue';
2 5
3 6 export enum PackageField {
4 7 TITLE = 'title',
5 8 VERSION = 'version',
6   - VERSION_LABEL = 'versionLabel',
7   - DEVICE_CONFIGURATION = 'deviceConfiguration',
8   - PACKAGE_TYPE = 'packageType',
9   - PACKAGE_UPDATE_TYPE = 'PackageUpdateType',
10   - PACKAGE_BINARY_FILE = 'fileList',
11   - PACKAGE_EXTERNAL_URL = 'packageEexternalUrl',
12   - CHECK_SUM_WAY = 'checkSumWay',
13   - ALG = 'alg',
14   - CHECK_SUM = 'checkSum',
  9 + VERSION_TAG = 'tag',
  10 + PACKAGE_TYPE = 'type',
  11 + URL = 'url',
  12 + IS_URL = 'isUrl',
  13 + CHECK_SUM_ALG = 'checksumAlgorithm',
  14 + CHECK_SUM = 'checksum',
15 15 DESCRIPTION = 'description',
  16 + DEVICE_PROFILE_INFO = 'deviceProfileId',
16 17
17   - CREATE_TIME = 'createTIme',
18   - FILE_SIZE = 'fileSize',
19   -}
  18 + CREATE_TIME = 'createdTime',
  19 + FILE_SIZE = 'dataSize',
  20 + FILE_NAME = 'fileName',
  21 + ADDITIONAL_INFO = 'additionalInfo',
  22 + FILE_TYPE = 'contentType',
20 23
21   -export enum PackageUpdateType {
22   - BINARY_FILE = 'binaryFile',
23   - EXTERNAL_URL = 'externalUrl',
  24 + PACKAGE_BINARY_FILE = 'fileList',
  25 + VALIDATE_WAY = 'validateWay',
24 26 }
25 27
26 28 export enum PackageType {
27   - FIRMWARE = 'firmware',
28   - SOFTWARE = 'software',
  29 + FIRMWARE = 'FIRMWARE',
  30 + SOFTWARE = 'SOFTWARE',
29 31 }
30 32
31 33 export enum CheckSumWay {
... ... @@ -34,23 +36,38 @@ export enum CheckSumWay {
34 36 }
35 37
36 38 export enum ALG {
37   - MD5 = 'md5',
38   - SHA_256 = 'sha-256',
39   - SHA_384 = 'sha-384',
40   - SHA_512 = 'sha-512',
41   - CRC_32 = 'crc-32',
42   - MURMUR3_32 = 'murmur3-32',
43   - MURMUR3_128 = 'murmur3-128',
  39 + MD5 = 'MD$',
  40 + SHA_256 = 'SHA256',
  41 + SHA_384 = 'SHA384',
  42 + SHA_512 = 'SHA512',
  43 + CRC_32 = 'CRC32',
  44 + MURMUR3_32 = 'MURMUR332',
  45 + MURMUR3_128 = 'MURMUR3128',
44 46 }
45 47
  48 +const getVersionTag = (title: string, version: string) => {
  49 + return `${title ?? ''} ${version ?? ''}`;
  50 +};
  51 +
46 52 export const formSchema: FormSchema[] = [
47 53 {
48 54 field: PackageField.TITLE,
49 55 label: '标题',
50 56 component: 'Input',
51 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 75 label: '版本',
59 76 component: 'Input',
60 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 95 label: '版本标签',
68 96 component: 'Input',
69 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 106 label: '设备配置',
77   - component: 'Select',
  107 + component: 'ApiSearchSelect',
78 108 helpMessage: ['上传的包仅适用于具有所选配置文件的设备'],
79 109 defaultValue: 'default',
80 110 rules: [{ required: true, message: '设备配置为必填项' }],
81   - componentProps: () => {
  111 + componentProps: ({ formActionType }) => {
  112 + const { setFieldsValue } = formActionType;
82 113 return {
83   - options: [{ label: 'default', value: 'default' }],
84 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 151 },
104 152 },
105 153 {
106   - field: PackageField.PACKAGE_UPDATE_TYPE,
  154 + field: PackageField.IS_URL,
107 155 label: '上传方式',
108 156 component: 'RadioGroup',
109   - defaultValue: PackageUpdateType.BINARY_FILE,
  157 + defaultValue: false,
110 158 componentProps: () => {
111 159 return {
  160 + defaultValue: false,
112 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 169 field: PackageField.PACKAGE_BINARY_FILE,
121 170 label: '二进制文件',
122 171 ifShow: ({ model }) => {
123   - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.BINARY_FILE;
  172 + return !model[PackageField.IS_URL];
124 173 },
125 174 component: 'ApiUpload',
126 175 valueField: PackageField.PACKAGE_BINARY_FILE,
127 176 changeEvent: `update:${PackageField.PACKAGE_BINARY_FILE}`,
  177 + rules: [{ required: true, message: '请上传二进制文件', type: 'array' }],
128 178 componentProps: {
129 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 187 label: '外部URL',
139 188 component: 'Input',
140 189 ifShow: ({ model }) => {
141   - return model[PackageField.PACKAGE_UPDATE_TYPE] === PackageUpdateType.EXTERNAL_URL;
  190 + return model[PackageField.IS_URL];
142 191 },
143 192 rules: [{ required: true, message: '外部URL为必填项' }],
144 193 componentProps: {
... ... @@ -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 199 label: '校验和方式',
151 200 component: 'RadioGroup',
152 201 defaultValue: CheckSumWay.AUTO,
  202 + ifShow: ({ model }) => {
  203 + return !model[PackageField.IS_URL];
  204 + },
153 205 componentProps: () => {
154 206 return {
155 207 options: [
... ... @@ -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 216 label: '校验和算法',
165 217 component: 'Select',
166 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 222 componentProps: {
170 223 placeholder: '请选择校验和算法',
171 224 options: Object.keys(ALG).map((key) => {
... ... @@ -181,7 +234,7 @@ export const formSchema: FormSchema[] = [
181 234 label: '校验和',
182 235 component: 'Input',
183 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 239 helpMessage: ['如果校验和为空,会自动生成'],
187 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 +];
... ...
  1 +import { downloadPackage } from '/@/api/ota';
  2 +import { OtaRecordDatum } from '/@/api/ota/model';
  3 +
  4 +export async function useDownload(record: OtaRecordDatum) {
  5 + try {
  6 + if (record.url || !record.fileName) return;
  7 + const data = await downloadPackage(record.id.id);
  8 + const aEl = document.createElement('a');
  9 + const blob = new Blob([data], { type: record.contentType });
  10 + const objectUrl = URL.createObjectURL(blob);
  11 + aEl.href = objectUrl;
  12 + aEl.download = record.fileName;
  13 + aEl.click();
  14 + URL.revokeObjectURL(objectUrl);
  15 + } catch (error) {}
  16 +}
... ...
1 1 <script lang="ts" setup>
2 2 import { Button } from 'ant-design-vue';
3   - import { columns, searchFormSchema } from './config/config';
  3 + import { columns, ModalPassRecord, OtaPermissionKey, searchFormSchema } from './config/config';
4 4 import { PageWrapper } from '/@/components/Page';
5   - import { BasicTable, useTable } from '/@/components/Table';
  5 + import { BasicTable, useTable, TableAction } from '/@/components/Table';
6 6 import PackageDetailModal from './components/PackageDetailModal.vue';
7 7 import { useModal } from '/@/components/Modal';
  8 + import { deleteOtaPackage, getOtaPackagesList } from '/@/api/ota';
  9 + import { GetOtaPackagesParams, OtaRecordDatum } from '/@/api/ota/model/index';
  10 + import PackagesDetailDrawer from './components/PackagesDetailDrawer.vue';
  11 + import { useDrawer } from '/@/components/Drawer';
  12 + import { useMessage } from '/@/hooks/web/useMessage';
  13 + import { useDownload } from './hook/useDownload';
  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 19 columns,
11 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 37 formConfig: {
13 38 labelWidth: 120,
14 39 schemas: searchFormSchema,
15 40 },
  41 + rowKey: (record: OtaRecordDatum) => record.id.id,
  42 + showIndexColumn: false,
16 43 useSearchForm: true,
17 44 showTableSetting: true,
  45 + rowSelection: {
  46 + type: 'checkbox',
  47 + },
18 48 });
19 49
  50 + const { createConfirm, createMessage } = useMessage();
  51 +
20 52 const [registerModal, { openModal }] = useModal();
21 53
  54 + const [registerDrawer, { openDrawer }] = useDrawer();
  55 +
22 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 100 </script>
26 101
27 102 <template>
28 103 <PageWrapper dense contentFullHeight contentClass="flex flex-col">
29   - <BasicTable @register="register">
  104 + <BasicTable @register="register" @row-click="handleOpenDetailDrawer" class="ota-list">
30 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 137 </template>
33 138 </BasicTable>
34   - <PackageDetailModal @register="registerModal" />
  139 + <PackageDetailModal @register="registerModal" @update:list="reload" />
  140 + <PackagesDetailDrawer @register="registerDrawer" @update:list="reload" />
35 141 </PageWrapper>
36 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 5 </script>
6 6 <script lang="ts" setup>
7 7 import type { ECharts, EChartsOption } from 'echarts';
8   - import { PropType, watch } from 'vue';
  8 + import { watch } from 'vue';
9 9 import { nextTick, onMounted, onUnmounted, ref, unref, computed } from 'vue';
10 10 import { init } from 'echarts';
11 11 import {
... ... @@ -26,29 +26,23 @@
26 26 import { Tooltip } from 'ant-design-vue';
27 27 import { useThrottleFn } from '@vueuse/shared';
28 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 47 const getControlsWidgetId = () => `widget-chart-${props.value.id}`;
54 48
... ...
... ... @@ -261,7 +261,15 @@ const handleValue = (value: any) => {
261 261
262 262 export const update_instrument_1_value = (params: DashBoardValue) => {
263 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 273 max = value > max ? max * 2 : max;
266 274 return {
267 275 series: [
... ... @@ -288,8 +296,22 @@ export const update_instrument_2_value = (params: DashBoardValue) => {
288 296 const thirdRecord = getGradient(Gradient.THIRD, gradientInfo);
289 297
290 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 315 max = value > max ? max * 2 : max;
294 316
295 317 const firstGradient = firstRecord?.value ? firstRecord.value / max : 0.3;
... ...
... ... @@ -4,32 +4,162 @@
4 4 };
5 5 </script>
6 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 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 47 async function initMap() {
15   - await toPromise();
16   - await nextTick();
17 48 const wrapEl = unref(wrapRef);
18 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 117 onMounted(() => {
27 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 136 </script>
30 137
31 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 164 </div>
35 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 25 export const transfromMapComponentConfig: ComponentConfig['transformConfig'] = (
4   - _componentConfig,
  26 + componentConfig: Config,
5 27 _record,
6 28 _dataSourceRecord
7 29 ) => {
8   - return {};
  30 + return {
  31 + layout: {
  32 + ...componentConfig,
  33 + },
  34 + };
9 35 };
... ...
1 1 <script lang="ts" setup>
2 2 import { useUpdateCenter } from '../../hook/useUpdateCenter';
3 3 import { FrontDataSourceRecord } from '../../types/type';
  4 + import { createVisualBoardContext } from '../../hook/useVisualBoardContext';
4 5
5 6 const props = defineProps<{
6 7 dataSource: FrontDataSourceRecord[];
... ... @@ -8,6 +9,8 @@
8 9
9 10 const { update, add, remove } = useUpdateCenter();
10 11
  12 + createVisualBoardContext({ update, add, remove });
  13 +
11 14 defineExpose({ update });
12 15 </script>
13 16
... ...
... ... @@ -27,7 +27,11 @@ import ToggleSwitch from './ControlComponent/ToggleSwitch.vue';
27 27 import SlidingSwitch from './ControlComponent/SlidingSwitch.vue';
28 28 import SwitchWithIcon from './ControlComponent/SwitchWithIcon.vue';
29 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 35 import { ComponentConfig } from '../types/type';
32 36 import { FrontComponent, FrontComponentCategory } from '../const/const';
33 37
... ... @@ -135,10 +139,20 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, {
135 139 transformConfig: transformControlConfig,
136 140 });
137 141
138   -frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK, {
  142 +frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_REAL, {
139 143 Component: MapComponent,
140 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 156 ComponentCategory: FrontComponentCategory.MAP,
143 157 transformConfig: transfromMapComponentConfig,
144 158 });
... ...
... ... @@ -27,7 +27,8 @@ export enum FrontComponent {
27 27 CONTROL_COMPONENT_TOGGLE_SWITCH = 'control-component-toggle-switch',
28 28 CONTROL_COMPONENT_SWITCH_WITH_ICON = 'control-component-switch-with-icon',
29 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 34 export enum Gradient {
... ...
... ... @@ -17,7 +17,7 @@
17 17 :row-props="{
18 18 gutter: 10,
19 19 }"
20   - layout="inline"
  20 + layout="horizontal"
21 21 :label-col="{ span: 0 }"
22 22 />
23 23 </template>
... ...
... ... @@ -39,7 +39,7 @@
39 39 :row-props="{
40 40 gutter: 10,
41 41 }"
42   - layout="inline"
  42 + layout="horizontal"
43 43 :label-col="{ span: 0 }"
44 44 />
45 45 </div>
... ...
  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 2 import { FrontComponent } from '../../../const/const';
3 3 import BasicDataSourceForm from './BasicDataSourceForm.vue';
4 4 import ControlDataSourceForm from './ControlDataSourceForm.vue';
  5 +import MapDataSourceForm from './MapDataSourceForm.vue';
5 6
6 7 const dataSourceComponentMap = new Map<FrontComponent, Component>();
7 8
8 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 13 export const getDataSourceComponent = (frontId: FrontComponent) => {
11 14 if (dataSourceComponentMap.has(frontId)) return dataSourceComponentMap.get(frontId)!;
... ...
... ... @@ -2,6 +2,8 @@ import { getAllDeviceByOrg, getDeviceAttributes, getGatewaySlaveDevice } from '/
2 2 import { getOrganizationList } from '/@/api/system/system';
3 3 import { FormSchema } from '/@/components/Form';
4 4 import { copyTransFun } from '/@/utils/fnUtils';
  5 +import { OnChangeHookParams } from '/@/components/Form/src/components/ApiSearchSelect.vue';
  6 +import { unref } from 'vue';
5 7
6 8 export enum BasicConfigField {
7 9 NAME = 'name',
... ... @@ -25,6 +27,8 @@ export enum DataSourceField {
25 27 ATTRIBUTE_RENAME = 'attributeRename',
26 28 DEVICE_NAME = 'deviceName',
27 29 DEVICE_RENAME = 'deviceRename',
  30 + LONGITUDE_ATTRIBUTE = 'longitudeAttribute',
  31 + LATITUDE_ATTRIBUTE = 'latitudeAttribute',
28 32 }
29 33
30 34 export const basicSchema: 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 45 import backIcon from '/@/assets/images/back.png';
46 46 import { useCalcGridLayout } from '../hook/useCalcGridLayout';
47 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 51 const ROUTE = useRoute();
50 52
51 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 60 const { createMessage, createConfirm } = useMessage();
56 61
... ... @@ -370,7 +375,9 @@
370 375 historyDataModalMethod.openModal(true, record);
371 376 };
372 377
373   - onMounted(() => {
  378 + onMounted(async () => {
  379 + await injectBaiDuMapLib();
  380 + await injectBaiDuMapTrackAniMationLib();
374 381 getDataBoardComponent();
375 382 });
376 383 </script>
... ... @@ -566,4 +573,8 @@
566 573 .board-detail:deep(.ant-page-header-content) {
567 574 padding-top: 20px;
568 575 }
  576 +
  577 + :deep(.vue-resizable-handle) {
  578 + z-index: 99;
  579 + }
569 580 </style>
... ...
... ... @@ -17,12 +17,13 @@ interface SocketMessageItem {
17 17 keys: string;
18 18 }
19 19
20   -interface CmdMapping {
21   - componentId: string;
22   - deviceId: string;
  20 +interface GroupMappingRecord {
  21 + id: string;
23 22 recordIndex: number;
24 23 dataSourceIndex: number;
25 24 attribute: string;
  25 + deviceId: string;
  26 + slaveDeviceId: string;
26 27 }
27 28
28 29 interface ResponseMessage {
... ... @@ -50,7 +51,9 @@ const generateMessage = (deviceId: string, cmdId: number, attr: string): SocketM
50 51 export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) {
51 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 58 const waitSendQueue: string[] = [];
56 59
... ... @@ -64,6 +67,44 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) {
64 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 108 const { close, send, open, status } = useWebSocket(config.server, {
68 109 onConnected() {
69 110 if (waitSendQueue.length) {
... ... @@ -80,11 +121,14 @@ export function useSocketConnect(dataSourceRef: Ref<DataBoardLayoutInfo[]>) {
80 121 if (isNullAndUnDef(subscriptionId)) return;
81 122 const mappingRecord = cmdIdMapping.get(subscriptionId);
82 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 132 } catch (error) {
89 133 throw Error(error as string);
90 134 }
... ... @@ -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 142 cmdIdMapping.set(cmdId, record);
99 143 };
100 144
101 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 147 return {
130   - tsSubCmds: messageList,
  148 + tsSubCmds: generateGroupMessage(),
131 149 } as SocketMessage;
132 150 };
133 151
... ...
  1 +export type UpdateCenter = ReturnType<typeof useUpdateCenter>;
  2 +
1 3 export function useUpdateCenter() {
2 4 const eventCenter = new Map<string, Fn>();
3 5
... ...
  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 +}
... ...