ソースを参照

feat(views): 优化实时监测页面

1. 可视化编辑器支持显示设备树
2. 记录组态画面中的设备 id
3. 支持点击组态中的设备打开对话框
wangcong 3 週間 前
コミット
1804bb86a9

+ 7 - 0
src/api/index.ts

@@ -1023,3 +1023,10 @@ export const getInfoListByOrgId = async (orgId: number) => {
   const data = await request<InfoListByOrg[]>(apiBiz(`/orgDeviceLimit/infoListByOrgId/${orgId}`));
   return data;
 };
+
+export const updateGroupModuleInfo = async (params: Partial<GroupModuleInfo>) => {
+  await request(apiBiz('/moduleInfo/update'), {
+    method: 'POST',
+    body: JSON.stringify(params),
+  });
+};

+ 17 - 3
src/components/visual-2d/Visual2DEditor.vue

@@ -2,20 +2,23 @@
 import { computed, onMounted, onUnmounted, ref, useTemplateRef, watch } from 'vue';
 import qs from 'qs';
 
+import { useRequest } from '@/hooks/request';
 import { useViewVisible } from '@/hooks/view-visible';
+import { updateGroupModuleInfo } from '@/api';
 
 import { getVisual2DMsgType, visual2DEditorPageUrl, Visual2DMsgType } from '.';
 
-import type { AllDevicesList, GroupModuleInfo, IframeMsg } from '@/types';
+import type { DeviceGroupTree, GroupModuleInfo, IframeMsg } from '@/types';
 
 interface Props {
   info: GroupModuleInfo;
-  deviceList?: AllDevicesList[];
+  deviceList?: DeviceGroupTree[];
 }
 
 const props = defineProps<Props>();
 
 const { visible, showView, hideView } = useViewVisible();
+const { handleRequest } = useRequest();
 const iframeRef = useTemplateRef('editorIframe');
 const isIframeLoaded = ref(false);
 
