Commit fa8080ad9f58db95254f70f2d92f680ba578bdd7

Authored by xp.Huang
2 parents 81a1c4fd 09f45ebb

Merge branch 'ww' into 'main'

fix: BUG in teambition && implement data component map comoponent && implement data component control comoponent

See merge request huang/yun-teng-iot-front!407
Showing 40 changed files with 1114 additions and 656 deletions
@@ -33,3 +33,4 @@ pnpm-debug.log* @@ -33,3 +33,4 @@ pnpm-debug.log*
33 *.sw? 33 *.sw?
34 34
35 yarn.lock 35 yarn.lock
  36 +.vscode
@@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
5 "public/resource/tinymce/langs" 5 "public/resource/tinymce/langs"
6 ], 6 ],
7 "cSpell.words": [ 7 "cSpell.words": [
  8 + "inited",
8 "unref", 9 "unref",
9 "VITE" 10 "VITE"
10 ] 11 ]
  1 +import { HistoryData } from './model';
1 import { defHttp } from '/@/utils/http/axios'; 2 import { defHttp } from '/@/utils/http/axios';
2 3
3 // 获取设备配置 4 // 获取设备配置
@@ -9,7 +10,7 @@ export const getDeviceProfile = () => { @@ -9,7 +10,7 @@ export const getDeviceProfile = () => {
9 10
10 // 获取历史数据 11 // 获取历史数据
11 export const getDeviceHistoryInfo = (params) => { 12 export const getDeviceHistoryInfo = (params) => {
12 - return defHttp.get( 13 + return defHttp.get<HistoryData>(
13 { 14 {
14 url: `/plugins/telemetry/DEVICE/${params.entityId}/values/timeseries`, 15 url: `/plugins/telemetry/DEVICE/${params.entityId}/values/timeseries`,
15 params: { ...params, entityId: null, orderBy: 'ASC' }, 16 params: { ...params, entityId: null, orderBy: 'ASC' },
  1 +export interface HistoryData {
  2 + [key: string]: { ts: number; value: string }[];
  3 +}
@@ -4,9 +4,11 @@ import { @@ -4,9 +4,11 @@ import {
4 ComponentInfoDetail, 4 ComponentInfoDetail,
5 DataBoardList, 5 DataBoardList,
6 DataComponentRecord, 6 DataComponentRecord,
  7 + DeviceAttributeParams,
  8 + DeviceAttributeRecord,
7 GetDataBoardParams, 9 GetDataBoardParams,
8 - Layout,  
9 MasterDeviceList, 10 MasterDeviceList,
  11 + SendCommandParams,
10 UpdateDataBoardLayoutParams, 12 UpdateDataBoardLayoutParams,
11 UpdateDataBoardParams, 13 UpdateDataBoardParams,
12 UpdateDataComponentParams, 14 UpdateDataComponentParams,
@@ -32,10 +34,14 @@ enum DataBoardShareUrl { @@ -32,10 +34,14 @@ enum DataBoardShareUrl {
32 GET_DATA_COMPONENT = '/noauth/share/data_board', 34 GET_DATA_COMPONENT = '/noauth/share/data_board',
33 } 35 }
34 36
  37 +enum SendCommand {
  38 + ONEWAY = '/plugins/rpc/oneway',
  39 +}
  40 +
35 enum DeviceUrl { 41 enum DeviceUrl {
36 GET_DEVICE = '/device/list/master', 42 GET_DEVICE = '/device/list/master',
37 GET_SLAVE_DEVICE = '/device/list/slave', 43 GET_SLAVE_DEVICE = '/device/list/slave',
38 - GET_DEVICE_ATTRIBUTE = '/plugins/telemetry', 44 + GET_DEVICE_ATTRIBUTE = '/device/attributes',
39 } 45 }
40 46
41 /** 47 /**
@@ -184,14 +190,22 @@ export const getGatewaySlaveDevice = (params: { organizationId: string; masterId @@ -184,14 +190,22 @@ export const getGatewaySlaveDevice = (params: { organizationId: string; masterId
184 * @param params 190 * @param params
185 * @returns 191 * @returns
186 */ 192 */
187 -export const getDeviceAttributes = (params: { entityType?: string; deviceId: string }) => {  
188 - const { entityType = 'DEVICE', deviceId } = params;  
189 - return defHttp.get<string[]>(  
190 - {  
191 - url: `${DeviceUrl.GET_DEVICE_ATTRIBUTE}/${entityType}/${deviceId}/keys/timeseries`, 193 +export const getDeviceAttributes = (params: DeviceAttributeParams) => {
  194 + const { deviceProfileId, dataType } = params;
  195 + return defHttp.get<DeviceAttributeRecord[]>({
  196 + url: `${DeviceUrl.GET_DEVICE_ATTRIBUTE}/${deviceProfileId}`,
  197 + params: {
  198 + dataType,
192 }, 199 },
  200 + });
  201 +};
  202 +
  203 +export const sendCommandOneway = (params: SendCommandParams) => {
  204 + return defHttp.post(
193 { 205 {
194 - joinPrefix: false,  
195 - } 206 + url: `${SendCommand.ONEWAY}/${params.deviceId}`,
  207 + params: params.value,
  208 + },
  209 + { joinPrefix: false }
196 ); 210 );
197 }; 211 };
@@ -133,10 +133,31 @@ export interface UpdateDataComponentParams { @@ -133,10 +133,31 @@ export interface UpdateDataComponentParams {
133 133
134 export interface MasterDeviceList { 134 export interface MasterDeviceList {
135 deviceType: 'DIRECT_CONNECTION' | 'GATEWAY'; 135 deviceType: 'DIRECT_CONNECTION' | 'GATEWAY';
  136 + deviceProfileId: string;
136 id: string; 137 id: string;
137 name: string; 138 name: string;
  139 + label?: string;
  140 + value?: string;
138 } 141 }
139 142
140 export interface ComponentInfoDetail { 143 export interface ComponentInfoDetail {
141 data: { componentData: DataComponentRecord[]; componentLayout: Layout[] }; 144 data: { componentData: DataComponentRecord[]; componentLayout: Layout[] };
142 } 145 }
  146 +
  147 +export type DataType = 'BOOL' | 'DOUBLE' | 'INT' | 'STRUCT' | 'TExT';
  148 +
  149 +export interface DeviceAttributeParams {
  150 + deviceProfileId: string;
  151 + dataType?: DataType;
  152 +}
  153 +
  154 +export interface DeviceAttributeRecord {
  155 + name: string;
  156 + identifier: string;
  157 + detail: DataType;
  158 +}
  159 +
  160 +export interface SendCommandParams {
  161 + deviceId: string;
  162 + value: any;
  163 +}
@@ -34,6 +34,7 @@ @@ -34,6 +34,7 @@
34 let { width = 300, height = 150 } = unref(getOptions); 34 let { width = 300, height = 150 } = unref(getOptions);
35 width = isNumber(width) ? (`${width}px` as unknown as number) : width; 35 width = isNumber(width) ? (`${width}px` as unknown as number) : width;
36 height = isNumber(height) ? (`${height}px` as unknown as number) : height; 36 height = isNumber(height) ? (`${height}px` as unknown as number) : height;
  37 + console.log({ getOptions });
37 return { width, height } as CSSProperties; 38 return { width, height } as CSSProperties;
38 }); 39 });
39 40
@@ -10,7 +10,9 @@ @@ -10,7 +10,9 @@
10 :showOkBtn="false" 10 :showOkBtn="false"
11 @cancel="handleCancel" 11 @cancel="handleCancel"
12 > 12 >
13 - <div class="flex items-center justify-center bg-dark-900 w-full h-full min-h-52"> 13 + <div
  14 + class="flex items-center justify-center bg-dark-900 w-full h-full min-h-52 video-container"
  15 + >
14 <BasicVideoPlay v-if="showVideo" :options="options" /> 16 <BasicVideoPlay v-if="showVideo" :options="options" />
15 </div> 17 </div>
16 </BasicModal> 18 </BasicModal>
@@ -63,3 +65,13 @@ @@ -63,3 +65,13 @@
63 showVideo.value = false; 65 showVideo.value = false;
64 }; 66 };
65 </script> 67 </script>
  68 +
  69 +<style lang="less" scoped>
  70 + .video-container:deep(.vben-basic-video-play) {
  71 + min-height: 13rem;
  72 + }
  73 +
  74 + .video-container:deep(.video-js) {
  75 + min-height: 13rem;
  76 + }
  77 +</style>
@@ -61,6 +61,9 @@ @@ -61,6 +61,9 @@
61 61
62 for (const item of items) { 62 for (const item of items) {
63 (item as CameraRecordItem).isTransform = false; 63 (item as CameraRecordItem).isTransform = false;
  64 + (item as CameraRecordItem).videoPlayerOptions = {
  65 + ...basicVideoPlayOptions,
  66 + };
64 beforeVideoPlay(item); 67 beforeVideoPlay(item);
65 } 68 }
66 if (items.length < pagination.pageSize) { 69 if (items.length < pagination.pageSize) {
@@ -117,6 +120,7 @@ @@ -117,6 +120,7 @@
117 const index = unref(cameraList).findIndex((item) => item.id === record.id); 120 const index = unref(cameraList).findIndex((item) => item.id === record.id);
118 if (~index) unref(cameraList)[index].isTransform = true; 121 if (~index) unref(cameraList)[index].isTransform = true;
119 } 122 }
  123 + console.log(unref(cameraList));
120 } 124 }
121 }; 125 };
122 126
@@ -162,7 +162,7 @@ @@ -162,7 +162,7 @@
162 } else { 162 } else {
163 isNull.value = true; 163 isNull.value = true;
164 } 164 }
165 - setChartOptions(res); 165 + setChartOptions(res, props.attr);
166 }; 166 };
167 167
168 onMounted(async () => { 168 onMounted(async () => {
@@ -479,7 +479,7 @@ export const selectDeviceAttrSchema: FormSchema[] = [ @@ -479,7 +479,7 @@ export const selectDeviceAttrSchema: FormSchema[] = [
479 }, 479 },
480 ]; 480 ];
481 481
482 -export const eChartOptions = (series, keys): EChartsOption => { 482 +export const eChartOptions = (series: EChartsOption['series'], keys: string[]): EChartsOption => {
483 return { 483 return {
484 tooltip: { 484 tooltip: {
485 trigger: 'axis', 485 trigger: 'axis',
@@ -57,7 +57,14 @@ @@ -57,7 +57,14 @@
57 v-show="!isNull" 57 v-show="!isNull"
58 /> 58 />
59 </BasicModal> 59 </BasicModal>
60 - <DeviceDetailDrawer @register="registerDetailDrawer" /> 60 + <DeviceDetailDrawer
  61 + @register="registerDetailDrawer"
  62 + @open-tb-device-detail="handleOpenTbDeviceDetail"
  63 + @open-gateway-device-detail="handleOpenGatewayDetail"
  64 + />
  65 + <DeviceDetailDrawer @register="registerTbDetailDrawer" />
  66 +
  67 + <DeviceDetailDrawer @register="registerGatewayDetailDrawer" />
61 </div> 68 </div>
62 </template> 69 </template>
63 <script lang="ts"> 70 <script lang="ts">
@@ -94,6 +101,7 @@ @@ -94,6 +101,7 @@
94 import { QueryWay, SchemaFiled, AggregateDataEnum } from './cpns/TimePeriodForm/config'; 101 import { QueryWay, SchemaFiled, AggregateDataEnum } from './cpns/TimePeriodForm/config';
95 import { dateUtil } from '/@/utils/dateUtil'; 102 import { dateUtil } from '/@/utils/dateUtil';
96 import { Spin } from 'ant-design-vue'; 103 import { Spin } from 'ant-design-vue';
  104 + import { useAsyncQueue } from './useAsyncQueue';
97 105
98 interface DeviceInfo { 106 interface DeviceInfo {
99 alarmStatus: 0 | 1; 107 alarmStatus: 0 | 1;
@@ -139,6 +147,8 @@ @@ -139,6 +147,8 @@
139 const isNull = ref(true); 147 const isNull = ref(true);
140 const { toPromise } = useScript({ src: BAI_DU_MAP_URL }); 148 const { toPromise } = useScript({ src: BAI_DU_MAP_URL });
141 const [registerDetailDrawer, { openDrawer }] = useDrawer(); 149 const [registerDetailDrawer, { openDrawer }] = useDrawer();
  150 + const [registerTbDetailDrawer, { openDrawer: openTbDeviceDrawer }] = useDrawer();
  151 + const [registerGatewayDetailDrawer, { openDrawer: openGatewayDetailDrawer }] = useDrawer();
142 const [registerModal, { openModal }] = useModal(); 152 const [registerModal, { openModal }] = useModal();
143 const BMapInstance = ref<Nullable<any>>(null); 153 const BMapInstance = ref<Nullable<any>>(null);
144 154
@@ -159,15 +169,31 @@ @@ -159,15 +169,31 @@
159 showSizeChanger: true, 169 showSizeChanger: true,
160 }, 170 },
161 afterFetch: async (data: DeviceInfo[]) => { 171 afterFetch: async (data: DeviceInfo[]) => {
  172 + if (!(window as any).BMap) {
  173 + clearTask();
  174 + setTask(createMarket.bind(null, data));
  175 + return data;
  176 + }
162 createMarket(data); 177 createMarket(data);
163 return data; 178 return data;
164 }, 179 },
165 }); 180 });
  181 + const { setTask, clearTask, executeFlag } = useAsyncQueue();
  182 +
  183 + let interval: Nullable<NodeJS.Timer> = setInterval(() => {
  184 + if ((window as any).BMap) {
  185 + executeFlag.value = true;
  186 + clearInterval(interval!);
  187 + interval = null;
  188 + }
  189 + }, 1000);
166 190
167 async function createMarket(data: DeviceInfo[]) { 191 async function createMarket(data: DeviceInfo[]) {
168 const BMap = (window as any).BMap; 192 const BMap = (window as any).BMap;
169 - if (!BMap) return;  
170 - unref(BMapInstance).clearOverlays(); 193 + if (!BMap) {
  194 + return;
  195 + }
  196 + unref(BMapInstance)?.clearOverlays();
171 const markerList: MarkerList[] = []; 197 const markerList: MarkerList[] = [];
172 data.forEach((item) => { 198 data.forEach((item) => {
173 const { 199 const {
@@ -242,6 +268,7 @@ @@ -242,6 +268,7 @@
242 }; 268 };
243 const { name, organizationDTO, deviceState, deviceProfile, deviceType } = record; 269 const { name, organizationDTO, deviceState, deviceProfile, deviceType } = record;
244 const { address, longitude, latitude } = record.deviceInfo; 270 const { address, longitude, latitude } = record.deviceInfo;
  271 +
245 // 创建信息窗口对象 272 // 创建信息窗口对象
246 const res = await getDeviceActiveTime(entityId); 273 const res = await getDeviceActiveTime(entityId);
247 274
@@ -454,6 +481,14 @@ @@ -454,6 +481,14 @@
454 (window as any).openHistoryModal = null; 481 (window as any).openHistoryModal = null;
455 }); 482 });
456 483
  484 + const handleOpenTbDeviceDetail = (data: { id: string; tbDeviceId: string }) => {
  485 + openTbDeviceDrawer(true, data);
  486 + };
  487 +
  488 + const handleOpenGatewayDetail = (data: { id: string; tbDeviceId: string }) => {
  489 + openGatewayDetailDrawer(true, data);
  490 + };
  491 +
457 return { 492 return {
458 wrapRef, 493 wrapRef,
459 registerTable, 494 registerTable,
@@ -467,6 +502,10 @@ @@ -467,6 +502,10 @@
467 spinning, 502 spinning,
468 timePeriodRegister, 503 timePeriodRegister,
469 handleCancelModal, 504 handleCancelModal,
  505 + registerTbDetailDrawer,
  506 + registerGatewayDetailDrawer,
  507 + handleOpenTbDeviceDetail,
  508 + handleOpenGatewayDetail,
470 }; 509 };
471 }, 510 },
472 }); 511 });
  1 +import { ref, watchEffect } from 'vue';
  2 +
  3 +export function useAsyncQueue() {
  4 + const queue: Fn<any, Promise<any>>[] = [];
  5 +
  6 + const executeFlag = ref(false);
  7 +
  8 + const setTask = (fn: Fn<any, Promise<any>>) => {
  9 + queue.push(fn);
  10 + };
  11 +
  12 + const removeTask = (fn: Fn<any, Promise<any>>) => {
  13 + const index = queue.findIndex((item) => item === fn);
  14 + ~index && queue.splice(index, 1);
  15 + };
  16 +
  17 + const clearTask = () => {
  18 + queue.length = 0;
  19 + };
  20 +
  21 + watchEffect(() => {
  22 + if (executeFlag.value) {
  23 + queue.forEach((item) => item());
  24 + executeFlag.value = false;
  25 + }
  26 + });
  27 +
  28 + return { setTask, removeTask, clearTask, executeFlag };
  29 +}