@@ -48,6 +51,8 @@ const handleIframeMsg = (e: MessageEvent<IframeMsg>) => {
   if (msgType === getVisual2DMsgType(Visual2DMsgType.EditLoaded)) {
     isIframeLoaded.value = true;
     sendDeviceData();
+  } else if (msgType === getVisual2DMsgType(Visual2DMsgType.SendDeviceIds)) {
+    updateGroupModule(e.data.deviceIds);
   } else if (msgType === getVisual2DMsgType(Visual2DMsgType.CloseEditor)) {
     hideView();
   }
@@ -57,13 +62,22 @@ const sendDeviceData = () => {
   if (props.deviceList) {
     const msg: IframeMsg = {
       msgType: getVisual2DMsgType(Visual2DMsgType.SendDeviceData),
-      ...JSON.parse(JSON.stringify(props.deviceList)),
+      deviceList: JSON.parse(JSON.stringify(props.deviceList)),
     };
 
     iframeRef.value?.contentWindow?.postMessage(msg, '*');
   }
 };
 
+const updateGroupModule = (deviceIds: string) => {
+  handleRequest(async () => {
+    await updateGroupModuleInfo({
+      id: props.info.id,
+      deviceIds,
+    });
+  });
+};
+
 defineExpose({
   showView,
   hideView,

+ 14 - 3
src/components/visual-2d/Visual2DPreview.vue

@@ -4,17 +4,18 @@ import qs from 'qs';
 
 import { getVisual2DMsgType, Visual2DMsgType, visual2DPreviewPageUrl } from '.';
 
-import type { AllDevicesList, GroupModuleInfo, IframeMsg } from '@/types';
+import type { DeviceGroupTree, GroupModuleInfo, IframeMsg } from '@/types';
 
 interface Props {
   info: GroupModuleInfo;
-  deviceList?: AllDevicesList[];
+  deviceList?: DeviceGroupTree[];
 }
 
 const props = defineProps<Props>();
 
 const emit = defineEmits<{
   click: [];
+  openDevCtrlModal: [id: number];
 }>();
 
 const iframeRef = useTemplateRef('previewIframe');
@@ -55,6 +56,8 @@ const handleIframeMsg = (e: MessageEvent<IframeMsg>) => {
     sendDeviceData();
   } else if (msgType === getVisual2DMsgType(Visual2DMsgType.PreviewClicked)) {
     emit('click');
+  } else if (msgType === getVisual2DMsgType(Visual2DMsgType.OpenDevCtrlModal)) {
+    emit('openDevCtrlModal', e.data.hvacDeviceId);
   }
 };
 
@@ -62,12 +65,20 @@ const sendDeviceData = () => {
   if (props.deviceList) {
     const msg: IframeMsg = {
       msgType: getVisual2DMsgType(Visual2DMsgType.SendDeviceData),
-      ...JSON.parse(JSON.stringify(props.deviceList)),
+      deviceList: JSON.parse(JSON.stringify(props.deviceList)),
     };
 
     iframeRef.value?.contentWindow?.postMessage(msg, '*');
   }
 };
+
+const exportImg = () => {
+  console.log('export img');
+};
+
+defineExpose({
+  exportImg,
+});
 </script>
 
 <template>

+ 2 - 0
src/components/visual-2d/index.ts

@@ -14,6 +14,8 @@ export const enum Visual2DMsgType {
   EditLoaded = 'edit-loaded',
   CloseEditor = 'close-editor',
   SendDeviceData = 'send-device-data',
+  SendDeviceIds = 'send-device-ids',
+  OpenDevCtrlModal = 'open-dev-ctrl-modal',
   PreviewLoaded = 'preview-loaded',
   PreviewClicked = 'preview-clicked',
 }

+ 10 - 0
src/types/index.ts

@@ -461,6 +461,16 @@ export interface DeviceGroupChild {
   userId: number;
 }
 
+export interface DeviceGroupTree extends DeviceGroupItem {
+  label: string;
+  children: DeviceGroupTreeChild[];
+}
+
+export interface DeviceGroupTreeChild extends DeviceGroupChild {
+  label: string;
+  children: (AllDevicesList & { label: string })[];
+}
+
 export interface ListInfo {
   dataType: number;
 }

+ 6 - 4
src/views/device-group/DeviceGroup.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { computed, onMounted, ref, watch } from 'vue';
+import { computed, onMounted, ref, useTemplateRef, watch } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 
 import DeviceWorkStatus from '@/views/device-work-status/DeviceWorkStatus.vue';
@@ -13,6 +13,7 @@ import type { TabComponent } from '@/types';
 const router = useRouter();
 const route = useRoute();
 const { renderView, refreshView } = useRefreshView();
+const realTimeMonitorRefs = useTemplateRef<InstanceType<typeof RealTimeMonitor>[]>('realTimeMonitor');
 const activeKey = ref('');
 
 const deviceGroupId = computed(() => {
@@ -62,21 +63,22 @@ const handleTabClick = (activeKey: string | number) => {
       <ATabPane v-for="item in aiSmartCtrlTabs" :key="item.key" :tab="item.name">
         <component
           v-if="activeKey === item.key && renderView"
+          :ref="item.key"
           :is="item.component"
           :device-group-id="Number(deviceGroupId)"
         />
       </ATabPane>
       <template #rightExtra>
         <div class="real-time-monitor-operate" v-show="activeKey === 'realTimeMonitor'">
-          <AButton class="icon-button">
+          <AButton class="icon-button" @click="realTimeMonitorRefs?.[0].openEditor">
             <SvgIcon name="edit-o" />
             {{ $t('common.editor') }}
           </AButton>
-          <AButton class="icon-button">
+          <AButton class="icon-button" @click="realTimeMonitorRefs?.[0].exportImg">
             <SvgIcon name="export" />
             {{ $t('common.exportImg') }}
           </AButton>
-          <AButton class="icon-button">
+          <AButton class="icon-button" @click="realTimeMonitorRefs?.[0].maximizeView">
             <SvgIcon name="maximize" />
             {{ $t('common.maximize') }}
           </AButton>

+ 5 - 1
src/views/real-time-monitor/DeviceCtrlModal.vue

@@ -14,6 +14,10 @@ interface Props {
 
 defineProps<Props>();
 
+defineEmits<{
+  openDevBatchExe: [];
+}>();
+
 const { visible, showView, hideView } = useViewVisible();
 
 const disableStartStopCtrl = computed(() => {
@@ -89,7 +93,7 @@ defineExpose({
     <div class="dev-ctrl-modal-separator"></div>
     <div class="dev-ctrl-modal-item">
       <span></span>
-      <span class="dev-ctrl-modal-operate">
+      <span class="dev-ctrl-modal-operate" @click="$emit('openDevBatchExe')">
         {{ $t('common.batchExecution') }}
         <SvgIcon name="right" />
       </span>

+ 124 - 5
src/views/real-time-monitor/RealTimeMonitor.vue

@@ -1,22 +1,141 @@
 <script setup lang="ts">
-import { useTemplateRef } from 'vue';
+import { onMounted, ref, useTemplateRef } from 'vue';
+
+import Visual2DEditor from '@/components/visual-2d/Visual2DEditor.vue';
+import Visual2DPreview from '@/components/visual-2d/Visual2DPreview.vue';
+import { useRequest } from '@/hooks/request';
+import { getGroupModuleInfo, getPageList, noPaginationDevicesList } from '@/api';
+import { VisualModuleType } from '@/constants';
 
 import DeviceControl from './device-control/DeviceControl.vue';
 import DeviceBatchExe from './DeviceBatchExe.vue';
 import DeviceCtrlModal from './DeviceCtrlModal.vue';
 
+import type {
+  AllDevicesList,
+  DevGroupTabCompProps,
+  DeviceGroupItem,
+  DeviceGroupTree,
+  DeviceGroupTreeChild,
+  GroupModuleInfo,
+} from '@/types';
+
+const props = defineProps<DevGroupTabCompProps>();
+
 const deviceControlRef = useTemplateRef('deviceControl');
 const deviceCtrlModalRef = useTemplateRef('deviceCtrlModal');
 const deviceBatchExeRef = useTemplateRef('deviceBatchExe');
+const visual2DEditorRef = useTemplateRef('visual2DEditor');
+const visual2DPreviewRef = useTemplateRef('visual2DPreview');
+
+const { isLoading, handleRequest } = useRequest();
+const moduleInfo = ref<GroupModuleInfo>();
+const deviceList = ref<DeviceGroupTree[]>([]);
+
+onMounted(() => {
+  handleRequest(async () => {
+    const groups = await getPageList();
+    const devices = await noPaginationDevicesList();
+    deviceList.value = transformGroupsAndDevices(groups, devices);
+
+    moduleInfo.value = await getGroupModuleInfo({
+      groupId: props.deviceGroupId,
+      moduleType: VisualModuleType.Module2D,
+    });
+  });
+});
+
+const transformGroupsAndDevices = (groups: DeviceGroupItem[], devices: AllDevicesList[]): DeviceGroupTree[] => {
+  // 创建一个映射表,用于快速查找每个组ID对应的设备
+  const deviceMap = new Map<number, AllDevicesList[]>();
+
+  devices.forEach((device) => {
+    if (!deviceMap.has(device.groupId)) {
+      deviceMap.set(device.groupId, []);
+    }
+
+    deviceMap.get(device.groupId)!.push(device);
+  });
+
+  // 转换顶级分组
+  return groups.map((group) => {
+    // 转换当前组
+    const transformed: DeviceGroupTree = {
+      ...group,
+      label: group.groupName,
+      children: [],
+    };
+
+    // 转换子分组(二级分组),并过滤掉没有设备的分组
+    if (group.deviceGroupChilds && group.deviceGroupChilds.length > 0) {
+      transformed.children = group.deviceGroupChilds
+        .map((child) => {
+          const transformedChild: DeviceGroupTreeChild = {
+            ...child,
+            label: child.groupName,
+            children: [],
+          };
+
+          // 查找并添加属于这个二级分组的设备
+          const childDevices = deviceMap.get(child.id) || [];
+
+          transformedChild.children = childDevices.map((device) => ({
+            ...device,
+            label: device.deviceName,
+          }));
+
+          return transformedChild;
+        })
+        .filter((child) => child.children.length > 0); // 过滤掉没有设备的二级分组
+    }
+
+    return transformed;
+  });
+};
+
+const openEditor = () => {
+  visual2DEditorRef.value?.showView();
+};
+
+const openDevCtrlModal = (id: number) => {
+  if (!id) {
+    return;
+  }
+
+  deviceCtrlModalRef.value?.showView();
+};
+
+const exportImg = () => {
+  visual2DPreviewRef.value?.exportImg();
+};
+
+const maximizeView = () => {
+  console.log('maximize view');
+};
+
+defineExpose({
+  openEditor,
+  exportImg,
+  maximizeView,
+});
 </script>
 
 <template>
-  <div class="real-time-monitor-container" @click="deviceControlRef?.closeCtrlPanel">
-    <AButton @click="deviceCtrlModalRef?.showView">Dev Modal</AButton>
-    <AButton @click="deviceBatchExeRef?.showView">Dev Batch</AButton>
+  <div class="real-time-monitor-container">
+    <template v-if="moduleInfo?.leId">
+      <Visual2DPreview
+        ref="visual2DPreview"
+        :info="moduleInfo"
+        :device-list="deviceList"
+        @click="deviceControlRef?.closeCtrlPanel"
+        @open-dev-ctrl-modal="openDevCtrlModal"
+      />
+      <Visual2DEditor ref="visual2DEditor" :info="moduleInfo" :device-list="deviceList" />
+    </template>
     <DeviceControl ref="deviceControl" />
-    <DeviceCtrlModal ref="deviceCtrlModal" />
+    <DeviceCtrlModal ref="deviceCtrlModal" @open-dev-batch-exe="deviceBatchExeRef?.showView" />
     <DeviceBatchExe ref="deviceBatchExe" />
+    <ASpin v-if="isLoading" class="center-loading" :spinning="true" />
   </div>
 </template>