@@ -28,7 +28,7 @@ @@ -28,7 +28,7 @@
28 import DeviceConfigurationStep from './step/DeviceConfigurationStep.vue'; 28 import DeviceConfigurationStep from './step/DeviceConfigurationStep.vue';
29 import TransportConfigurationStep from './step/TransportConfigurationStep.vue'; 29 import TransportConfigurationStep from './step/TransportConfigurationStep.vue';
30 import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue'; 30 import PhysicalModelManagementStep from './step/PhysicalModelManagementStep.vue';
31 - import { ref, unref } from 'vue'; 31 + import { nextTick, ref, unref } from 'vue';
32 import { deviceConfigGetDetail } from '/@/api/device/deviceConfigApi'; 32 import { deviceConfigGetDetail } from '/@/api/device/deviceConfigApi';
33 import { DeviceRecord } from '/@/api/device/model/deviceModel'; 33 import { DeviceRecord } from '/@/api/device/model/deviceModel';
34 import TopicPanel from './step/TopicPanel.vue'; 34 import TopicPanel from './step/TopicPanel.vue';
@@ -49,6 +49,7 @@ @@ -49,6 +49,7 @@
49 unref(DevConStRef)?.setFormData(res); 49 unref(DevConStRef)?.setFormData(res);
50 }; 50 };
51 const setTransConfFormData = async (res: Recordable) => { 51 const setTransConfFormData = async (res: Recordable) => {
  52 + await nextTick();
52 unref(TransConStRef)?.setFormData(res); 53 unref(TransConStRef)?.setFormData(res);
53 }; 54 };
54 55
@@ -9,6 +9,13 @@ import { FormField, FunctionType } from './step/cpns/physical/cpns/config'; @@ -9,6 +9,13 @@ import { FormField, FunctionType } from './step/cpns/physical/cpns/config';
9 import { h } from 'vue'; 9 import { h } from 'vue';
10 import { Tag } from 'ant-design-vue'; 10 import { Tag } from 'ant-design-vue';
11 11
  12 +export enum ModelOfMatterPermission {
  13 + CREATE = 'api:yt:things_model:post',
  14 + UPDATE = 'api:yt:things_model:put',
  15 + DELETE = 'api:yt:things_model:delete',
  16 + RELEASE = 'api:yt:things_model:release',
  17 +}
  18 +
12 export const steps = [ 19 export const steps = [
13 { 20 {
14 title: '产品', 21 title: '产品',
@@ -27,18 +27,16 @@ @@ -27,18 +27,16 @@
27 </div> 27 </div>
28 <div class="flex justify-between items-end"> 28 <div class="flex justify-between items-end">
29 <div class="flex gap-2"> 29 <div class="flex gap-2">
30 - <Authority value=""> 30 + <Authority :value="ModelOfMatterPermission.CREATE">
31 <Button v-if="isShowBtn" type="primary" @click="handleCreateOrEdit()"> 31 <Button v-if="isShowBtn" type="primary" @click="handleCreateOrEdit()">
32 新增物模型 32 新增物模型
33 </Button> 33 </Button>
34 - <Button type="primary" @click="handleOpenTsl"> 物模型TSL </Button>  
35 - <Button v-if="isShowBtn" type="primary" @click="handleImportModel"  
36 - >导入物模型</Button  
37 - >  
38 </Authority> 34 </Authority>
  35 + <Button type="primary" @click="handleOpenTsl"> 物模型TSL </Button>
  36 + <Button v-if="isShowBtn" type="primary" @click="handleImportModel">导入物模型</Button>
39 </div> 37 </div>
40 <div class="flex gap-2"> 38 <div class="flex gap-2">
41 - <Authority value=""> 39 + <Authority :value="ModelOfMatterPermission.RELEASE">
42 <Popconfirm 40 <Popconfirm
43 title="是否需要发布上线?" 41 title="是否需要发布上线?"
44 ok-text="确定" 42 ok-text="确定"
@@ -49,9 +47,12 @@ @@ -49,9 +47,12 @@
49 发布上线 47 发布上线
50 </Button> 48 </Button>
51 </Popconfirm> 49 </Popconfirm>
52 - <Button v-if="isShowBtn" class="!bg-gray-200" type="text" @click="handleReturn">  
53 - 返回  
54 - </Button> 50 + </Authority>
  51 +
  52 + <Button v-if="isShowBtn" class="!bg-gray-200" type="text" @click="handleReturn">
  53 + 返回
  54 + </Button>
  55 + <Authority :value="ModelOfMatterPermission.DELETE">
55 <Popconfirm 56 <Popconfirm
56 title="您确定要批量删除数据" 57 title="您确定要批量删除数据"
57 ok-text="确定" 58 ok-text="确定"
@@ -84,14 +85,14 @@ @@ -84,14 +85,14 @@
84 { 85 {
85 label: '编辑', 86 label: '编辑',
86 icon: 'clarity:note-edit-line', 87 icon: 'clarity:note-edit-line',
87 - auth: '', 88 + auth: ModelOfMatterPermission.UPDATE,
88 onClick: handleCreateOrEdit.bind(null, record), 89 onClick: handleCreateOrEdit.bind(null, record),
89 ifShow: !isShowBtn ? false : true, 90 ifShow: !isShowBtn ? false : true,
90 }, 91 },
91 { 92 {
92 label: '删除', 93 label: '删除',
93 icon: 'ant-design:delete-outlined', 94 icon: 'ant-design:delete-outlined',
94 - auth: '', 95 + auth: ModelOfMatterPermission.DELETE,
95 color: 'error', 96 color: 'error',
96 ifShow: !isShowBtn ? false : true, 97 ifShow: !isShowBtn ? false : true,
97 popConfirm: { 98 popConfirm: {
@@ -114,7 +115,11 @@ @@ -114,7 +115,11 @@
114 <script lang="ts" setup> 115 <script lang="ts" setup>
115 import { BasicTable, useTable, TableAction } from '/@/components/Table'; 116 import { BasicTable, useTable, TableAction } from '/@/components/Table';
116 import { useModal } from '/@/components/Modal'; 117 import { useModal } from '/@/components/Modal';
117 - import { modelOfMatterForm, physicalColumn } from '../device.profile.data'; 118 + import {
  119 + modelOfMatterForm,
  120 + ModelOfMatterPermission,
  121 + physicalColumn,
  122 + } from '../device.profile.data';
118 import { useBatchDelete } from '/@/hooks/web/useBatchDelete'; 123 import { useBatchDelete } from '/@/hooks/web/useBatchDelete';
119 import { Authority } from '/@/components/Authority'; 124 import { Authority } from '/@/components/Authority';
120 import PhysicalModelModal from './cpns/physical/PhysicalModelModal.vue'; 125 import PhysicalModelModal from './cpns/physical/PhysicalModelModal.vue';
@@ -78,11 +78,12 @@ @@ -78,11 +78,12 @@
78 submitOnReset: false, 78 submitOnReset: false,
79 showActionButtonGroup: props.ifShowBtn ? true : false, 79 showActionButtonGroup: props.ifShowBtn ? true : false,
80 }); 80 });
81 - const setFormData = (v) => { 81 + const setFormData = async (v) => {
82 setFieldsValue({ 82 setFieldsValue({
83 transportType: v?.profileData?.transportConfiguration?.type, 83 transportType: v?.profileData?.transportConfiguration?.type,
84 }); 84 });
85 isMqttType.value = v?.profileData?.transportConfiguration?.type; 85 isMqttType.value = v?.profileData?.transportConfiguration?.type;
  86 + await nextTick();
86 mqttRef.value?.setFormData(v?.profileData?.transportConfiguration); 87 mqttRef.value?.setFormData(v?.profileData?.transportConfiguration);
87 coapRef.value?.setFormData(v?.profileData?.transportConfiguration); 88 coapRef.value?.setFormData(v?.profileData?.transportConfiguration);
88 lwm2mRef.value?.setFormData(v?.profileData?.transportConfiguration); 89 lwm2mRef.value?.setFormData(v?.profileData?.transportConfiguration);
@@ -5,8 +5,10 @@ @@ -5,8 +5,10 @@
5 </script> 5 </script>
6 <script lang="ts" setup> 6 <script lang="ts" setup>
7 import { RadioRecord } from '../../detail/config/util'; 7 import { RadioRecord } from '../../detail/config/util';
  8 + import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config';
  9 + import { useSendCommand } from './useSendCommand';
8 10
9 - interface VisualComponentProps<Layout = Recordable, Value = Recordable> { 11 + interface VisualComponentProps<Layout = Recordable, Value = ControlComponentValue> {
10 value?: Value; 12 value?: Value;
11 layout?: Layout; 13 layout?: Layout;
12 radio?: RadioRecord; 14 radio?: RadioRecord;
@@ -15,29 +17,40 @@ @@ -15,29 +17,40 @@
15 update?: () => void; 17 update?: () => void;
16 remove?: (key: string) => void; 18 remove?: (key: string) => void;
17 } 19 }
  20 +
18 const props = defineProps<VisualComponentProps>(); 21 const props = defineProps<VisualComponentProps>();
19 22
20 const emit = defineEmits(['update:value', 'change']); 23 const emit = defineEmits(['update:value', 'change']);
21 24
  25 + const { sendCommand } = useSendCommand();
22 const handleChange = (event: Event) => { 26 const handleChange = (event: Event) => {
23 const _value = (event.target as HTMLInputElement).checked; 27 const _value = (event.target as HTMLInputElement).checked;
24 emit('update:value', _value); 28 emit('update:value', _value);
25 emit('change', _value); 29 emit('change', _value);
  30 + sendCommand(props.value?.slaveDeviceId || props.value?.deviceId, _value);
26 }; 31 };
27 </script> 32 </script>
28 33
29 <template> 34 <template>
30 - <label class="sliding-switch">  
31 - <input  
32 - :value="props.value?.value"  
33 - type="checkbox"  
34 - :checked="props.value?.value"  
35 - @change="handleChange"  
36 - />  
37 - <span class="slider"></span>  
38 - <span class="on">ON</span>  
39 - <span class="off">OFF</span>  
40 - </label> 35 + <div class="flex flex-col justify-center">
  36 + <label class="sliding-switch">
  37 + <input
  38 + :value="props.value?.value"
  39 + type="checkbox"
  40 + :checked="props.value?.value"
  41 + @change="handleChange"
  42 + />
  43 + <span class="slider"></span>
  44 + <span class="on">ON</span>
  45 + <span class="off">OFF</span>
  46 + </label>
  47 + <div
  48 + class="text-center mt-2 text-gray-700"
  49 + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }"
  50 + >
  51 + {{ props.value?.attributeRename || props.value?.attribute }}</div
  52 + >
  53 + </div>
41 </template> 54 </template>
42 55
43 <style scoped lang="less"> 56 <style scoped lang="less">
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 </script> 5 </script>
6 <script lang="ts" setup> 6 <script lang="ts" setup>
7 import { Switch } from 'ant-design-vue'; 7 import { Switch } from 'ant-design-vue';
8 - import { computed } from 'vue'; 8 + import { computed, ref, watchEffect } from 'vue';
9 import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util'; 9 import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util';
10 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue'; 10 import SvgIcon from '/@/components/Icon/src/SvgIcon.vue';
11 import { 11 import {
@@ -13,6 +13,7 @@ @@ -13,6 +13,7 @@
13 ControlComponentValue, 13 ControlComponentValue,
14 ControlComponentLayout, 14 ControlComponentLayout,
15 } from './control.config'; 15 } from './control.config';
  16 + import { useSendCommand } from './useSendCommand';
16 const props = withDefaults( 17 const props = withDefaults(
17 defineProps<{ 18 defineProps<{
18 layout?: ControlComponentLayout; 19 layout?: ControlComponentLayout;
@@ -26,6 +27,18 @@ @@ -26,6 +27,18 @@
26 const getRadio = computed(() => { 27 const getRadio = computed(() => {
27 return props.radio || DEFAULT_RADIO_RECORD; 28 return props.radio || DEFAULT_RADIO_RECORD;
28 }); 29 });
  30 +
  31 + const checked = ref(!!Number(props.value.value));
  32 +
  33 + const { sendCommand } = useSendCommand();
  34 + const handleChange = (value: boolean) => {
  35 + console.log(props.value);
  36 + sendCommand(props.value.slaveDeviceId! || props.value.deviceId!, value);
  37 + };
  38 +
  39 + watchEffect(() => {
  40 + checked.value = !!Number(props.value.value);
  41 + });
29 </script> 42 </script>
30 43
31 <template> 44 <template>
@@ -40,8 +53,13 @@ @@ -40,8 +53,13 @@
40 height: fontSize({ radioRecord: getRadio, basic: 30, min: 16 }), 53 height: fontSize({ radioRecord: getRadio, basic: 30, min: 16 }),
41 }" 54 }"
42 /> 55 />
43 - <span class="flex-auto mx-4 flex items-center truncate inline-block">属性名</span> 56 + <span
  57 + class="flex-auto mx-4 flex items-center truncate inline-block text-gray-700"
  58 + :style="{ color: props.value.fontColor || ControlComponentDefaultConfig.fontColor }"
  59 + >
  60 + {{ props.value.attributeRename || props.value.attribute }}
  61 + </span>
44 </div> 62 </div>
45 - <Switch /> 63 + <Switch v-model:checked="checked" @change="handleChange" />
46 </div> 64 </div>
47 </template> 65 </template>
@@ -6,7 +6,8 @@ @@ -6,7 +6,8 @@
6 <script lang="ts" setup> 6 <script lang="ts" setup>
7 import { computed } from '@vue/reactivity'; 7 import { computed } from '@vue/reactivity';
8 import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util'; 8 import { DEFAULT_RADIO_RECORD, fontSize, RadioRecord } from '../../detail/config/util';
9 - import { ControlComponentValue } from './control.config'; 9 + import { ControlComponentDefaultConfig, ControlComponentValue } from './control.config';
  10 + import { useSendCommand } from './useSendCommand';
10 11
11 const props = defineProps<{ 12 const props = defineProps<{
12 value?: ControlComponentValue; 13 value?: ControlComponentValue;
@@ -16,10 +17,12 @@ @@ -16,10 +17,12 @@
16 17
17 const emit = defineEmits(['update:value', 'change']); 18 const emit = defineEmits(['update:value', 'change']);
18 19
  20 + const { sendCommand } = useSendCommand();
19 const handleChange = (event: Event) => { 21 const handleChange = (event: Event) => {
20 const _value = (event.target as HTMLInputElement).checked; 22 const _value = (event.target as HTMLInputElement).checked;
21 emit('update:value', _value); 23 emit('update:value', _value);
22 emit('change', _value); 24 emit('change', _value);
  25 + sendCommand(props.value?.slaveDeviceId || props.value?.deviceId, _value);
23 }; 26 };
24 27
25 const getRadio = computed(() => { 28 const getRadio = computed(() => {
@@ -28,28 +31,36 @@ @@ -28,28 +31,36 @@
28 </script> 31 </script>
29 32
30 <template> 33 <template>
31 - <div  
32 - class="toggle-switch"  
33 - :style="{  
34 - width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }),  
35 - height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }),  
36 - }"  
37 - >  
38 - <label class="switch">  
39 - <input  
40 - :value="props.value?.value"  
41 - type="checkbox"  
42 - :checked="props.value?.value"  
43 - @change="handleChange"  
44 - />  
45 - <div class="button">  
46 - <div class="light"></div>  
47 - <div class="dots"></div>  
48 - <div class="characters"></div>  
49 - <div class="shine"></div>  
50 - <div class="shadow"></div>  
51 - </div>  
52 - </label> 34 + <div class="flex flex-col">
  35 + <div
  36 + class="toggle-switch"
  37 + :style="{
  38 + width: fontSize({ radioRecord: getRadio, basic: 75, max: 75, min: 60 }),
  39 + height: fontSize({ radioRecord: getRadio, basic: 97.5, max: 97.5, min: 80 }),
  40 + }"
  41 + >
  42 + <label class="switch">
  43 + <input
  44 + :value="props.value?.value"
  45 + type="checkbox"
  46 + :checked="props.value?.value"
  47 + @change="handleChange"
  48 + />
  49 + <div class="button">
  50 + <div class="light"></div>
  51 + <div class="dots"></div>
  52 + <div class="characters"></div>
  53 + <div class="shine"></div>
  54 + <div class="shadow"></div>
  55 + </div>
  56 + </label>
  57 + </div>
  58 + <div
  59 + class="text-center mt-2 text-gray-700"
  60 + :style="{ color: props?.value?.fontColor || ControlComponentDefaultConfig.fontColor }"
  61 + >
  62 + {{ props.value?.attributeRename || props.value?.attribute }}</div
  63 + >
53 </div> 64 </div>
54 </template> 65 </template>
55 66
@@ -6,14 +6,19 @@ export interface ControlComponentLayout { @@ -6,14 +6,19 @@ export interface ControlComponentLayout {
6 6
7 export interface ControlComponentValue { 7 export interface ControlComponentValue {
8 value?: boolean; 8 value?: boolean;
9 - name?: string; 9 + attribute?: string;
  10 + attributeRename?: string;
10 icon?: string; 11 icon?: string;
11 iconColor?: string; 12 iconColor?: string;
  13 + deviceId?: string;
  14 + fontColor?: string;
  15 + slaveDeviceId?: string;
12 } 16 }
13 17
14 export const ControlComponentDefaultConfig: ControlComponentValue = { 18 export const ControlComponentDefaultConfig: ControlComponentValue = {
15 icon: 'shuiwen', 19 icon: 'shuiwen',
16 iconColor: '#367BFF', 20 iconColor: '#367BFF',
  21 + fontColor: '#000',
17 }; 22 };
18 23
19 export const transformControlConfig = ( 24 export const transformControlConfig = (
@@ -23,8 +28,11 @@ export const transformControlConfig = ( @@ -23,8 +28,11 @@ export const transformControlConfig = (
23 ) => { 28 ) => {
24 return { 29 return {
25 value: { 30 value: {
26 - value: dataSourceRecord.componentInfo.value,  
27 - icon: dataSourceRecord.componentInfo.icon, 31 + ...dataSourceRecord.componentInfo,
  32 + attribute: dataSourceRecord.attribute,
  33 + attributeRename: dataSourceRecord.attributeRename,
  34 + deviceId: dataSourceRecord.deviceId,
  35 + slaveDeviceId: dataSourceRecord.slaveDeviceId,
28 } as ControlComponentValue, 36 } as ControlComponentValue,
29 }; 37 };
30 }; 38 };
  1 +import { sendCommandOneway } from '/@/api/dataBoard';
  2 +import { useMessage } from '/@/hooks/web/useMessage';
  3 +
  4 +const { createMessage } = useMessage();
  5 +export function useSendCommand() {
  6 + const sendCommand = async (deviceId: string, value: any) => {
  7 + if (!deviceId) return;
  8 + try {
  9 + await sendCommandOneway({
  10 + deviceId,
  11 + value: {
  12 + params: Number(value),
  13 + persistent: true,
  14 + additionalInfo: {
  15 + cmdType: 'API',
  16 + },
  17 + method: 'methodThingskit',
  18 + },
  19 + });
  20 + createMessage.success('命令下发成功');
  21 + } catch (error) {}
  22 + };
  23 + return {
  24 + sendCommand,
  25 + };
  26 +}
  1 +<script lang="ts" setup>
  2 + import { BasicForm, useForm } from '/@/components/Form';
  3 + import { BasicModal, useModalInner } from '/@/components/Modal';
  4 + import {
  5 + formSchema,
  6 + getHistorySearchParams,
  7 + SchemaFiled,
  8 + } from '../../../board/detail/config/historyTrend.config';
  9 + import { HistoryModalOkEmitParams, HistoryModalParams } from './type';
  10 + import { DataSource } from '/@/api/dataBoard/model';
  11 + import { ref } from 'vue';
  12 + import { getAllDeviceByOrg } from '/@/api/dataBoard';
  13 + import { getDeviceHistoryInfo } from '/@/api/alarm/position';
  14 +
  15 + const emit = defineEmits(['register', 'ok']);
  16 +
  17 + const [registerForm, { updateSchema, setFieldsValue, validate, getFieldsValue }] = useForm({
  18 + schemas: formSchema,
  19 + showActionButtonGroup: false,
  20 + fieldMapToTime: [
  21 + [SchemaFiled.DATE_RANGE, [SchemaFiled.START_TS, SchemaFiled.END_TS], 'YYYY-MM-DD HH:ss'],
  22 + ],
  23 + });
  24 +
  25 + const [registerModal, { closeModal }] = useModalInner(async (params: HistoryModalParams) => {
  26 + try {
  27 + const { dataSource = [] } = params;
  28 + const deviceRecord = dataSource?.at(0) || ({} as DataSource);
  29 + if (!deviceRecord.organizationId) return;
  30 + const deviceList = await getAllDeviceByOrg(deviceRecord.organizationId);
  31 + const options = deviceList
  32 + .filter((item) => item.id === deviceRecord.deviceId)
  33 + .map((item) => ({ ...item, label: item.name, value: item.id }));
  34 + const attKey = dataSource.map((item) => ({
  35 + ...item,
  36 + label: item.attribute,
  37 + value: item.attribute,
  38 + }));
  39 + updateSchema([
  40 + {
  41 + field: SchemaFiled.DEVICE_ID,
  42 + componentProps: {
  43 + options,
  44 + },
  45 + },
  46 + {
  47 + field: SchemaFiled.KEYS,
  48 + component: 'Select',
  49 + defaultValue: attKey.map((item) => item.value),
  50 + componentProps: {
  51 + options: attKey,
  52 + mode: 'multiple',
  53 + disabled: true,
  54 + },
  55 + },
  56 + ]);
  57 +
  58 + setFieldsValue({
  59 + [SchemaFiled.DEVICE_ID]: deviceRecord.deviceId,
  60 + [SchemaFiled.KEYS]: attKey.map((item) => item.value),
  61 + });
  62 + } catch (error) {
  63 + throw error;
  64 + }
  65 + });
  66 +
  67 + const loading = ref(false);
  68 + const handleOk = async () => {
  69 + try {
  70 + await validate();
  71 + let value = getFieldsValue();
  72 +
  73 + value = getHistorySearchParams(value);
  74 +
  75 + loading.value = true;
  76 +
  77 + const res = await getDeviceHistoryInfo({
  78 + ...value,
  79 + [SchemaFiled.KEYS]: value[SchemaFiled.KEYS].join(','),
  80 + });
  81 +
  82 + let timespanList = Object.keys(res).reduce((prev, next) => {
  83 + const ts = res[next].map((item) => item.ts);
  84 + return [...prev, ...ts];
  85 + }, [] as number[]);
  86 + timespanList = [...new Set(timespanList)];
  87 +
  88 + const track: Record<'lng' | 'lat', number>[] = [];
  89 + const keys = Object.keys(res);
  90 +
  91 + for (const ts of timespanList) {
  92 + const list: { ts: number; value: number }[] = [];
  93 + for (const key of keys) {
  94 + const record = res[key].find((item) => ts === item.ts);
  95 + list.push(record as any);
  96 + }
  97 + if (list.every(Boolean)) {
  98 + const lng = list.at(0)?.value;
  99 + const lat = list.at(1)?.value;
  100 + if (lng && lat) track.push({ lng, lat });
  101 + }
  102 + }
  103 + emit('ok', { track, value } as HistoryModalOkEmitParams);
  104 + closeModal();
  105 + } catch (error) {
  106 + throw error;
  107 + } finally {
  108 + loading.value = false;
  109 + }
  110 + };
  111 +</script>
  112 +
  113 +<template>
  114 + <BasicModal
  115 + title="历史轨迹"
  116 + @register="registerModal"
  117 + @ok="handleOk"
  118 + :ok-button-props="{ loading }"
  119 + >
  120 + <BasicForm @register="registerForm" />
  121 + </BasicModal>
  122 +</template>
1 <script lang="ts"> 1 <script lang="ts">
2 export default { 2 export default {
  3 + components: { Spin },
3 inheritAttrs: false, 4 inheritAttrs: false,
4 }; 5 };
5 </script> 6 </script>
6 <script lang="ts" setup> 7 <script lang="ts" setup>
7 - import { computed, onMounted, ref, unref } from 'vue'; 8 + import { computed, onMounted, reactive, ref, unref, watchEffect } from 'vue';
8 import { RadioRecord } from '../../detail/config/util'; 9 import { RadioRecord } from '../../detail/config/util';
9 import { MapComponentLayout, MapComponentValue } from './map.config'; 10 import { MapComponentLayout, MapComponentValue } from './map.config';
10 import { 11 import {
@@ -12,11 +13,18 @@ @@ -12,11 +13,18 @@
12 PlayCircleOutlined, 13 PlayCircleOutlined,
13 PauseCircleOutlined, 14 PauseCircleOutlined,
14 } from '@ant-design/icons-vue'; 15 } from '@ant-design/icons-vue';
15 - import { Button, Tooltip } from 'ant-design-vue'; 16 + import { Button, Spin, Tooltip } from 'ant-design-vue';
16 import { FrontComponent } from '../../const/const'; 17 import { FrontComponent } from '../../const/const';
17 import { buildUUID } from '/@/utils/uuid'; 18 import { buildUUID } from '/@/utils/uuid';
  19 + import { useModal } from '/@/components/Modal';
  20 + import HistoryDataModel from './HistoryDataModel.vue';
  21 + import { HistoryModalOkEmitParams, HistoryModalParams } from './type';
  22 + import { formatToDateTime } from '/@/utils/dateUtil';
  23 + import { isEqual } from 'lodash-es';
  24 + import { useAsyncQueue } from '/@/views/device/localtion/useAsyncQueue';
18 25
19 // useVisualBoardContext(); 26 // useVisualBoardContext();
  27 + type TrackRecord = Record<'lng' | 'lat' | 'ts', number>;
20 28
21 const startMethodName = `trackPlayMethod_${buildUUID()}`; 29 const startMethodName = `trackPlayMethod_${buildUUID()}`;
22 30
@@ -44,19 +52,52 @@ @@ -44,19 +52,52 @@
44 const trackAni = ref<Nullable<any>>(null); 52 const trackAni = ref<Nullable<any>>(null);
45 let mapInstance: Nullable<Recordable> = null; 53 let mapInstance: Nullable<Recordable> = null;
46 54
  55 + const trackList = ref<TrackRecord[]>([]);
  56 +
  57 + watchEffect(() => {
  58 + if (
  59 + props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_REAL &&
  60 + props.value?.track?.length
  61 + ) {
  62 + const lng = props.value.track.at(0)!;
  63 + const lat = props.value.track.at(1)!;
  64 + const record = {
  65 + lng: lng.value,
  66 + lat: lat.value,
  67 + ts: lng.ts,
  68 + };
  69 + if (unref(trackList).length && isEqual(unref(trackList).at(-1), record)) return;
  70 + trackList.value.push(record);
  71 +
  72 + randomAnimation(unref(trackList));
  73 + // marketPoint(record);
  74 + }
  75 + });
  76 +
  77 + // function marketPoint(params: Record<'lng' | 'lat', number>) {
  78 + // const { lng, lat } = params;
  79 + // const BMap = (window as any).BMapGL;
  80 + // const marker = new BMap.Marker(new BMap.Point(lng, lat));
  81 + // unref(mapInstance)?.centerAndZoom(new BMap.Point(lng, lat));
  82 + // unref(mapInstance)?.addOverlay(marker);
  83 + // }
  84 +
  85 + const prepare = ref(false);
47 async function initMap() { 86 async function initMap() {
48 const wrapEl = unref(wrapRef); 87 const wrapEl = unref(wrapRef);
49 if (!wrapEl) return; 88 if (!wrapEl) return;
50 const BMapGL = (window as any).BMapGL; 89 const BMapGL = (window as any).BMapGL;
51 mapInstance = new BMapGL.Map(wrapId); 90 mapInstance = new BMapGL.Map(wrapId);
52 - const point = new BMapGL.Point(116.404, 39.915); 91 + const point = new BMapGL.Point(104.09457, 30.53189);
53 mapInstance!.centerAndZoom(point, 15); 92 mapInstance!.centerAndZoom(point, 15);
54 mapInstance!.enableScrollWheelZoom(true); 93 mapInstance!.enableScrollWheelZoom(true);
55 - props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY && randomAnimation(); 94 + props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY &&
  95 + props.random &&
  96 + randomAnimation();
56 } 97 }
57 98
58 - const randomAnimation = () => {  
59 - const path = [ 99 + const randomAnimation = (path?: Record<'lng' | 'lat', number>[], clearOverlays = true) => {
  100 + path = path || [
60 { 101 {
61 lng: 116.297611, 102 lng: 116.297611,
62 lat: 40.047363, 103 lat: 40.047363,
@@ -90,6 +131,8 @@ @@ -90,6 +131,8 @@
90 const point: any[] = []; 131 const point: any[] = [];
91 const BMapGL = (window as any).BMapGL; 132 const BMapGL = (window as any).BMapGL;
92 133
  134 + clearOverlays && unref(mapInstance)?.clearOverlays();
  135 +
93 for (const { lng, lat } of path) { 136 for (const { lng, lat } of path) {
94 point.push(new BMapGL.Point(lng, lat)); 137 point.push(new BMapGL.Point(lng, lat));
95 } 138 }
@@ -99,10 +142,10 @@ @@ -99,10 +142,10 @@
99 142
100 const dynamicPlayMethod = { 143 const dynamicPlayMethod = {
101 [startMethodName]() { 144 [startMethodName]() {
102 - trackAni.value = new BMapGLLib.TrackAnimation(mapInstance, pl, { 145 + trackAni.value = new BMapGLLib.TrackAnimation(unref(mapInstance), pl, {
103 overallView: true, 146 overallView: true,
104 tilt: 30, 147 tilt: 30,
105 - duration: 20000, 148 + duration: 5000,
106 delay: 300, 149 delay: 300,
107 }); 150 });
108 trackAni.value!.start(); 151 trackAni.value!.start();
@@ -114,15 +157,43 @@ @@ -114,15 +157,43 @@
114 setTimeout(`${startMethodName}()`); 157 setTimeout(`${startMethodName}()`);
115 }; 158 };
116 159
  160 + const { setTask, executeFlag } = useAsyncQueue();
117 onMounted(() => { 161 onMounted(() => {
  162 + let interval: Nullable<NodeJS.Timer> = setInterval(() => {
  163 + if ((window as any).BMapGL) {
  164 + prepare.value = true;
  165 + executeFlag.value = true;
  166 + clearInterval(interval!);
  167 + interval = null;
  168 + }
  169 + }, 1000);
  170 + if (!(window as any).BMapGL) {
  171 + setTask(initMap);
  172 + return;
  173 + }
118 initMap(); 174 initMap();
119 }); 175 });
120 176
  177 + const timeRange = reactive<Record<'start' | 'end', Nullable<number>>>({
  178 + start: null,
  179 + end: null,
  180 + });
121 const getTimeRange = computed(() => { 181 const getTimeRange = computed(() => {
122 - return ` - 从 ${'2020-10-20 10:10:10'} 到 ${'2020-10-20 10:10:10'}`; 182 + const { start, end } = timeRange;
  183 + if (!start || !end) return `- 请选择`;
  184 + return ` - 从 ${formatToDateTime(start, 'YYYY-MM-DD HH:mm:ss')} 到 ${formatToDateTime(
  185 + end,
  186 + 'YYYY-MM-DD HH:mm:ss'
  187 + )}`;
123 }); 188 });
124 189
125 - const handleTrackSwitch = () => {}; 190 + const [register, { openModal }] = useModal();
  191 +
  192 + const handleTrackSwitch = () => {
  193 + openModal(true, {
  194 + dataSource: props.value?.dataSource || [],
  195 + } as HistoryModalParams);
  196 + };
126 197
127 const getTrackPlayStatus = computed(() => { 198 const getTrackPlayStatus = computed(() => {
128 return (trackAni.value || {})._status; 199 return (trackAni.value || {})._status;
@@ -133,10 +204,17 @@ @@ -133,10 +204,17 @@
133 else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PLAY) unref(trackAni).pause(); 204 else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PLAY) unref(trackAni).pause();
134 else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PAUSE) unref(trackAni).continue(); 205 else if (unref(getTrackPlayStatus) === TrackAnimationStatus.PAUSE) unref(trackAni).continue();
135 }; 206 };
  207 +
  208 + const handleRenderHistroyData = (params: HistoryModalOkEmitParams) => {
  209 + const { track, value } = params;
  210 + track.length && randomAnimation(track as unknown as Record<'lng' | 'lat', number>[]);
  211 + timeRange.start = value.startTs as number;
  212 + timeRange.end = value.endTs as number;
  213 + };
136 </script> 214 </script>
137 215
138 <template> 216 <template>
139 - <div class="w-full h-full flex justify-center items-center flex-col"> 217 + <div class="w-full h-full flex justify-center items-center flex-col p-2 no-drag">
140 <div 218 <div
141 class="w-full flex" 219 class="w-full flex"
142 v-if="props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY" 220 v-if="props.layout?.componentType === FrontComponent.MAP_COMPONENT_TRACK_HISTORY"
@@ -144,7 +222,7 @@ @@ -144,7 +222,7 @@
144 <Button type="text" class="!px-2 flex-auto !text-left truncate" @click="handleTrackSwitch"> 222 <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"> 223 <div class="w-full truncate text-gray-500 flex items-center">
146 <ClockCircleOutlined /> 224 <ClockCircleOutlined />
147 - <span class="mx-1">实时</span> 225 + <span class="mx-1">历史</span>
148 <Tooltip :title="getTimeRange.replace('-', '')"> 226 <Tooltip :title="getTimeRange.replace('-', '')">
149 <span class="truncate"> 227 <span class="truncate">
150 {{ getTimeRange }} 228 {{ getTimeRange }}
@@ -154,12 +232,17 @@ @@ -154,12 +232,17 @@
154 </Button> 232 </Button>
155 <Button type="text" class="!px-2 !text-gray-500" @click="handlePlay"> 233 <Button type="text" class="!px-2 !text-gray-500" @click="handlePlay">
156 <PlayCircleOutlined v-show="getTrackPlayStatus !== TrackAnimationStatus.PLAY" /> 234 <PlayCircleOutlined v-show="getTrackPlayStatus !== TrackAnimationStatus.PLAY" />
157 - <PauseCircleOutlined v-show="getTrackPlayStatus === TrackAnimationStatus.PLAY" /> 235 + <PauseCircleOutlined
  236 + class="!ml-0"
  237 + v-show="getTrackPlayStatus === TrackAnimationStatus.PLAY"
  238 + />
158 <span> 239 <span>
159 {{ getTrackPlayStatus !== TrackAnimationStatus.PLAY ? '播放轨迹' : '暂停播放' }} 240 {{ getTrackPlayStatus !== TrackAnimationStatus.PLAY ? '播放轨迹' : '暂停播放' }}
160 </span> 241 </span>
161 </Button> 242 </Button>
162 </div> 243 </div>
  244 + <Spin class="w-full h-full" :spinning="!prepare" tip="Loading..." />
163 <div ref="wrapRef" :id="wrapId" class="w-full h-full"></div> 245 <div ref="wrapRef" :id="wrapId" class="w-full h-full"></div>
  246 + <HistoryDataModel @register="register" @ok="handleRenderHistroyData" />
164 </div> 247 </div>
165 </template> 248 </template>
1 import { FrontComponent } from '../../const/const'; 1 import { FrontComponent } from '../../const/const';
2 import { ComponentConfig } from '../../types/type'; 2 import { ComponentConfig } from '../../types/type';
  3 +import { DataSource } from '/@/api/dataBoard/model';
3 4
4 export interface MapComponentLayout { 5 export interface MapComponentLayout {
5 componentType?: FrontComponent; 6 componentType?: FrontComponent;
@@ -7,7 +8,8 @@ export interface MapComponentLayout { @@ -7,7 +8,8 @@ export interface MapComponentLayout {
7 8
8 export interface MapComponentValue { 9 export interface MapComponentValue {
9 icon?: string; 10 icon?: string;
10 - track?: Recordable[]; 11 + track?: Record<'ts' | 'value', number>[];
  12 + dataSource?: DataSource[];
11 } 13 }
12 14
13 interface Config { 15 interface Config {
@@ -22,14 +24,39 @@ export const MapRealTrackConfig: Config = { @@ -22,14 +24,39 @@ export const MapRealTrackConfig: Config = {
22 componentType: FrontComponent.MAP_COMPONENT_TRACK_REAL, 24 componentType: FrontComponent.MAP_COMPONENT_TRACK_REAL,
23 }; 25 };
24 26
  27 +const getTrack = (dataSource: DataSource[], config: Config) => {
  28 + if (dataSource.length >= 2) {
  29 + const trackRecord = dataSource.slice(0, 2);
  30 + if (
  31 + !trackRecord.every((item) => item.componentInfo.value) &&
  32 + config.componentType === FrontComponent.MAP_COMPONENT_TRACK_REAL
  33 + )
  34 + return { track: [], dataSource: [] };
  35 +
  36 + const track = trackRecord.map((item) => {
  37 + return {
  38 + ts: item.componentInfo.updateTime,
  39 + value: item.componentInfo.value || 0,
  40 + };
  41 + });
  42 + return { track, dataSource };
  43 + }
  44 + return { track: [], dataSource: [] };
  45 +};
  46 +
25 export const transfromMapComponentConfig: ComponentConfig['transformConfig'] = ( 47 export const transfromMapComponentConfig: ComponentConfig['transformConfig'] = (
26 componentConfig: Config, 48 componentConfig: Config,
27 _record, 49 _record,
28 - _dataSourceRecord 50 + dataSourceRecord
29 ) => { 51 ) => {
  52 + const { track, dataSource } = getTrack(dataSourceRecord as DataSource[], componentConfig);
30 return { 53 return {
31 layout: { 54 layout: {
32 ...componentConfig, 55 ...componentConfig,
33 - }, 56 + } as MapComponentLayout,
  57 + value: {
  58 + track,
  59 + dataSource,
  60 + } as MapComponentValue,
34 }; 61 };
35 }; 62 };
  1 +import { SchemaFiled } from '../../detail/config/historyTrend.config';
  2 +import { DataSource } from '/@/api/dataBoard/model';
  3 +
  4 +export interface HistoryModalParams {
  5 + dataSource?: DataSource[];
  6 +}
  7 +
  8 +export interface HistoryModalOkEmitParams {
  9 + track: {
  10 + lng: number | string;
  11 + lat: number | string;
  12 + }[];
  13 + value: Record<SchemaFiled, string | number>;
  14 +}
@@ -2,11 +2,22 @@ @@ -2,11 +2,22 @@
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 import { createVisualBoardContext } from '../../hook/useVisualBoardContext';
  5 + import { DataComponentRecord } from '/@/api/dataBoard/model';
  6 + import { computed } from 'vue';
  7 + import { frontComponentMap } from '../help';
  8 + import { FrontComponent } from '../../const/const';
5 9
6 const props = defineProps<{ 10 const props = defineProps<{
  11 + record: DataComponentRecord;
7 dataSource: FrontDataSourceRecord[]; 12 dataSource: FrontDataSourceRecord[];
8 }>(); 13 }>();
9 14
  15 + const isMultipleDataSource = computed(() => {
  16 + const { record } = props;
  17 + const componentInfo = frontComponentMap.get(record.frontId as FrontComponent);
  18 + return componentInfo?.isMultipleDataSource;
  19 + });
  20 +
10 const { update, add, remove } = useUpdateCenter(); 21 const { update, add, remove } = useUpdateCenter();
11 22
12 createVisualBoardContext({ update, add, remove }); 23 createVisualBoardContext({ update, add, remove });
@@ -19,24 +30,41 @@ @@ -19,24 +30,41 @@
19 <slot name="header"></slot> 30 <slot name="header"></slot>
20 31
21 <div class="widget-content"> 32 <div class="widget-content">
22 - <div  
23 - v-for="item in props.dataSource"  
24 - :key="item.id"  
25 - :style="{ width: `${item.width || 100}%`, height: `${item.height || 100}%` }"  
26 - class="widget-item"  
27 - >  
28 - <div class="widget-box">  
29 - <div class="widget-controls-container">  
30 - <slot  
31 - name="controls"  
32 - :record="item"  
33 - :add="add"  
34 - :remove="remove"  
35 - :update="update"  
36 - ></slot> 33 + <template v-if="!isMultipleDataSource">
  34 + <div
  35 + v-for="item in props.dataSource"
  36 + :key="item.id"
  37 + :style="{ width: `${item.width || 100}%`, height: `${item.height || 100}%` }"
  38 + class="widget-item"
  39 + >
  40 + <div class="widget-box">
  41 + <div class="widget-controls-container">
  42 + <slot
  43 + name="controls"
  44 + :record="item"
  45 + :add="add"
  46 + :remove="remove"
  47 + :update="update"
  48 + ></slot>
  49 + </div>
  50 + </div>
  51 + </div>
  52 + </template>
  53 + <template v-if="isMultipleDataSource">
  54 + <div :style="{ width: `${100}%`, height: `${100}%` }" class="widget-item">
  55 + <div class="widget-box">
  56 + <div class="widget-controls-container">
  57 + <slot
  58 + name="controls"
  59 + :record="props.dataSource"
  60 + :add="add"
  61 + :remove="remove"
  62 + :update="update"
  63 + ></slot>
  64 + </div>
37 </div> 65 </div>
38 </div> 66 </div>
39 - </div> 67 + </template>
40 </div> 68 </div>
41 <slot name="footer"></slot> 69 <slot name="footer"></slot>
42 </section> 70 </section>
@@ -45,6 +45,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_1, { @@ -45,6 +45,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_1, {
45 ComponentKey: FrontComponent.TEXT_COMPONENT_1, 45 ComponentKey: FrontComponent.TEXT_COMPONENT_1,
46 ComponentConfig: TextComponent1Config, 46 ComponentConfig: TextComponent1Config,
47 ComponentCategory: FrontComponentCategory.TEXT, 47 ComponentCategory: FrontComponentCategory.TEXT,
  48 + isMultipleDataSource: false,
  49 + hasHistoryTrend: true,
  50 + hasSetting: true,
48 transformConfig: transformTextComponentConfig, 51 transformConfig: transformTextComponentConfig,
49 }); 52 });
50 53
@@ -54,6 +57,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_3, { @@ -54,6 +57,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_3, {
54 ComponentKey: FrontComponent.TEXT_COMPONENT_3, 57 ComponentKey: FrontComponent.TEXT_COMPONENT_3,
55 ComponentConfig: TextComponent3Config, 58 ComponentConfig: TextComponent3Config,
56 ComponentCategory: FrontComponentCategory.TEXT, 59 ComponentCategory: FrontComponentCategory.TEXT,
  60 + isMultipleDataSource: false,
  61 + hasHistoryTrend: true,
  62 + hasSetting: true,
57 transformConfig: transformTextComponentConfig, 63 transformConfig: transformTextComponentConfig,
58 }); 64 });
59 65
@@ -63,6 +69,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_4, { @@ -63,6 +69,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_4, {
63 ComponentKey: FrontComponent.TEXT_COMPONENT_4, 69 ComponentKey: FrontComponent.TEXT_COMPONENT_4,
64 ComponentConfig: TextComponent4Config, 70 ComponentConfig: TextComponent4Config,
65 ComponentCategory: FrontComponentCategory.TEXT, 71 ComponentCategory: FrontComponentCategory.TEXT,
  72 + isMultipleDataSource: false,
  73 + hasHistoryTrend: true,
  74 + hasSetting: true,
66 transformConfig: transformTextComponentConfig, 75 transformConfig: transformTextComponentConfig,
67 }); 76 });
68 77
@@ -72,6 +81,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_5, { @@ -72,6 +81,9 @@ frontComponentMap.set(FrontComponent.TEXT_COMPONENT_5, {
72 ComponentKey: FrontComponent.TEXT_COMPONENT_5, 81 ComponentKey: FrontComponent.TEXT_COMPONENT_5,
73 ComponentConfig: TextComponent5Config, 82 ComponentConfig: TextComponent5Config,
74 ComponentCategory: FrontComponentCategory.TEXT, 83 ComponentCategory: FrontComponentCategory.TEXT,
  84 + isMultipleDataSource: false,
  85 + hasHistoryTrend: true,
  86 + hasSetting: true,
75 transformConfig: transformTextComponentConfig, 87 transformConfig: transformTextComponentConfig,
76 }); 88 });
77 89
@@ -84,6 +96,9 @@ frontComponentMap.set(FrontComponent.INSTRUMENT_COMPONENT_1, { @@ -84,6 +96,9 @@ frontComponentMap.set(FrontComponent.INSTRUMENT_COMPONENT_1, {
84 componentType: FrontComponent.INSTRUMENT_COMPONENT_1, 96 componentType: FrontComponent.INSTRUMENT_COMPONENT_1,
85 } as DashboardComponentLayout, 97 } as DashboardComponentLayout,
86 ComponentCategory: FrontComponentCategory.INSTRUMENT, 98 ComponentCategory: FrontComponentCategory.INSTRUMENT,
  99 + isMultipleDataSource: false,
  100 + hasHistoryTrend: true,
  101 + hasSetting: true,
87 transformConfig: transformDashboardComponentConfig, 102 transformConfig: transformDashboardComponentConfig,
88 }); 103 });
89 104
@@ -96,6 +111,9 @@ frontComponentMap.set(FrontComponent.INSTRUMENT_COMPONENT_2, { @@ -96,6 +111,9 @@ frontComponentMap.set(FrontComponent.INSTRUMENT_COMPONENT_2, {
96 componentType: FrontComponent.INSTRUMENT_COMPONENT_2, 111 componentType: FrontComponent.INSTRUMENT_COMPONENT_2,
97 } as DashboardComponentLayout, 112 } as DashboardComponentLayout,
98 ComponentCategory: FrontComponentCategory.INSTRUMENT, 113 ComponentCategory: FrontComponentCategory.INSTRUMENT,
  114 + isMultipleDataSource: false,
  115 + hasHistoryTrend: true,
  116 + hasSetting: true,
99 transformConfig: transformDashboardComponentConfig, 117 transformConfig: transformDashboardComponentConfig,
100 }); 118 });
101 119
@@ -104,6 +122,9 @@ frontComponentMap.set(FrontComponent.DIGITAL_DASHBOARD_COMPONENT, { @@ -104,6 +122,9 @@ frontComponentMap.set(FrontComponent.DIGITAL_DASHBOARD_COMPONENT, {
104 ComponentName: '数字仪表盘', 122 ComponentName: '数字仪表盘',
105 ComponentKey: FrontComponent.DIGITAL_DASHBOARD_COMPONENT, 123 ComponentKey: FrontComponent.DIGITAL_DASHBOARD_COMPONENT,
106 ComponentCategory: FrontComponentCategory.INSTRUMENT, 124 ComponentCategory: FrontComponentCategory.INSTRUMENT,
  125 + isMultipleDataSource: false,
  126 + hasHistoryTrend: true,
  127 + hasSetting: true,
107 transformConfig: transformDashboardComponentConfig, 128 transformConfig: transformDashboardComponentConfig,
108 }); 129 });
109 130
@@ -112,6 +133,9 @@ frontComponentMap.set(FrontComponent.PICTURE_COMPONENT_1, { @@ -112,6 +133,9 @@ frontComponentMap.set(FrontComponent.PICTURE_COMPONENT_1, {
112 ComponentName: '图片组件', 133 ComponentName: '图片组件',
113 ComponentKey: FrontComponent.PICTURE_COMPONENT_1, 134 ComponentKey: FrontComponent.PICTURE_COMPONENT_1,
114 ComponentCategory: FrontComponentCategory.PICTURE, 135 ComponentCategory: FrontComponentCategory.PICTURE,
  136 + isMultipleDataSource: false,
  137 + hasHistoryTrend: true,
  138 + hasSetting: false,
115 transformConfig: transformPictureConfig, 139 transformConfig: transformPictureConfig,
116 }); 140 });
117 141
@@ -120,6 +144,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON, { @@ -120,6 +144,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON, {
120 ComponentName: '控制按钮1', 144 ComponentName: '控制按钮1',
121 ComponentKey: FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON, 145 ComponentKey: FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON,
122 ComponentCategory: FrontComponentCategory.CONTROL, 146 ComponentCategory: FrontComponentCategory.CONTROL,
  147 + isMultipleDataSource: false,
  148 + hasHistoryTrend: true,
  149 + hasSetting: true,
123 transformConfig: transformControlConfig, 150 transformConfig: transformControlConfig,
124 }); 151 });
125 152
@@ -128,6 +155,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH, { @@ -128,6 +155,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH, {
128 ComponentName: '控制按钮2', 155 ComponentName: '控制按钮2',
129 ComponentKey: FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH, 156 ComponentKey: FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH,
130 ComponentCategory: FrontComponentCategory.CONTROL, 157 ComponentCategory: FrontComponentCategory.CONTROL,
  158 + isMultipleDataSource: false,
  159 + hasHistoryTrend: true,
  160 + hasSetting: false,
131 transformConfig: transformControlConfig, 161 transformConfig: transformControlConfig,
132 }); 162 });
133 163
@@ -136,6 +166,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, { @@ -136,6 +166,9 @@ frontComponentMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, {
136 ComponentName: '控制按钮3', 166 ComponentName: '控制按钮3',
137 ComponentKey: FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, 167 ComponentKey: FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH,
138 ComponentCategory: FrontComponentCategory.CONTROL, 168 ComponentCategory: FrontComponentCategory.CONTROL,
  169 + isMultipleDataSource: false,
  170 + hasHistoryTrend: true,
  171 + hasSetting: false,
139 transformConfig: transformControlConfig, 172 transformConfig: transformControlConfig,
140 }); 173 });
141 174
@@ -145,6 +178,9 @@ frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_REAL, { @@ -145,6 +178,9 @@ frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_REAL, {
145 ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_REAL, 178 ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_REAL,
146 ComponentConfig: MapRealTrackConfig, 179 ComponentConfig: MapRealTrackConfig,
147 ComponentCategory: FrontComponentCategory.MAP, 180 ComponentCategory: FrontComponentCategory.MAP,
  181 + isMultipleDataSource: true,
  182 + hasHistoryTrend: false,
  183 + hasSetting: false,
148 transformConfig: transfromMapComponentConfig, 184 transformConfig: transfromMapComponentConfig,
149 }); 185 });
150 186
@@ -154,6 +190,9 @@ frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_HISTORY, { @@ -154,6 +190,9 @@ frontComponentMap.set(FrontComponent.MAP_COMPONENT_TRACK_HISTORY, {
154 ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_HISTORY, 190 ComponentKey: FrontComponent.MAP_COMPONENT_TRACK_HISTORY,
155 ComponentConfig: MaphistoryTrackConfig, 191 ComponentConfig: MaphistoryTrackConfig,
156 ComponentCategory: FrontComponentCategory.MAP, 192 ComponentCategory: FrontComponentCategory.MAP,
  193 + isMultipleDataSource: true,
  194 + hasHistoryTrend: false,
  195 + hasSetting: false,
157 transformConfig: transfromMapComponentConfig, 196 transformConfig: transfromMapComponentConfig,
158 }); 197 });
159 198
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 - import { CopyOutlined, DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue'; 2 + import {
  3 + CopyOutlined,
  4 + DeleteOutlined,
  5 + SettingOutlined,
  6 + SwapOutlined,
  7 + } from '@ant-design/icons-vue';
3 import { Tooltip, Button } from 'ant-design-vue'; 8 import { Tooltip, Button } from 'ant-design-vue';
4 import { FormActionType, useForm } from '/@/components/Form'; 9 import { FormActionType, useForm } from '/@/components/Form';
5 - import { basicSchema } from '../config/basicConfiguration'; 10 + import { basicSchema, DataSourceField } from '../config/basicConfiguration';
6 import BasicForm from '/@/components/Form/src/BasicForm.vue'; 11 import BasicForm from '/@/components/Form/src/BasicForm.vue';
7 - import { ref, shallowReactive, unref, nextTick, watch, computed } from 'vue'; 12 + import { ref, shallowReactive, unref, nextTick, watch, computed, onMounted } from 'vue';
8 import VisualOptionsModal from './VisualOptionsModal.vue'; 13 import VisualOptionsModal from './VisualOptionsModal.vue';
9 import { useModal } from '/@/components/Modal'; 14 import { useModal } from '/@/components/Modal';
10 import { buildUUID } from '/@/utils/uuid'; 15 import { buildUUID } from '/@/utils/uuid';
@@ -13,6 +18,10 @@ @@ -13,6 +18,10 @@
13 import { DataBoardLayoutInfo } from '../../types/type'; 18 import { DataBoardLayoutInfo } from '../../types/type';
14 import { getDataSourceComponent } from './DataSourceForm/help'; 19 import { getDataSourceComponent } from './DataSourceForm/help';
15 import { FrontComponent } from '../../const/const'; 20 import { FrontComponent } from '../../const/const';
  21 + import { isNullAndUnDef } from '/@/utils/is';
  22 + import { useSortable } from '/@/hooks/web/useSortable';
  23 + import { cloneDeep } from 'lodash-es';
  24 + import { frontComponentMap } from '../../components/help';
16 25
17 type DataSourceFormEL = { [key: string]: Nullable<FormActionType> }; 26 type DataSourceFormEL = { [key: string]: Nullable<FormActionType> };
18 27
@@ -61,27 +70,55 @@ @@ -61,27 +70,55 @@
61 70
62 const validateDataSourceField = async () => { 71 const validateDataSourceField = async () => {
63 const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]); 72 const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]);
64 - const _dataSource: boolean[] = []; 73 + const _dataSource: Record<DataSourceField, string>[] = [];
65 for (const id of hasExistEl) { 74 for (const id of hasExistEl) {
66 - const flag = (await (dataSourceEl[id] as FormActionType).validate()) as boolean;  
67 - _dataSource.push(flag); 75 + const flag = (await (dataSourceEl[id] as FormActionType).validate()) as Record<
  76 + DataSourceField,
  77 + string
  78 + >;
  79 + flag && _dataSource.push(flag);
  80 + }
  81 +
  82 + if (
  83 + [
  84 + FrontComponent.MAP_COMPONENT_TRACK_HISTORY,
  85 + FrontComponent.MAP_COMPONENT_TRACK_REAL,
  86 + ].includes(props.frontId!)
  87 + ) {
  88 + await validateMapComponent(_dataSource);
68 } 89 }
69 return _dataSource; 90 return _dataSource;
70 }; 91 };
71 92
  93 + const validateMapComponent = async (dataSource: Record<DataSourceField, string>[]) => {
  94 + if (dataSource.length) {
  95 + const firstRecord = dataSource.at(0)!;
  96 + const { deviceId, slaveDeviceId } = firstRecord;
  97 + const flag = dataSource.every(
  98 + (item) => item.deviceId === deviceId && item.slaveDeviceId === slaveDeviceId
  99 + );
  100 + if (!flag) {
  101 + createMessage.warning('地图组件绑定的数据源应该一致');
  102 + return Promise.reject(false);
  103 + }
  104 + }
  105 + };
  106 +
72 const getDataSourceField = () => { 107 const getDataSourceField = () => {
73 const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]); 108 const hasExistEl = Object.keys(dataSourceEl).filter((key) => dataSourceEl[key]);
74 const _dataSource: DataSource[] = []; 109 const _dataSource: DataSource[] = [];
  110 +
75 for (const id of hasExistEl) { 111 for (const id of hasExistEl) {
76 const index = unref(dataSource).findIndex((item) => item.id === id); 112 const index = unref(dataSource).findIndex((item) => item.id === id);
77 const value = (dataSourceEl[id] as FormActionType).getFieldsValue() as DataSource; 113 const value = (dataSourceEl[id] as FormActionType).getFieldsValue() as DataSource;
78 if (!~index) continue; 114 if (!~index) continue;
79 const componentInfo = unref(dataSource)[index].componentInfo || {}; 115 const componentInfo = unref(dataSource)[index].componentInfo || {};
80 - _dataSource.unshift({ 116 + _dataSource[index] = {
81 ...value, 117 ...value,
82 componentInfo: { ...(props.defaultConfig || {}), ...componentInfo }, 118 componentInfo: { ...(props.defaultConfig || {}), ...componentInfo },
83 - }); 119 + };
84 } 120 }
  121 +
85 return _dataSource; 122 return _dataSource;
86 }; 123 };
87 124
@@ -160,7 +197,8 @@ @@ -160,7 +197,8 @@
160 }; 197 };
161 198
162 const showSettingButton = computed(() => { 199 const showSettingButton = computed(() => {
163 - return props.frontId !== FrontComponent.PICTURE_COMPONENT_1; 200 + const flag = frontComponentMap.get(props.frontId!)?.hasSetting;
  201 + return flag;
164 }); 202 });
165 203
166 watch( 204 watch(
@@ -179,6 +217,41 @@ @@ -179,6 +217,41 @@
179 return getDataSourceComponent(props.frontId as FrontComponent); 217 return getDataSourceComponent(props.frontId as FrontComponent);
180 }); 218 });
181 219
  220 + let inited = false;
  221 + const formListEl = ref<HTMLElement>();
  222 + async function handleSort() {
  223 + if (inited) return;
  224 + await nextTick();
  225 + const formList = unref(formListEl);
  226 + if (!formList) return;
  227 +
  228 + const { initSortable } = useSortable(unref(formList), {
  229 + handle: '.sort-icon',
  230 + onEnd: (evt) => {
  231 + const { oldIndex, newIndex } = evt;
  232 + if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
  233 + return;
  234 + }
  235 +
  236 + const _dataSource = cloneDeep(unref(dataSource));
  237 +
  238 + if (oldIndex > newIndex) {
  239 + _dataSource.splice(newIndex, 0, _dataSource[oldIndex]);
  240 + _dataSource.splice(oldIndex + 1, 1);
  241 + } else {
  242 + _dataSource.splice(newIndex + 1, 0, _dataSource[oldIndex]);
  243 + _dataSource.splice(oldIndex, 1);
  244 + }
  245 +
  246 + dataSource.value = _dataSource;
  247 + },
  248 + });
  249 + initSortable();
  250 + inited = true;
  251 + }
  252 +
  253 + onMounted(() => handleSort());
  254 +
182 defineExpose({ 255 defineExpose({
183 getAllDataSourceFieldValue, 256 getAllDataSourceFieldValue,
184 validate, 257 validate,
@@ -192,36 +265,47 @@ @@ -192,36 +265,47 @@
192 <BasicForm @register="basicRegister" class="w-full" /> 265 <BasicForm @register="basicRegister" class="w-full" />
193 </div> 266 </div>
194 <h3 class="w-24 flex-shrink-0 text-right pr-2 my-4">选择数据源</h3> 267 <h3 class="w-24 flex-shrink-0 text-right pr-2 my-4">选择数据源</h3>
195 - <div v-for="item in dataSource" :key="item.id" class="flex">  
196 - <div  
197 - class="w-24 text-right leading-30px pr-8px flex right"  
198 - style="flex: 0 0 96px; justify-content: right"  
199 - >  
200 - 选择设备  
201 - </div>  
202 - <div class="pl-2 flex-auto">  
203 - <!-- <BasicDataSourceForm /> -->  
204 - <component :is="dataSourceComponent" :ref="(el) => setFormEl(el, item.id)" />  
205 - </div>  
206 - <div class="flex justify-center gap-3 w-24">  
207 - <Tooltip title="复制">  
208 - <CopyOutlined @click="handleCopy(item)" class="cursor-pointer text-lg !leading-32px" />  
209 - </Tooltip>  
210 - <Tooltip title="设置">  
211 - <SettingOutlined  
212 - v-show="showSettingButton"  
213 - @click="handleSetting(item)"  
214 - class="cursor-pointer text-lg !leading-32px"  
215 - />  
216 - </Tooltip>  
217 - <Tooltip title="删除">  
218 - <DeleteOutlined  
219 - @click="handleDelete(item)"  
220 - class="cursor-pointer text-lg !leading-32px" 268 + <section ref="formListEl">
  269 + <div v-for="item in dataSource" :data-id="item.id" :key="item.id" class="flex bg-light-50">
  270 + <div
  271 + class="w-24 text-right leading-30px pr-8px flex right"
  272 + style="flex: 0 0 96px; justify-content: right"
  273 + >
  274 + 选择设备
  275 + </div>
  276 + <div class="pl-2 flex-auto">
  277 + <component
  278 + :frontId="$props.frontId"
  279 + :is="dataSourceComponent"
  280 + :ref="(el) => setFormEl(el, item.id)"
221 /> 281 />
222 - </Tooltip> 282 + </div>
  283 + <div class="flex justify-center gap-3 w-28">
  284 + <Tooltip title="复制">
  285 + <CopyOutlined @click="handleCopy(item)" class="cursor-pointer text-lg !leading-32px" />
  286 + </Tooltip>
  287 + <Tooltip title="设置">
  288 + <SettingOutlined
  289 + v-show="showSettingButton"
  290 + @click="handleSetting(item)"
  291 + class="cursor-pointer text-lg !leading-32px"
  292 + />
  293 + </Tooltip>
  294 + <Tooltip title="拖拽排序">
  295 + <SwapOutlined
  296 + class="cursor-pointer text-lg !leading-32px svg:transform svg:rotate-90 sort-icon"
  297 + />
  298 + </Tooltip>
  299 + <Tooltip title="删除">
  300 + <DeleteOutlined
  301 + @click="handleDelete(item)"
  302 + class="cursor-pointer text-lg !leading-32px"
  303 + />
  304 + </Tooltip>
  305 + </div>
223 </div> 306 </div>
224 - </div> 307 + </section>
  308 +
225 <div class="text-center"> 309 <div class="text-center">
226 <Button type="primary" @click="handleAdd">添加数据源</Button> 310 <Button type="primary" @click="handleAdd">添加数据源</Button>
227 </div> 311 </div>
@@ -66,6 +66,7 @@ @@ -66,6 +66,7 @@
66 const { getAllDataSourceFieldValue, validate } = unref(basicConfigurationEl)!; 66 const { getAllDataSourceFieldValue, validate } = unref(basicConfigurationEl)!;
67 await validate(); 67 await validate();
68 const value = getAllDataSourceFieldValue(); 68 const value = getAllDataSourceFieldValue();
  69 +
69 unref(isEdit) ? handleUpdateComponent(value) : handleAddComponent(value); 70 unref(isEdit) ? handleUpdateComponent(value) : handleAddComponent(value);
70 resetForm(); 71 resetForm();
71 } catch (error: unknown) { 72 } catch (error: unknown) {
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 import { ref } from 'vue'; 2 import { ref } from 'vue';
  3 + import { FrontComponent } from '../../../const/const';
3 import { dataSourceSchema } from '../../config/basicConfiguration'; 4 import { dataSourceSchema } from '../../config/basicConfiguration';
4 import { FormActionType } from '/@/components/Form'; 5 import { FormActionType } from '/@/components/Form';
5 import BasicForm from '/@/components/Form/src/BasicForm.vue'; 6 import BasicForm from '/@/components/Form/src/BasicForm.vue';
6 const formEl = ref<Nullable<FormActionType>>(null); 7 const formEl = ref<Nullable<FormActionType>>(null);
7 8
  9 + defineProps<{
  10 + frontId?: FrontComponent;
  11 + }>();
  12 +
8 defineExpose({ formActionType: formEl }); 13 defineExpose({ formActionType: formEl });
9 </script> 14 </script>
10 15
11 <template> 16 <template>
12 <BasicForm 17 <BasicForm
13 ref="formEl" 18 ref="formEl"
14 - :schemas="dataSourceSchema" 19 + :schemas="dataSourceSchema($props.frontId)"
15 class="w-full flex-1 data-source-form" 20 class="w-full flex-1 data-source-form"
16 :show-action-button-group="false" 21 :show-action-button-group="false"
17 :row-props="{ 22 :row-props="{
1 <script lang="ts" setup> 1 <script lang="ts" setup>
2 import { ref, unref } from 'vue'; 2 import { ref, unref } from 'vue';
3 import { BasicForm, FormActionType } from '/@/components/Form'; 3 import { BasicForm, FormActionType } from '/@/components/Form';
4 - import { controlFormSchema } from '../../config/basicConfiguration'; 4 + import { dataSourceSchema } from '../../config/basicConfiguration';
  5 + import { FrontComponent } from '../../../const/const';
  6 +
  7 + defineProps<{
  8 + frontId?: FrontComponent;
  9 + }>();
5 10
6 const formEl = ref<Nullable<FormActionType>>(); 11 const formEl = ref<Nullable<FormActionType>>();
7 12
@@ -33,7 +38,7 @@ @@ -33,7 +38,7 @@
33 <div class="w-full flex-1"> 38 <div class="w-full flex-1">
34 <BasicForm 39 <BasicForm
35 :ref="(el) => setFormEl(el)" 40 :ref="(el) => setFormEl(el)"
36 - :schemas="controlFormSchema" 41 + :schemas="dataSourceSchema($props.frontId)"
37 class="w-full flex-1 data-source-form" 42 class="w-full flex-1 data-source-form"
38 :show-action-button-group="false" 43 :show-action-button-group="false"
39 :row-props="{ 44 :row-props="{
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>  
1 import { Component } from 'vue'; 1 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';  
5 -import MapDataSourceForm from './MapDataSourceForm.vue'; 4 +// import ControlDataSourceForm from './ControlDataSourceForm.vue';
6 5
7 const dataSourceComponentMap = new Map<FrontComponent, Component>(); 6 const dataSourceComponentMap = new Map<FrontComponent, Component>();
8 7
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);  
12 -  
13 export const getDataSourceComponent = (frontId: FrontComponent) => { 8 export const getDataSourceComponent = (frontId: FrontComponent) => {
14 if (dataSourceComponentMap.has(frontId)) return dataSourceComponentMap.get(frontId)!; 9 if (dataSourceComponentMap.has(frontId)) return dataSourceComponentMap.get(frontId)!;
15 return BasicDataSourceForm; 10 return BasicDataSourceForm;
@@ -8,20 +8,24 @@ @@ -8,20 +8,24 @@
8 import { AggregateDataEnum, eChartOptions } from '/@/views/device/localtion/config.data'; 8 import { AggregateDataEnum, eChartOptions } from '/@/views/device/localtion/config.data';
9 import { useGridLayout } from '/@/hooks/component/useGridLayout'; 9 import { useGridLayout } from '/@/hooks/component/useGridLayout';
10 import { ColEx } from '/@/components/Form/src/types'; 10 import { ColEx } from '/@/components/Form/src/types';
11 - import { DataSource } from '/@/api/dataBoard/model'; 11 + import { DataSource, DeviceAttributeRecord } from '/@/api/dataBoard/model';
12 import { useForm, BasicForm } from '/@/components/Form'; 12 import { useForm, BasicForm } from '/@/components/Form';
13 import { formSchema, QueryWay, SchemaFiled } from '../config/historyTrend.config'; 13 import { formSchema, QueryWay, SchemaFiled } from '../config/historyTrend.config';
14 import { DEFAULT_DATE_FORMAT } from '../config/util'; 14 import { DEFAULT_DATE_FORMAT } from '../config/util';
15 import { Loading } from '/@/components/Loading'; 15 import { Loading } from '/@/components/Loading';
16 import BasicModal from '/@/components/Modal/src/BasicModal.vue'; 16 import BasicModal from '/@/components/Modal/src/BasicModal.vue';
17 import { useModalInner } from '/@/components/Modal'; 17 import { useModalInner } from '/@/components/Modal';
18 - import { getDeviceAttributes } from '/@/api/dataBoard'; 18 + import { getAllDeviceByOrg, getDeviceAttributes } from '/@/api/dataBoard';
  19 + import { HistoryData } from '/@/api/alarm/position/model';
  20 + import { EChartsOption } from 'echarts';
  21 +
  22 + type DeviceOption = Record<'label' | 'value' | 'organizationId', string>;
19 23
20 defineEmits(['register']); 24 defineEmits(['register']);
21 25
22 const chartRef = ref(); 26 const chartRef = ref();
23 27
24 - const deviceAttrs = ref<string[]>([]); 28 + const deviceAttrs = ref<DeviceAttributeRecord[]>([]);
25 29
26 const loading = ref(false); 30 const loading = ref(false);
27 31
@@ -29,26 +33,27 @@ @@ -29,26 +33,27 @@
29 33
30 function getSearchParams(value: Recordable) { 34 function getSearchParams(value: Recordable) {
31 const { startTs, endTs, interval, agg, limit, keys, way, deviceId } = value; 35 const { startTs, endTs, interval, agg, limit, keys, way, deviceId } = value;
  36 + const basicRecord = {
  37 + entityId: deviceId,
  38 + keys: keys
  39 + ? keys
  40 + : unref(deviceAttrs)
  41 + .map((item) => item.identifier)
  42 + .join(),
  43 + interval,
  44 + agg,
  45 + limit,
  46 + };
32 if (way === QueryWay.LATEST) { 47 if (way === QueryWay.LATEST) {
33 - return {  
34 - entityId: deviceId,  
35 - keys: keys ? keys : unref(deviceAttrs).join(), 48 + return Object.assign(basicRecord, {
36 startTs: moment().subtract(startTs, 'ms').valueOf(), 49 startTs: moment().subtract(startTs, 'ms').valueOf(),
37 endTs: Date.now(), 50 endTs: Date.now(),
38 - interval,  
39 - agg,  
40 - limit,  
41 - }; 51 + });
42 } else { 52 } else {
43 - return {  
44 - entityId: deviceId,  
45 - keys: keys ? keys : unref(deviceAttrs).join(), 53 + return Object.assign(basicRecord, {
46 startTs: moment(startTs).valueOf(), 54 startTs: moment(startTs).valueOf(),
47 endTs: moment(endTs).valueOf(), 55 endTs: moment(endTs).valueOf(),
48 - interval,  
49 - agg,  
50 - limit,  
51 - }; 56 + });
52 } 57 }
53 } 58 }
54 59
@@ -60,24 +65,24 @@ @@ -60,24 +65,24 @@
60 } 65 }
61 } 66 }
62 67
63 - function setChartOptions(data, keys?) { 68 + function setChartOptions(data: HistoryData, keys?: string | string[]) {
64 const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>); 69 const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
65 70
66 - const dataArray: any[] = []; 71 + const dataArray: [string, string, string][] = [];
67 for (const key in data) { 72 for (const key in data) {
68 for (const item1 of data[key]) { 73 for (const item1 of data[key]) {
69 let { ts, value } = item1; 74 let { ts, value } = item1;
70 const time = dateUtil(ts).format(DEFAULT_DATE_FORMAT); 75 const time = dateUtil(ts).format(DEFAULT_DATE_FORMAT);
71 value = Number(value).toFixed(2); 76 value = Number(value).toFixed(2);
72 - dataArray.push([time, value, key]); 77 + dataArray.push([time, value, key as string]);
73 } 78 }
74 } 79 }
75 - keys = keys ? [keys] : unref(deviceAttrs);  
76 - const series: any = keys.map((item) => { 80 + keys = keys ? [keys as string] : unref(deviceAttrs).map((item) => item.identifier);
  81 + const series: EChartsOption['series'] = (keys as string[]).map((item) => {
77 return { 82 return {
78 name: item, 83 name: item,
79 type: 'line', 84 type: 'line',
80 - data: dataArray.filter((item1) => item1[2] === item), 85 + data: dataArray.filter((temp) => temp[2] === item),
81 }; 86 };
82 }); 87 });
83 // 设置数据 88 // 设置数据
@@ -118,18 +123,23 @@ @@ -118,18 +123,23 @@
118 }, 123 },
119 }); 124 });
120 125
121 - const getDeviceDataKey = async (deviceId: string) => {  
122 - if (!deviceId) return; 126 + const getDeviceDataKey = async (record: DeviceOption) => {
  127 + const { organizationId, value } = record;
123 try { 128 try {
124 - deviceAttrs.value = (await getDeviceAttributes({ deviceId })) || []; 129 + const options = await getAllDeviceByOrg(organizationId);
  130 + const record = options.find((item) => item.id === value);
  131 + const { deviceProfileId } = record!;
  132 + deviceAttrs.value = (await getDeviceAttributes({ deviceProfileId })) || [];
125 await nextTick(); 133 await nextTick();
126 method.updateSchema({ 134 method.updateSchema({
127 field: SchemaFiled.KEYS, 135 field: SchemaFiled.KEYS,
128 componentProps: { 136 componentProps: {
129 - options: unref(deviceAttrs).map((item) => ({ label: item, value: item })), 137 + options: unref(deviceAttrs).map((item) => ({ label: item.name, value: item.identifier })),
130 }, 138 },
131 }); 139 });
132 - } catch (error) {} 140 + } catch (error) {
  141 + throw error;
  142 + }
133 }; 143 };
134 144
135 const handleModalOpen = async () => { 145 const handleModalOpen = async () => {
@@ -147,7 +157,9 @@ @@ -147,7 +157,9 @@
147 157
148 const res = await getDeviceHistoryInfo({ 158 const res = await getDeviceHistoryInfo({
149 entityId: deviceId, 159 entityId: deviceId,
150 - keys: unref(deviceAttrs).join(), 160 + keys: unref(deviceAttrs)
  161 + .map((item) => item.identifier)
  162 + .join(),
151 startTs: Date.now() - 1 * 24 * 60 * 60 * 1000, 163 startTs: Date.now() - 1 * 24 * 60 * 60 * 1000,
152 endTs: Date.now(), 164 endTs: Date.now(),
153 agg: AggregateDataEnum.NONE, 165 agg: AggregateDataEnum.NONE,
@@ -166,9 +178,10 @@ @@ -166,9 +178,10 @@
166 178
167 const generateDeviceOptions = (dataSource: DataSource[]) => { 179 const generateDeviceOptions = (dataSource: DataSource[]) => {
168 const record: { [key: string]: boolean } = {}; 180 const record: { [key: string]: boolean } = {};
169 - const options: Record<'label' | 'value', string>[] = []; 181 +
  182 + const options: DeviceOption[] = [];
170 for (const item of dataSource) { 183 for (const item of dataSource) {
171 - const { deviceName, gatewayDevice, slaveDeviceId } = item; 184 + const { deviceName, gatewayDevice, slaveDeviceId, organizationId } = item;
172 let { deviceId } = item; 185 let { deviceId } = item;
173 if (gatewayDevice) { 186 if (gatewayDevice) {
174 deviceId = slaveDeviceId; 187 deviceId = slaveDeviceId;
@@ -177,6 +190,7 @@ @@ -177,6 +190,7 @@
177 options.push({ 190 options.push({
178 label: deviceName, 191 label: deviceName,
179 value: deviceId, 192 value: deviceId,
  193 + organizationId,
180 }); 194 });
181 record[deviceId] = true; 195 record[deviceId] = true;
182 } 196 }
@@ -195,8 +209,8 @@ @@ -195,8 +209,8 @@
195 const { setFieldsValue } = formActionType; 209 const { setFieldsValue } = formActionType;
196 return { 210 return {
197 options, 211 options,
198 - onChange(value: string) {  
199 - getDeviceDataKey(value); 212 + onChange(_, record: DeviceOption) {
  213 + getDeviceDataKey(record);
200 setFieldsValue({ [SchemaFiled.KEYS]: null }); 214 setFieldsValue({ [SchemaFiled.KEYS]: null });
201 }, 215 },
202 }; 216 };
@@ -204,17 +218,10 @@ @@ -204,17 +218,10 @@
204 }); 218 });
205 219
206 if (options.length && options.at(0)?.value) { 220 if (options.length && options.at(0)?.value) {
207 - const value = options.at(0)!.value;  
208 - getDeviceDataKey(value); 221 + const record = options.at(0)!;
  222 + await getDeviceDataKey(record);
209 try { 223 try {
210 - deviceAttrs.value = (await getDeviceAttributes({ deviceId: value })) || [];  
211 - method.updateSchema({  
212 - field: SchemaFiled.KEYS,  
213 - componentProps: {  
214 - options: unref(deviceAttrs).map((item) => ({ label: item, value: item })),  
215 - },  
216 - });  
217 - await method.setFieldsValue({ [SchemaFiled.DEVICE_ID]: value }); 224 + await method.setFieldsValue({ [SchemaFiled.DEVICE_ID]: record.value });
218 } catch (error) {} 225 } catch (error) {}
219 } 226 }
220 227
@@ -231,7 +238,7 @@ @@ -231,7 +238,7 @@
231 <section class="bg-white my-3 p-2"> 238 <section class="bg-white my-3 p-2">
232 <BasicForm @register="register" /> 239 <BasicForm @register="register" />
233 </section> 240 </section>
234 - <section class="bg-white p-3" style="min-hight: 350px"> 241 + <section class="bg-white p-3" style="min-height: 350px">
235 <div v-show="isNull" ref="chartRef" :style="{ height: '350px', width: '100%' }"> 242 <div v-show="isNull" ref="chartRef" :style="{ height: '350px', width: '100%' }">
236 <Loading :loading="loading" :absolute="true" /> 243 <Loading :loading="loading" :absolute="true" />
237 </div> 244 </div>
@@ -2,18 +2,18 @@ import { getAllDeviceByOrg, getDeviceAttributes, getGatewaySlaveDevice } from '/ @@ -2,18 +2,18 @@ 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 +import { DeviceAttributeParams, MasterDeviceList } from '/@/api/dataBoard/model';
  6 +import { FrontComponent } from '../../const/const';
7 7
8 export enum BasicConfigField { 8 export enum BasicConfigField {
9 NAME = 'name', 9 NAME = 'name',
10 REMARK = 'remark', 10 REMARK = 'remark',
11 } 11 }
12 12
13 -const getDeviceAttribute = async (deviceId: string) => { 13 +const getDeviceAttribute = async (params: DeviceAttributeParams) => {
14 try { 14 try {
15 - const data = await getDeviceAttributes({ deviceId });  
16 - if (data) return data.map((item) => ({ label: item, value: item })); 15 + const data = await getDeviceAttributes(params);
  16 + if (data) return data.map((item) => ({ label: item.name, value: item.identifier }));
17 } catch (error) {} 17 } catch (error) {}
18 return []; 18 return [];
19 }; 19 };
@@ -23,6 +23,8 @@ export enum DataSourceField { @@ -23,6 +23,8 @@ export enum DataSourceField {
23 ORIGINATION_ID = 'organizationId', 23 ORIGINATION_ID = 'organizationId',
24 DEVICE_ID = 'deviceId', 24 DEVICE_ID = 'deviceId',
25 SLAVE_DEVICE_ID = 'slaveDeviceId', 25 SLAVE_DEVICE_ID = 'slaveDeviceId',
  26 + DEVICE_PROFILE_ID = 'deviceProfileId',
  27 + SLAVE_DEVICE_PROFILE_ID = 'slaveDeviceProfileId',
26 ATTRIBUTE = 'attribute', 28 ATTRIBUTE = 'attribute',
27 ATTRIBUTE_RENAME = 'attributeRename', 29 ATTRIBUTE_RENAME = 'attributeRename',
28 DEVICE_NAME = 'deviceName', 30 DEVICE_NAME = 'deviceName',
@@ -31,6 +33,18 @@ export enum DataSourceField { @@ -31,6 +33,18 @@ export enum DataSourceField {
31 LATITUDE_ATTRIBUTE = 'latitudeAttribute', 33 LATITUDE_ATTRIBUTE = 'latitudeAttribute',
32 } 34 }
33 35
  36 +const isControlComponent = (frontId: FrontComponent) => {
  37 + const list = [
  38 + FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH,
  39 + FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON,
  40 + FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH,
  41 + ];
  42 + if (list.includes(frontId)) {
  43 + return true;
  44 + }
  45 + return false;
  46 +};
  47 +
34 export const basicSchema: FormSchema[] = [ 48 export const basicSchema: FormSchema[] = [
35 { 49 {
36 field: BasicConfigField.NAME, 50 field: BasicConfigField.NAME,
@@ -52,388 +66,214 @@ export const basicSchema: FormSchema[] = [ @@ -52,388 +66,214 @@ export const basicSchema: FormSchema[] = [
52 }, 66 },
53 ]; 67 ];
54 68
55 -export const dataSourceSchema: FormSchema[] = [  
56 - {  
57 - field: DataSourceField.IS_GATEWAY_DEVICE,  
58 - component: 'Switch',  
59 - label: '是否是网关设备',  
60 - show: false,  
61 - },  
62 - {  
63 - field: DataSourceField.DEVICE_NAME,  
64 - component: 'Input',  
65 - label: '设备名',  
66 - show: false,  
67 - },  
68 - {  
69 - field: DataSourceField.ORIGINATION_ID,  
70 - component: 'ApiTreeSelect',  
71 - label: '组织',  
72 - colProps: { span: 8 },  
73 - rules: [{ required: true, message: '组织为必填项' }],  
74 - componentProps({ formActionType }) {  
75 - const { setFieldsValue } = formActionType;  
76 - return {  
77 - placeholder: '请选择组织',  
78 - api: async () => {  
79 - const data = await getOrganizationList();  
80 - copyTransFun(data as any as any[]);  
81 - return data;  
82 - },  
83 - onChange() {  
84 - setFieldsValue({  
85 - [DataSourceField.DEVICE_ID]: null,  
86 - [DataSourceField.ATTRIBUTE]: null,  
87 - [DataSourceField.SLAVE_DEVICE_ID]: null,  
88 - [DataSourceField.IS_GATEWAY_DEVICE]: false,  
89 - });  
90 - },  
91 - getPopupContainer: () => document.body,  
92 - };  
93 - },  
94 - },  
95 - {  
96 - field: DataSourceField.DEVICE_ID,  
97 - component: 'ApiSelect',  
98 - label: '设备',  
99 - colProps: { span: 8 },  
100 - rules: [{ required: true, message: '设备名称为必填项' }],  
101 - componentProps({ formModel, formActionType }) {  
102 - const { setFieldsValue } = formActionType;  
103 - const organizationId = formModel[DataSourceField.ORIGINATION_ID];  
104 - return {  
105 - api: async () => {  
106 - if (organizationId) {  
107 - try {  
108 - const data = await getAllDeviceByOrg(organizationId);  
109 - if (data)  
110 - return data.map((item) => ({  
111 - label: item.name,  
112 - value: item.id,  
113 - deviceType: item.deviceType,  
114 - }));  
115 - } catch (error) {}  
116 - }  
117 - return [];  
118 - },  
119 - onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) {  
120 - setFieldsValue({  
121 - [DataSourceField.ATTRIBUTE]: null,  
122 - [DataSourceField.IS_GATEWAY_DEVICE]: record?.deviceType === 'GATEWAY',  
123 - [DataSourceField.SLAVE_DEVICE_ID]: null,  
124 - [DataSourceField.DEVICE_NAME]: record?.label,  
125 - });  
126 - },  
127 - placeholder: '请选择设备',  
128 - getPopupContainer: () => document.body,  
129 - };  
130 - },  
131 - },  
132 - {  
133 - field: DataSourceField.SLAVE_DEVICE_ID,  
134 - label: '网关子设备',  
135 - component: 'ApiSelect',  
136 - colProps: { span: 8 },  
137 - rules: [{ required: true, message: '网关子设备为必填项' }],  
138 - ifShow({ model }) {  
139 - return model[DataSourceField.IS_GATEWAY_DEVICE];  
140 - },  
141 - dynamicRules({ model }) {  
142 - return [{ required: model[DataSourceField.IS_GATEWAY_DEVICE], message: '请选择网关子设备' }]; 69 +export const dataSourceSchema = (frontId?: FrontComponent): FormSchema[] => {
  70 + return [
  71 + {
  72 + field: DataSourceField.IS_GATEWAY_DEVICE,
  73 + component: 'Switch',
  74 + label: '是否是网关设备',
  75 + show: false,
143 }, 76 },
144 - componentProps({ formModel, formActionType }) {  
145 - const { setFieldsValue } = formActionType;  
146 - const organizationId = formModel[DataSourceField.ORIGINATION_ID];  
147 - const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];  
148 - const deviceId = formModel[DataSourceField.DEVICE_ID];  
149 - return {  
150 - api: async () => {  
151 - if (organizationId && isGatewayDevice) {  
152 - try {  
153 - const data = await getGatewaySlaveDevice({ organizationId, masterId: deviceId });  
154 - if (data)  
155 - return data.map((item) => ({  
156 - label: item.name,  
157 - value: item.id,  
158 - deviceType: item.deviceType,  
159 - }));  
160 - } catch (error) {}  
161 - }  
162 - return [];  
163 - },  
164 - onChange(_value, record: Record<'value' | 'label' | 'deviceType', string>) {  
165 - setFieldsValue({  
166 - [DataSourceField.ATTRIBUTE]: null,  
167 - [DataSourceField.DEVICE_NAME]: record?.label,  
168 - });  
169 - },  
170 - placeholder: '请选择网关子设备',  
171 - getPopupContainer: () => document.body,  
172 - }; 77 + {
  78 + field: DataSourceField.DEVICE_NAME,
  79 + component: 'Input',
  80 + label: '设备名',
  81 + show: false,
173 }, 82 },
174 - },  
175 - {  
176 - field: DataSourceField.ATTRIBUTE,  
177 - component: 'ApiSelect',  
178 - label: '属性',  
179 - colProps: { span: 8 },  
180 - rules: [{ required: true, message: '属性为必填项' }],  
181 - componentProps({ formModel }) {  
182 - const organizationId = formModel[DataSourceField.ORIGINATION_ID];  
183 - const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];  
184 - const deviceId = formModel[DataSourceField.DEVICE_ID];  
185 - const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];  
186 - return {  
187 - api: async () => {  
188 - if (organizationId && deviceId) {  
189 - try {  
190 - if (isGatewayDevice && slaveDeviceId) {  
191 - return await getDeviceAttribute(slaveDeviceId);  
192 - }  
193 - if (!isGatewayDevice) {  
194 - return await getDeviceAttribute(deviceId);  
195 - }  
196 - } catch (error) {}  
197 - }  
198 - return [];  
199 - },  
200 - placeholder: '请选择属性',  
201 - getPopupContainer: () => document.body,  
202 - }; 83 + {
  84 + field: DataSourceField.ORIGINATION_ID,
  85 + component: 'ApiTreeSelect',
  86 + label: '组织',
  87 + colProps: { span: 8 },
  88 + rules: [{ required: true, message: '组织为必填项' }],
  89 + componentProps({ formActionType }) {
  90 + const { setFieldsValue } = formActionType;
  91 + return {
  92 + placeholder: '请选择组织',
  93 + api: async () => {
  94 + const data = await getOrganizationList();
  95 + copyTransFun(data as any as any[]);
  96 + return data;
  97 + },
  98 + onChange() {
  99 + setFieldsValue({
  100 + [DataSourceField.DEVICE_ID]: null,
  101 + [DataSourceField.ATTRIBUTE]: null,
  102 + [DataSourceField.SLAVE_DEVICE_ID]: null,
  103 + [DataSourceField.IS_GATEWAY_DEVICE]: false,
  104 + [DataSourceField.DEVICE_PROFILE_ID]: null,
  105 + [DataSourceField.SLAVE_DEVICE_PROFILE_ID]: null,
  106 + });
  107 + },
  108 + getPopupContainer: () => document.body,
  109 + };
  110 + },
203 }, 111 },
204 - },  
205 - {  
206 - field: DataSourceField.DEVICE_RENAME,  
207 - component: 'Input',  
208 - label: '设备',  
209 - colProps: { span: 8 },  
210 - componentProps: {  
211 - placeholder: '设备重命名', 112 + {
  113 + field: DataSourceField.DEVICE_PROFILE_ID,
  114 + component: 'Input',
  115 + label: '',
  116 + show: false,
212 }, 117 },
213 - },  
214 - {  
215 - field: DataSourceField.ATTRIBUTE_RENAME,  
216 - component: 'Input',  
217 - label: '属性',  
218 - colProps: { span: 8 },  
219 - componentProps: {  
220 - placeholder: '属性重命名',  
221 - },  
222 - },  
223 -]; 118 + {
  119 + field: DataSourceField.DEVICE_ID,
  120 + component: 'ApiSelect',
  121 + label: '设备',
  122 + colProps: { span: 8 },
  123 + rules: [{ required: true, message: '设备名称为必填项' }],
  124 + componentProps({ formModel, formActionType }) {
  125 + const { setFieldsValue } = formActionType;
  126 + const organizationId = formModel[DataSourceField.ORIGINATION_ID];
  127 + const deviceId = formModel[DataSourceField.DEVICE_ID];
  128 + return {
  129 + api: async () => {
  130 + if (organizationId) {
  131 + try {
  132 + const data = await getAllDeviceByOrg(organizationId);
  133 + if (deviceId) {
  134 + const record = data.find((item) => item.id === deviceId);
  135 + setFieldsValue({ [DataSourceField.DEVICE_PROFILE_ID]: record?.deviceProfileId });
  136 + }
  137 + if (data)
  138 + return data.map((item) => ({
  139 + ...item,
  140 + label: item.name,
  141 + value: item.id,
  142 + deviceType: item.deviceType,
  143 + }));
  144 + } catch (error) {}
  145 + }
  146 + return [];
  147 + },
224 148
225 -export const controlFormSchema: FormSchema[] = [  
226 - {  
227 - field: DataSourceField.DEVICE_RENAME,  
228 - component: 'Input',  
229 - label: '设备',  
230 - colProps: { span: 8 },  
231 - componentProps: {  
232 - placeholder: '设备重命名', 149 + onChange(_value, record: MasterDeviceList) {
  150 + setFieldsValue({
  151 + [DataSourceField.ATTRIBUTE]: null,
  152 + [DataSourceField.IS_GATEWAY_DEVICE]: record?.deviceType === 'GATEWAY',
  153 + [DataSourceField.DEVICE_PROFILE_ID]: record?.deviceProfileId,
  154 + [DataSourceField.SLAVE_DEVICE_ID]: null,
  155 + [DataSourceField.SLAVE_DEVICE_PROFILE_ID]: null,
  156 + [DataSourceField.DEVICE_NAME]: record?.label,
  157 + });
  158 + },
  159 + placeholder: '请选择设备',
  160 + getPopupContainer: () => document.body,
  161 + };
  162 + },
233 }, 163 },
234 - },  
235 - {  
236 - field: DataSourceField.ATTRIBUTE_RENAME,  
237 - component: 'Input',  
238 - label: '属性',  
239 - colProps: { span: 8 },  
240 - componentProps: {  
241 - placeholder: '属性重命名', 164 + {
  165 + field: DataSourceField.SLAVE_DEVICE_PROFILE_ID,
  166 + component: 'Input',
  167 + label: '',
  168 + show: false,
242 }, 169 },
243 - },  
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 - }; 170 + {
  171 + field: DataSourceField.SLAVE_DEVICE_ID,
  172 + label: '网关子设备',
  173 + component: 'ApiSelect',
  174 + colProps: { span: 8 },
  175 + rules: [{ required: true, message: '网关子设备为必填项' }],
  176 + ifShow({ model }) {
  177 + return model[DataSourceField.IS_GATEWAY_DEVICE];
  178 + },
  179 + dynamicRules({ model }) {
  180 + return [
  181 + { required: model[DataSourceField.IS_GATEWAY_DEVICE], message: '请选择网关子设备' },
  182 + ];
  183 + },
  184 + componentProps({ formModel, formActionType }) {
  185 + const { setFieldsValue } = formActionType;
  186 + const organizationId = formModel[DataSourceField.ORIGINATION_ID];
  187 + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
  188 + const deviceId = formModel[DataSourceField.DEVICE_ID];
  189 + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];
  190 + return {
  191 + api: async () => {
  192 + if (organizationId && isGatewayDevice) {
  193 + try {
  194 + const data = await getGatewaySlaveDevice({ organizationId, masterId: deviceId });
  195 + if (slaveDeviceId) {
  196 + const record = data.find((item) => item.id === slaveDeviceId);
  197 + setFieldsValue({ [DataSourceField.DEVICE_PROFILE_ID]: record?.deviceProfileId });
  198 + }
  199 + if (data)
  200 + return data.map((item) => ({
  201 + ...item,
  202 + label: item.name,
  203 + value: item.id,
  204 + deviceType: item.deviceType,
  205 + }));
  206 + } catch (error) {}
  207 + }
  208 + return [];
  209 + },
  210 + onChange(_value, record: MasterDeviceList) {
  211 + setFieldsValue({
  212 + [DataSourceField.ATTRIBUTE]: null,
  213 + [DataSourceField.SLAVE_DEVICE_PROFILE_ID]: record.deviceProfileId,
  214 + [DataSourceField.DEVICE_NAME]: record?.label,
  215 + });
  216 + },
  217 + placeholder: '请选择网关子设备',
  218 + getPopupContainer: () => document.body,
  219 + };
  220 + },
285 }, 221 },
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 - }; 222 + {
  223 + field: DataSourceField.ATTRIBUTE,
  224 + component: 'ApiSelect',
  225 + label: '属性',
  226 + colProps: { span: 8 },
  227 + rules: [{ required: true, message: '属性为必填项' }],
  228 + componentProps({ formModel }) {
  229 + const isGatewayDevice = formModel[DataSourceField.IS_GATEWAY_DEVICE];
  230 + const deviceId = formModel[DataSourceField.DEVICE_ID];
  231 + const deviceProfileId = formModel[DataSourceField.DEVICE_PROFILE_ID];
  232 + const slaveDeviceId = formModel[DataSourceField.SLAVE_DEVICE_ID];
  233 + const slaveDeviceProfileId = formModel[DataSourceField.SLAVE_DEVICE_PROFILE_ID];
  234 +
  235 + return {
  236 + api: async () => {
  237 + if (deviceId) {
  238 + try {
  239 + if (isGatewayDevice && slaveDeviceId && slaveDeviceProfileId) {
  240 + return await getDeviceAttribute({
  241 + deviceProfileId: slaveDeviceProfileId,
  242 + dataType: isControlComponent(frontId!) ? 'BOOL' : undefined,
  243 + });
  244 + }
  245 + if (!isGatewayDevice && deviceProfileId) {
  246 + return await getDeviceAttribute({
  247 + deviceProfileId,
  248 + dataType: isControlComponent(frontId!) ? 'BOOL' : undefined,
  249 + });
  250 + }
  251 + } catch (error) {}
  252 + }
  253 + return [];
  254 + },
  255 + placeholder: '请选择属性',
  256 + getPopupContainer: () => document.body,
  257 + };
  258 + },
367 }, 259 },
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 - }; 260 + {
  261 + field: DataSourceField.DEVICE_RENAME,
  262 + component: 'Input',
  263 + label: '设备',
  264 + colProps: { span: 8 },
  265 + componentProps: {
  266 + placeholder: '设备重命名',
  267 + },
402 }, 268 },
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 - }; 269 + {
  270 + field: DataSourceField.ATTRIBUTE_RENAME,
  271 + component: 'Input',
  272 + label: '属性',
  273 + colProps: { span: 8 },
  274 + componentProps: {
  275 + placeholder: '属性重命名',
  276 + },
437 }, 277 },
438 - },  
439 -]; 278 + ];
  279 +};
1 import moment from 'moment'; 1 import moment from 'moment';
2 import { Moment } from 'moment'; 2 import { Moment } from 'moment';
3 -import { getDeviceAttributes } from '/@/api/dataBoard';  
4 import { FormSchema } from '/@/components/Form'; 3 import { FormSchema } from '/@/components/Form';
5 import { ColEx } from '/@/components/Form/src/types'; 4 import { ColEx } from '/@/components/Form/src/types';
6 import { useGridLayout } from '/@/hooks/component/useGridLayout'; 5 import { useGridLayout } from '/@/hooks/component/useGridLayout';
@@ -134,7 +133,6 @@ export const formSchema: FormSchema[] = [ @@ -134,7 +133,6 @@ export const formSchema: FormSchema[] = [
134 field: SchemaFiled.AGG, 133 field: SchemaFiled.AGG,
135 label: '数据聚合功能', 134 label: '数据聚合功能',
136 component: 'Select', 135 component: 'Select',
137 - // defaultValue: AggregateDataEnum.NONE,  
138 componentProps: { 136 componentProps: {
139 getPopupContainer: () => document.body, 137 getPopupContainer: () => document.body,
140 options: [ 138 options: [
@@ -181,7 +179,6 @@ export const formSchema: FormSchema[] = [ @@ -181,7 +179,6 @@ export const formSchema: FormSchema[] = [
181 field: SchemaFiled.LIMIT, 179 field: SchemaFiled.LIMIT,
182 label: '最大条数', 180 label: '最大条数',
183 component: 'InputNumber', 181 component: 'InputNumber',
184 - // defaultValue: 7,  
185 ifShow({ values }) { 182 ifShow({ values }) {
186 return values[SchemaFiled.AGG] === AggregateDataEnum.NONE; 183 return values[SchemaFiled.AGG] === AggregateDataEnum.NONE;
187 }, 184 },
@@ -201,34 +198,30 @@ export const formSchema: FormSchema[] = [ @@ -201,34 +198,30 @@ export const formSchema: FormSchema[] = [
201 componentProps: { 198 componentProps: {
202 getPopupContainer: () => document.body, 199 getPopupContainer: () => document.body,
203 }, 200 },
204 - // componentProps({ formModel }) {  
205 - // const deviceId = formModel[SchemaFiled.DEVICE_ID];  
206 - // return {  
207 - // api: async () => {  
208 - // if (deviceId) {  
209 - // try {  
210 - // try {  
211 - // const data = await getDeviceAttributes({ deviceId });  
212 - // if (data) return data.map((item) => ({ label: item, value: item }));  
213 - // } catch (error) {}  
214 - // return [];  
215 - // } catch (error) {}  
216 - // }  
217 - // return [];  
218 - // },  
219 - // getPopupContainer: () => document.body,  
220 - // };  
221 - // },  
222 }, 201 },
223 ]; 202 ];
224 203
225 -// export const selectDeviceAttrSchema: FormSchema[] = [  
226 -// {  
227 -// field: 'keys',  
228 -// label: '设备属性',  
229 -// component: 'Select',  
230 -// componentProps: {  
231 -// getPopupContainer: () => document.body,  
232 -// },  
233 -// },  
234 -// ]; 204 +export function getHistorySearchParams(value: Recordable) {
  205 + const { startTs, endTs, interval, agg, limit, way, keys, deviceId } = value;
  206 + if (way === QueryWay.LATEST) {
  207 + return {
  208 + keys,
  209 + entityId: deviceId,
  210 + startTs: moment().subtract(startTs, 'ms').valueOf(),
  211 + endTs: Date.now(),
  212 + interval,
  213 + agg,
  214 + limit,
  215 + };
  216 + } else {
  217 + return {
  218 + keys,
  219 + entityId: deviceId,
  220 + startTs: moment(startTs).valueOf(),
  221 + endTs: moment(endTs).valueOf(),
  222 + interval,
  223 + agg,
  224 + limit,
  225 + };
  226 + }
  227 +}
@@ -178,6 +178,39 @@ export const modeFour: FormSchema[] = [ @@ -178,6 +178,39 @@ export const modeFour: FormSchema[] = [
178 }, 178 },
179 ]; 179 ];
180 180
  181 +export const modeFive: FormSchema[] = [
  182 + {
  183 + field: visualOptionField.FONT_COLOR,
  184 + label: '数值字体颜色',
  185 + component: 'ColorPicker',
  186 + changeEvent: 'update:value',
  187 + componentProps: {
  188 + defaultValue: '#000',
  189 + },
  190 + },
  191 + {
  192 + field: visualOptionField.ICON_COLOR,
  193 + label: '图标颜色',
  194 + component: 'ColorPicker',
  195 + changeEvent: 'update:value',
  196 + componentProps: {
  197 + defaultValue: '#367BFF',
  198 + },
  199 + },
  200 + {
  201 + field: visualOptionField.ICON,
  202 + label: '图标',
  203 + component: 'IconDrawer',
  204 + changeEvent: 'update:value',
  205 + componentProps({ formModel }) {
  206 + const color = formModel[visualOptionField.ICON_COLOR];
  207 + return {
  208 + color,
  209 + };
  210 + },
  211 + },
  212 +];
  213 +
181 export const schemasMap = new Map<FrontComponent, FormSchema[]>(); 214 export const schemasMap = new Map<FrontComponent, FormSchema[]>();
182 215
183 schemasMap.set(FrontComponent.TEXT_COMPONENT_1, modeOne); 216 schemasMap.set(FrontComponent.TEXT_COMPONENT_1, modeOne);
@@ -188,3 +221,6 @@ schemasMap.set(FrontComponent.TEXT_COMPONENT_5, modeTwo); @@ -188,3 +221,6 @@ schemasMap.set(FrontComponent.TEXT_COMPONENT_5, modeTwo);
188 schemasMap.set(FrontComponent.INSTRUMENT_COMPONENT_1, modeOne); 221 schemasMap.set(FrontComponent.INSTRUMENT_COMPONENT_1, modeOne);
189 schemasMap.set(FrontComponent.INSTRUMENT_COMPONENT_2, modeThree); 222 schemasMap.set(FrontComponent.INSTRUMENT_COMPONENT_2, modeThree);
190 schemasMap.set(FrontComponent.DIGITAL_DASHBOARD_COMPONENT, modeFour); 223 schemasMap.set(FrontComponent.DIGITAL_DASHBOARD_COMPONENT, modeFour);
  224 +schemasMap.set(FrontComponent.CONTROL_COMPONENT_SWITCH_WITH_ICON, modeFive);
  225 +schemasMap.set(FrontComponent.CONTROL_COMPONENT_SLIDING_SWITCH, modeOne);
  226 +schemasMap.set(FrontComponent.CONTROL_COMPONENT_TOGGLE_SWITCH, modeOne);
@@ -119,7 +119,7 @@ @@ -119,7 +119,7 @@
119 data.width = newWPx; 119 data.width = newWPx;
120 data.height = newHPx; 120 data.height = newHPx;
121 121
122 - data.record.dataSource = data?.record.dataSource.map((item) => { 122 + data.record.dataSource = data.record.dataSource.map((item) => {
123 if (!item.uuid) item.uuid = buildUUID(); 123 if (!item.uuid) item.uuid = buildUUID();
124 return { 124 return {
125 ...item, 125 ...item,
@@ -265,9 +265,9 @@ @@ -265,9 +265,9 @@
265 record: item, 265 record: item,
266 }; 266 };
267 }); 267 });
268 -  
269 beginSendMessage(); 268 beginSendMessage();
270 } catch (error) { 269 } catch (error) {
  270 + throw error;
271 } finally { 271 } finally {
272 loading.value = false; 272 loading.value = false;
273 } 273 }
@@ -310,7 +310,7 @@ @@ -310,7 +310,7 @@
310 310
311 const getComponentConfig = ( 311 const getComponentConfig = (
312 record: DataBoardLayoutInfo['record'], 312 record: DataBoardLayoutInfo['record'],
313 - dataSourceRecord: DataSource 313 + dataSourceRecord: DataSource | DataSource[]
314 ) => { 314 ) => {
315 const frontComponent = record.frontId; 315 const frontComponent = record.frontId;
316 const component = frontComponentMap.get(frontComponent as FrontComponent); 316 const component = frontComponentMap.get(frontComponent as FrontComponent);
@@ -375,9 +375,13 @@ @@ -375,9 +375,13 @@
375 historyDataModalMethod.openModal(true, record); 375 historyDataModalMethod.openModal(true, record);
376 }; 376 };
377 377
  378 + const hasHistoryTrend = (item: DataBoardLayoutInfo) => {
  379 + return frontComponentMap.get(item.record.frontId as FrontComponent)?.hasHistoryTrend;
  380 + };
  381 +
378 onMounted(async () => { 382 onMounted(async () => {
379 - await injectBaiDuMapLib();  
380 - await injectBaiDuMapTrackAniMationLib(); 383 + injectBaiDuMapLib();
  384 + injectBaiDuMapTrackAniMationLib();
381 getDataBoardComponent(); 385 getDataBoardComponent();
382 }); 386 });
383 </script> 387 </script>
@@ -438,10 +442,12 @@ @@ -438,10 +442,12 @@
438 @resize="itemResize" 442 @resize="itemResize"
439 @moved="itemMoved" 443 @moved="itemMoved"
440 @container-resized="itemContainerResized" 444 @container-resized="itemContainerResized"
  445 + drag-ignore-from=".no-drag"
441 > 446 >
442 <WidgetWrapper 447 <WidgetWrapper
443 :key="item.i" 448 :key="item.i"
444 :ref="(el: Element) => setComponentRef(el, item)" 449 :ref="(el: Element) => setComponentRef(el, item)"
  450 + :record="item.record"
445 :data-source="item.record.dataSource" 451 :data-source="item.record.dataSource"
446 > 452 >
447 <template #header> 453 <template #header>
@@ -455,7 +461,7 @@ @@ -455,7 +461,7 @@
455 <Tooltip title="趋势"> 461 <Tooltip title="趋势">
456 <img 462 <img
457 :src="trendIcon" 463 :src="trendIcon"
458 - v-if="!getIsSharePage" 464 + v-if="!getIsSharePage && hasHistoryTrend(item)"
459 class="cursor-pointer w-4.5 h-4.5" 465 class="cursor-pointer w-4.5 h-4.5"
460 @click="handleOpenHistroyDataModal(item.record.dataSource)" 466 @click="handleOpenHistroyDataModal(item.record.dataSource)"
461 /> 467 />
@@ -18,10 +18,13 @@ export interface ComponentConfig { @@ -18,10 +18,13 @@ export interface ComponentConfig {
18 ComponentKey: FrontComponent; 18 ComponentKey: FrontComponent;
19 ComponentConfig?: Recordable; 19 ComponentConfig?: Recordable;
20 ComponentCategory: FrontComponentCategory; 20 ComponentCategory: FrontComponentCategory;
  21 + isMultipleDataSource?: boolean;
  22 + hasHistoryTrend?: boolean;
  23 + hasSetting?: boolean;
21 transformConfig: ( 24 transformConfig: (
22 componentConfig: Recordable, 25 componentConfig: Recordable,
23 record: DataComponentRecord, 26 record: DataComponentRecord,
24 - dataSourceRecord: DataSource 27 + dataSourceRecord: DataSource | DataSource[]
25 ) => Recordable; 28 ) => Recordable;
26 } 29 }
27 30