소스 검색

Merge branch 'master' of http://182.92.126.35:3000/hrx/mky-vent-base

hongrunxia 5 달 전
부모
커밋
ff08b46232
22개의 변경된 파일1099개의 추가작업 그리고 377개의 파일을 삭제
  1. 1 0
      package.json
  2. BIN
      src/assets/images/home-container/configurable/board_bg_10.png
  3. BIN
      src/assets/images/home-container/configurable/board_bg_7.png
  4. BIN
      src/assets/images/home-container/configurable/board_bg_8.png
  5. BIN
      src/assets/images/home-container/configurable/board_bg_9.png
  6. BIN
      src/assets/images/home-container/configurable/minehome/list-bg-n5.png
  7. BIN
      src/assets/images/home-container/configurable/minehome/list-bg-n6.png
  8. BIN
      src/assets/images/home-container/configurable/minehome/list-icon-file.png
  9. 338 177
      src/components/AIChat/MiniChat.vue
  10. 37 0
      src/views/vent/home/configurable/components/detail/MiniBoard.vue
  11. 16 39
      src/views/vent/monitorManager/airDoor/components/door-content-r.vue
  12. 1 1
      src/views/vent/monitorManager/airDoor/components/door-menu-l.vue
  13. 6 8
      src/views/vent/monitorManager/airDoor/index.vue
  14. 15 11
      src/views/vent/monitorManager/balancePressMonitor/components/balancePressHandleHistory.vue
  15. 1 1
      src/views/vent/monitorManager/comment/HandlerHistoryTable.vue
  16. 16 28
      src/views/vent/monitorManager/footageMonitor/components/moduleCommon.vue
  17. 12 5
      src/views/vent/monitorManager/footageMonitor/components/moduleHead.vue
  18. 4 26
      src/views/vent/monitorManager/footageMonitor/index.vue
  19. 136 78
      src/views/vent/monitorManager/sprayMonitor/component.vue
  20. 308 3
      src/views/vent/monitorManager/sprayMonitor/index.vue
  21. 113 0
      src/views/vent/monitorManager/sprayMonitor/spray.three.ts
  22. 95 0
      src/views/vent/monitorManager/sprayMonitor/spray.threejs.base.ts

+ 1 - 0
package.json

@@ -77,6 +77,7 @@
     "three.path": "^1.0.1",
     "tinymce": "^5.10.3",
     "vditor": "^3.9.5",
+    "video.js": "^8.23.4",
     "vue": "^3.2.0",
     "vue-cropper": "^0.6.2",
     "vue-cropperjs": "^5.0.0",

BIN
src/assets/images/home-container/configurable/board_bg_10.png


BIN
src/assets/images/home-container/configurable/board_bg_7.png


BIN
src/assets/images/home-container/configurable/board_bg_8.png


BIN
src/assets/images/home-container/configurable/board_bg_9.png


BIN
src/assets/images/home-container/configurable/minehome/list-bg-n5.png


BIN
src/assets/images/home-container/configurable/minehome/list-bg-n6.png


BIN
src/assets/images/home-container/configurable/minehome/list-icon-file.png


+ 338 - 177
src/components/AIChat/MiniChat.vue

@@ -8,6 +8,23 @@
   <div v-if="isShowChatBroad" class="mini-chat">
     <!-- 左侧折叠区域 -->
     <div class="left-side" :class="{ collapsed: isFold }" id="leftSide">
+      <div
+        class="addBtn"
+        :style="{
+          backgroundImage: `url(${isFold ? '/src/assets/images/vent/home/add.svg' : ''})`,
+          backgroundColor: isFold ? '' : '#2cb6ff',
+          width: isFold ? '20px' : 'auto',
+        }"
+        @click="addNew"
+      >
+        <span
+          class="btn-text-bg"
+          :style="{
+            backgroundImage: `url(${!isFold ? '/src/assets/images/vent/home/addB.svg' : ''})`,
+          }"
+        ></span>
+        <span v-if="!isFold" class="btn-text">添加新对话</span>
+      </div>
       <div
         v-if="isFold"
         class="historyBtn"
@@ -81,23 +98,35 @@
             </div>
           </template>
           <template v-else>
-            <SvgIcon size="80" class="ml-2px mr-2px" name="ai-logo" />
+            <SvgIcon size="40" class="answerIcon" name="ai-logo" />
             <div class="message-wrapper ai-message-wrapper">
               <div class="answer-message">
-                <div v-if="message.contentR1" class="color-gray font-size-12px" v-html="message.contentR1"> </div>
-                <div v-else v-html="message.content"> </div>
+                <div v-if="message.contentR1" class="thinking-section">
+                  <div class="thinking-header" @click="isShow(message)">
+                    <span class="thinking-title"
+                      >思考过程:<RightOutlined v-if="!message.isShowThink" /> <DownOutlined v-if="message.isShowThink"
+                    /></span>
+                  </div>
+                  <div v-show="message.isShowThink" class="color-gray font-size-12px" v-html="formatMessage(message.contentR1)"></div>
+                </div>
+                <div v-if="message.content" v-html="formatMessage(message.content)"> </div>
               </div>
-              <CopyOutlined class="copy-icon" @click="copyToClipboard(message.contentR1 || message.content)" title="复制消息" />
+              <CopyOutlined class="copy-icon" @click="copyToClipboard(message.content)" title="复制消息" />
             </div>
           </template>
         </div>
+        <!-- 建议信息 -->
+        <div v-for="(item, index) in suggestList" :key="index" class="suggestion-item" @click="handleSuggestClick(item)">
+          <span class="suggestion-text">{{ item }}</span>
+          <a-icon type="right" class="suggestion-arrow" />
+        </div>
       </div>
       <!-- 底部输入区 -->
       <div class="input-area">
-        <a-input v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" />
+        <a-textarea v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" auto-size />
         <div class="ctrl-btn">
           <div class="input-controls">
-            <button class="control-btn">深度学习</button>
+            <button class="control-btn" :class="{ active: isThinking }" @click="toggleThinking">深度学习</button>
             <button class="control-btn" @click="stopReq()">停止响应</button>
           </div>
           <div class="action-bar">
@@ -117,15 +146,16 @@
         </div>
         <!-- 右侧文件上传区 -->
         <div v-if="open" class="file-upload">
-          <!-- 输入框区域,包含确认按钮 -->
-          <div class="input-container">
-            <a-input v-model:value="filePath" placeholder="输入文件连接" class="file-input" @pressEnter="handlePathConfirm" />
-            <button class="confirm-btn" @click="handlePathConfirm">确认</button>
-          </div>
           <!-- 上传按钮 -->
-          <!-- <a-upload> <button class="upload-btn" @click="customUpload">从本地上传</button></a-upload> -->
-          <a-upload class="custom-upload" name="file" :multiple="false">
-            <a-button class="upload-btn" @click="customUpload">
+          <a-upload
+            class="custom-upload"
+            name="file"
+            :multiple="false"
+            :before-upload="handleBeforeUpload"
+            :file-list="fileList"
+            :remove="handleRemove"
+          >
+            <a-button class="upload-btn">
               <UploadOutlined></UploadOutlined>
               从本地上传
             </a-button>
@@ -142,7 +172,7 @@ import { SvgIcon } from '../Icon';
 import { Space, Button, Modal, Input, message } from 'ant-design-vue';
 // import AIChat from './index.vue';
 import { useUserStore } from '/@/store/modules/user';
-import { EditOutlined, DeleteOutlined, UploadOutlined, CopyOutlined } from '@ant-design/icons-vue';
+import { EditOutlined, DeleteOutlined, UploadOutlined, CopyOutlined, RightOutlined, DownOutlined } from '@ant-design/icons-vue';
 import { createVNode } from 'vue';
 const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
 const inputText = ref(''); // 输入框内容
@@ -152,7 +182,11 @@ const editingId = ref<number | null>(null);
 const editText = ref('');
 const currentSessionID = ref('');
 const taskID = ref('');
+const messageID = ref('');
 const open = ref<boolean>(false);
+const isThinking = ref(false); //深度思考是否开启
+const Thinking = ref(false);
+const APIKEY = ref('Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd');
 interface ListItem {
   id: number;
   name?: string;
@@ -164,6 +198,7 @@ interface Message {
   /** 深度思考时的文本 */
   contentR1: string;
   timestamp: number; // 排序依据
+  isShowThink: boolean; //深度思考展示
 }
 // 定义消息历史数组类型
 const messageHistory = ref<Message[]>([]);
@@ -172,89 +207,29 @@ const userid = useUserStore().getUserInfo.id as string;
 const filePath = ref(''); // 绑定输入框值
 const showConfirmBtn = ref(false); // 控制确认按钮显示状态
 const fileList = ref([]);
+const suggestList = ref([]); //建议列表
+//启用深度思考
+const toggleThinking = () => {
+  isThinking.value = !isThinking.value;
+  if (isThinking.value) {
+    APIKEY.value = 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN';
+  } else {
+    APIKEY.value = 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd';
+  }
+};
+// 折叠思考过程
+const isShow = (message) => {
+  message.isShowThink = !message.isShowThink;
+};
+// 点击建议项时的处理函数
+const handleSuggestClick = (text) => {
+  // 将选中的建议填充到文本框
+  inputText.value = text;
+};
 function showAIChat() {
   isShowChatBroad.value = !isShowChatBroad.value;
 }
-//获取消息列表
-// async function handleSend(data) {
-//   messageHistory.value.push({
-//     id: `user_${Date.now()}`,
-//     type: 'user',
-//     content: data,
-//     contentR1: '',
-//     timestamp: Date.now(),
-//   });
-//   // 发送 POST 请求
-//   fetch('http://39.97.59.228:8000/v1/chat-messages', {
-//     method: 'POST',
-//     headers: {
-//       'Content-Type': 'application/json',
-//       Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
-//     },
-//     body: JSON.stringify({
-//       conversation_id: currentSessionID.value,
-//       query: data,
-//       response_mode: 'streaming',
-//       user: userid,
-//       inputs: {},
-//     }),
-//   }).then((response) => {
-//     const decoder = new TextDecoder('utf-8');
-//     let buffer = [];
-//     // 获取可读流
-//     const reader = response.body.getReader();
-//     const newMessage = {
-//       id: `response_${Date.now()}`,
-//       type: 'response' as any,
-//       content: '',
-//       contentR1: '',
-//       timestamp: Date.now(),
-//     };
-//     messageHistory.value.push(newMessage);
-//     // 读取数据
-//     function read() {
-//       return reader.read().then(({ done, value }) => {
-//         if (done) {
-//           return buffer;
-//         }
-//         // 解码数据块
-//         const chunk = decoder.decode(value, { stream: false });
-//         // 处理每段数据
-//         const processedData = processStreamChunk(chunk);
-//         buffer = buffer.concat(processedData);
-//         // 继续读取
-//         return read();
-//       });
-//     }
-//     // 开始读取
-//     return read();
-//     function processStreamChunk(chunk) {
-//       try {
-//         // 移除 "data: " 前缀
-//         const jsonStr = chunk.replace('data: ', '');
-//         const data = JSON.parse(jsonStr);
-//         const targetMessage = messageHistory.value.find((msg) => msg.id === newMessage.id);
-//         if (!targetMessage) return;
-//         // 根据事件类型分发处理
-//         switch (data.event) {
-//           case 'message':
-//             if (!taskID.value && !currentSessionID.value) {
-//               taskID.value = data.task_id;
-//               currentSessionID.value = data.conversation_id;
-//             }
-//             targetMessage.content += data.answer; // 追加内容
-//             break;
-//         }
-//         return data;
-//       } catch (error) {
-//         // 请求失败时设置系统消息
-//         return null;
-//       }
-//     }
-//   });
-//   inputText.value = '';
-// }
-// 复制消息
+//复制消息
 function copyToClipboard(text) {
   if (!text || text.trim() === '') {
     message.warn('没有可复制的内容');
@@ -285,24 +260,37 @@ function copyToClipboard(text) {
     document.body.removeChild(textarea);
   }
 }
+//获取消息列表
 async function handleSend(data) {
   inputText.value = '';
-  messageHistory.value.push({
-    id: `user_${Date.now()}`,
-    type: 'user',
-    content: data,
-    contentR1: '',
-    timestamp: Date.now(),
-  });
+  if (isThinking) {
+    messageHistory.value.push({
+      id: `user_${Date.now()}`,
+      type: 'user',
+      content: data,
+      contentR1: '',
+      timestamp: Date.now(),
+      isShowThink: true,
+    });
+  } else {
+    messageHistory.value.push({
+      id: `user_${Date.now()}`,
+      type: 'user',
+      content: data,
+      contentR1: '',
+      timestamp: Date.now(),
+      isShowThink: false,
+    });
+  }
   try {
     const response = await fetch('http://39.97.59.228:8000/v1/chat-messages', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
-        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+        Authorization: APIKEY.value,
       },
       body: JSON.stringify({
-        conversation_id: currentSessionID.value,
+        conversation_id: currentSessionID.value ? currentSessionID.value : '',
         query: data,
         response_mode: 'streaming',
         user: userid,
@@ -317,14 +305,31 @@ async function handleSend(data) {
     const decoder = new TextDecoder('utf-8');
     const reader = response.body.getReader();
     let textBuffer = ''; // 使用字符串缓冲区来累积数据
-    const newMessage = {
-      id: `response_${Date.now()}`,
-      type: 'response',
-      content: '',
-      contentR1: '',
-      timestamp: Date.now(),
-    };
-    messageHistory.value.push(newMessage);
+    // 在组件中定义
+    const currentProcessingMessage = ref(null);
+    if (isThinking) {
+      const newMessage = {
+        id: `response_${Date.now()}`,
+        type: 'response',
+        content: '',
+        contentR1: '',
+        timestamp: Date.now(),
+        isShowThink: true,
+      };
+      messageHistory.value.push(newMessage);
+      currentProcessingMessage.value = newMessage; // 保存引用
+    } else {
+      const newMessage = {
+        id: `response_${Date.now()}`,
+        type: 'response',
+        content: '',
+        contentR1: '',
+        timestamp: Date.now(),
+        isShowThink: false,
+      };
+      messageHistory.value.push(newMessage);
+      currentProcessingMessage.value = newMessage; // 保存引用
+    }
     while (true) {
       const { done, value } = await reader.read();
       if (done) {
@@ -353,13 +358,53 @@ async function handleSend(data) {
           switch (data.event) {
             case 'message':
               if (data.answer) {
-                const targetMessage = messageHistory.value.find((msg) => msg.id === newMessage.id);
-                if (targetMessage) {
-                  targetMessage.content += data.answer;
+                const targetMessage = messageHistory.value.find((msg) => msg.id === currentProcessingMessage.value.id);
+
+                if (!targetMessage) break;
+                let currentChunk = data.answer;
+
+                // 检查是否包含起始标签
+                const startIndex = currentChunk.indexOf('<think>');
+                if (startIndex !== -1) {
+                  // 找到起始标签:将标签前的内容作为正文
+                  if (startIndex > 0) {
+                    targetMessage.content += currentChunk.substring(0, startIndex);
+                  }
+                  // 进入思考模式,将标签后的内容追加到contentR1
+                  Thinking.value = true;
+                  const remainingContent = currentChunk.substring(startIndex + '<think>'.length);
+                  if (remainingContent) {
+                    targetMessage.contentR1 += remainingContent;
+                  }
+                } else if (Thinking.value) {
+                  // 结束标签
+                  const endIndex = currentChunk.indexOf('</think>');
+                  if (endIndex !== -1) {
+                    // 找到结束标签标签前的内容追加到contentR1
+                    if (endIndex > 0) {
+                      targetMessage.contentR1 += currentChunk.substring(0, endIndex);
+                    }
+                    //将标签后的内容作为正文
+                    const remainingContent = currentChunk.substring(endIndex + '</think>'.length);
+                    if (remainingContent) {
+                      targetMessage.content += remainingContent;
+                    }
+                    targetMessage.content += currentChunk;
+                    Thinking.value = false;
+                    targetMessage.isShowThink = false;
+                  } else {
+                    // 没有结束标签,继续追加到contentR1
+                    targetMessage.contentR1 += currentChunk;
+                  }
+                } else {
+                  targetMessage.content += currentChunk;
                 }
               }
               if (data.task_id && !taskID.value) taskID.value = data.task_id;
               if (data.conversation_id && !currentSessionID.value) currentSessionID.value = data.conversation_id;
+              if (data.message_id && !messageID.value) {
+                messageID.value = data.message_id;
+              }
               break;
           }
         } catch (error) {
@@ -367,6 +412,7 @@ async function handleSend(data) {
         }
       }
     }
+    getNextSuggest();
   } catch (error) {
     console.error('Error in handleSend:', error);
     // 在 UI 上显示错误信息
@@ -376,41 +422,57 @@ async function handleSend(data) {
       content: '请求错误',
       contentR1: '',
       timestamp: Date.now(),
+      isShowThink: false,
     });
   }
 }
+//创建新对话
+async function addNew() {
+  messageHistory.value = [];
+  currentSessionID.value = '';
+  taskID.value = '';
+  messageID.value = '';
+}
 // 上传文件
 const showModal = () => {
   open.value = !open.value;
 };
-async function customUpload(data) {
+// 上传文件
+const handleBeforeUpload = async (file) => {
   const formData = new FormData();
-  if (!data) {
-    return message.warn('请选择文件');
-  }
-  // 添加文件参数
-  formData.append('file', data.file);
-  // 添加用户标识参数
+  formData.append('file', file);
   formData.append('user', userid);
+
   try {
-    let response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
+    const response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
       method: 'POST',
       headers: {
-        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+        Authorization: APIKEY.value,
       },
       body: formData,
     });
-    // if (response) {
-    //   message.success('上传成功');
-    // }
-    console.log(response, '123');
-    if (!response) {
-      throw new Error('Network response was not ok');
+
+    const result = await response.json();
+    if (response.ok) {
+      message.success('上传成功');
+      const linkText = `[${result.name}](${result.source_url})`;
+      inputText.value += `\n${linkText}`;
+    } else {
+      message.error(`上传失败: ${result.message || '网络错误'}`);
     }
   } catch (error) {
     console.error('保存失败:', error);
+    message.error('上传失败,请重试');
   }
-}
+  return false;
+};
+
+const handleRemove = (file) => {
+  const index = fileList.value.findIndex((item) => item.uid === file.uid);
+  if (index !== -1) {
+    fileList.value.splice(index, 1);
+  }
+};
 //停止响应
 async function stopReq() {
   try {
@@ -418,7 +480,7 @@ async function stopReq() {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
-        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+        Authorization: APIKEY.value,
       },
       body: JSON.stringify({
         user: userid,
@@ -433,13 +495,12 @@ async function stopReq() {
 }
 //获取具体会话记录
 async function sessionsHistory(id: string) {
-  console.log(id, '123');
   try {
     let response = await fetch(`http://39.97.59.228:8000/v1/messages?conversation_id=${id}&user=${userid}`, {
       method: 'GET',
       headers: {
         'Content-Type': 'application/json',
-        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+        Authorization: APIKEY.value,
       },
     });
     const data = await response.json();
@@ -447,19 +508,45 @@ async function sessionsHistory(id: string) {
     if (data.data.length > 0) {
       messageHistory.value = [];
       data.data.forEach((item: any) => {
+        currentSessionID.value = item.conversation_id;
         messageHistory.value.push({
           id: `user_${Date.now()}`,
           type: 'user',
           content: item.query,
           contentR1: '',
           timestamp: Date.now(),
+          isShowThink: false,
         });
+        const answer = item.answer;
+        // 查找<think>
+        const startIndex = answer.indexOf('<think>');
+        const endIndex = answer.indexOf('</think>');
+        let content = '';
+        let contentR1 = '';
+        // 根据标签判断
+        if (startIndex !== -1 && endIndex !== -1) {
+          content = answer.substring(0, startIndex) + answer.substring(endIndex + '</think>'.length);
+          contentR1 = answer.substring(startIndex + '<think>'.length, endIndex);
+        } else if (startIndex !== -1) {
+          // 只有起始标签
+          content = answer.substring(0, startIndex);
+          contentR1 = answer.substring(startIndex + '<think>'.length);
+        } else if (endIndex !== -1) {
+          // 只有结束标签
+          content = answer.substring(endIndex + '</think>'.length);
+          contentR1 = answer.substring(0, endIndex);
+        } else {
+          // 没有标签
+          content = answer;
+        }
+        // 添加到消息历史
         messageHistory.value.push({
           id: `system_${Date.now()}`,
           type: 'system',
-          content: item.answer,
-          contentR1: '',
+          content: content.trim(),
+          contentR1: contentR1.trim(),
           timestamp: Date.now(),
+          isShowThink: contentR1.length > 0, // 如果有思考内容则显示
         });
       });
     }
@@ -471,37 +558,32 @@ async function sessionsHistory(id: string) {
   }
   editingId.value = null;
 }
-//编辑标题
-const startEditing = (item: ListItem) => {
-  editingId.value = item.id;
-  editText.value = item.name || '';
-};
-// 输入框确认按钮点击事件
-async function handlePathConfirm() {
-  const formData = new FormData();
-  // 添加文件参数
-  formData.append('file', filePath.value);
-  // 添加用户标识参数
-  formData.append('user', userid);
+//获取下一轮建议问题列表
+//停止响应
+async function getNextSuggest() {
+  console.log(messageID.value, '123');
   try {
-    let response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
-      method: 'POST',
+    let response = await fetch(`http://39.97.59.228:8000/v1/messages/${messageID.value}/suggested?user=${userid}`, {
+      method: 'GET',
       headers: {
-        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+        'Content-Type': 'application/json',
+        Authorization: APIKEY.value,
       },
-      body: formData,
     });
-    if (!response.ok) {
+    const data = await response.json();
+    suggestList.value = data.data;
+    if (!response) {
       throw new Error('Network response was not ok');
     }
   } catch (error) {
     console.error('保存失败:', error);
   }
-  console.log('确认的文件路径:', filePath.value);
-  // 这里可以添加路径验证、保存等逻辑
-  filePath.value = ''; // 可选:确认后清空输入框
-  showConfirmBtn.value = false;
 }
+//编辑标题
+const startEditing = (item: ListItem) => {
+  editingId.value = item.id;
+  editText.value = item.name || '';
+};
 // 保存修改
 const handleSave = async (item: ListItem) => {
   try {
@@ -509,7 +591,7 @@ const handleSave = async (item: ListItem) => {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
-        Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+        Authorization: APIKEY.value,
       },
       body: JSON.stringify({
         name: editText.value,
@@ -539,7 +621,7 @@ const startDelete = async (item: ListItem) => {
           method: 'DELETE',
           headers: {
             'Content-Type': 'application/json',
-            Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+            Authorization: APIKEY.value,
           },
           body: JSON.stringify({
             user: userid,
@@ -571,14 +653,33 @@ async function getHistoryList() {
     method: 'get',
     headers: {
       'Content-Type': 'application/json',
-      Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+      Authorization: APIKEY.value,
     },
   });
   const data = await response.json();
   sessionHistory.value = data.data;
-  console.log(sessionHistory.value, '123');
+  data.data.forEach((item) => {
+    currentSessionID.value = item.conversation_id;
+  });
 }
-
+//格式化消息
+const formatMessage = (text) => {
+  if (!text) return '';
+  let formatted = text
+    // 处理换行
+    .replace(/\n\n/g, '<br>')
+    .replace(/\n###/g, '<br> ')
+    .replace(/###/g, '')
+    .replace(/---/g, '')
+    .replace(/^- /gm, '<br> - ')
+    // 处理粗体
+    .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
+    // 处理斜体
+    .replace(/\*(.*?)\*/g, '<em>$1</em>')
+    // 处理行内代码
+    .replace(/`([^`]+)`/g, '<code>$1</code>');
+  return formatted;
+};
 // 初始化按钮定位
 onMounted(() => {
   getHistoryList();
@@ -594,15 +695,15 @@ onMounted(() => {
 }
 .mini-chat {
   display: flex;
-  width: 500px;
-  height: 400px;
+  width: 550px;
+  height: 85%;
   border-radius: 4px;
   position: fixed;
-  top: 60px;
+  top: 70px;
   right: 20px;
   background-color: rgb(255, 255, 255);
   background: url('../../assets/images/warn-dialog-bg.png') no-repeat center;
-  background-size: 100% 100%;
+  background-size: 103% 130%;
   z-index: 9999999;
   color: #fff;
 }
@@ -617,8 +718,21 @@ onMounted(() => {
   width: 40px; /* 折叠时宽度 */
 }
 
+.addBtn {
+  height: 30px;
+  position: absolute;
+  background-size: 100% 100%;
+  background-position: center;
+  padding: 2px;
+  right: 10px;
+  bottom: 40px;
+  left: 10px;
+  align-items: center;
+  border-radius: 3px;
+  cursor: pointer;
+}
 .custom-list {
-  height: 325px;
+  height: 680px;
   overflow-y: auto;
 }
 .text-container {
@@ -682,7 +796,7 @@ onMounted(() => {
   color: #fff;
   white-space: nowrap;
   margin-left: 30px;
-  line-height: 35px;
+  line-height: 29px;
 }
 .historyBtn {
   width: 20px;
@@ -726,7 +840,7 @@ onMounted(() => {
   flex: 1; /* 占据剩余空间 */
   display: flex;
   flex-direction: column;
-
+  width: calc(100% - 134px) !important;
   .dialog-area {
     flex: 1; /* 占据剩余空间 */
     gap: 10px; /* 消息块间隔统一控制 */
@@ -740,7 +854,12 @@ onMounted(() => {
       padding: 10px;
       border-radius: 5px;
       background: #0c2842;
-      max-width: 80%;
+      max-width: 300px;
+      white-space: pre-wrap;
+      word-wrap: break-word;
+      overflow-wrap: break-word;
+      overflow: auto;
+      line-height: 1.5;
     }
     .answer-message {
       padding: 10px;
@@ -793,9 +912,9 @@ onMounted(() => {
   }
 
   .ant-input {
-    background-color: rgba(255, 255, 255, 0) !important;
     border: none;
-    outline: none;
+    background-color: rgba(255, 255, 255, 0) !important;
+    color: #fff;
   }
   .ant-input:focus {
     border: none; /* 聚焦时无边框 */
@@ -828,12 +947,12 @@ onMounted(() => {
     font-size: 10px;
     margin-right: 10px;
     cursor: pointer;
-    transition: all 0.2s;
+    transition: background-color 0.2s ease; /* 平滑过渡效果 */
   }
-
-  .control-btn:hover {
-    background-color: #043256;
-    color: #e2e8f0;
+  /* 激活状态样式(点击后) */
+  .control-btn.active {
+    background-color: #2cb6ff; /* 蓝色背景(Ant Design 主色) */
+    color: white; /* 白色文字 */
   }
   .control-btn1 {
     height: 20px;
@@ -932,6 +1051,48 @@ onMounted(() => {
   .custom-upload .ant-upload-select:hover .ant-btn {
     border-color: #1f84bd !important;
   }
+  .message-wrapper.ai-message-wrapper {
+    display: flex;
+    align-items: flex-start;
+  }
+  .answerIcon {
+    flex: 0 0 45px;
+  }
+
+  .suggestion-item {
+    height: 30px;
+    margin-left: 45px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 10px 16px;
+    border: 1px solid #1890ff;
+    color: white;
+    border-radius: 4px;
+    cursor: pointer;
+    width: 33%;
+    font-size: 12px;
+  }
+  .thinking-section {
+    border-left: 3px solid #e5e7eb;
+    padding-left: 12px;
+    margin-bottom: 16px;
+  }
+
+  .thinking-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px 0;
+    cursor: pointer;
+    user-select: none;
+  }
+
+  .thinking-title {
+    font-size: 14px;
+    font-weight: 500;
+    color: #6b7280;
+  }
 }
 </style>
 <style scoped>

+ 37 - 0
src/views/vent/home/configurable/components/detail/MiniBoard.vue

@@ -212,6 +212,10 @@
     --image-board-bg-nl2: url('/@/assets/images/home-container/configurable/tashanhome/board-bg-n3.png');
     --image-board-bg-nr2: url('/@/assets/images/home-container/configurable/tashanhome/board-bg-n4.png');
     --image-board-bg-o: url('/@/assets/images/home-container/configurable/tashanhome/board-bg-o.png');
+    --image-board_bg_7: url('/@/assets/images/home-container/configurable/board_bg_7.png');
+    --image-board_bg_8: url('/@/assets/images/home-container/configurable/board_bg_8.png');
+    --image-board_bg_9: url('/@/assets/images/home-container/configurable/board_bg_9.png');
+    --image-board_bg_10: url('/@/assets/images/home-container/configurable/board_bg_10.png');
 
     --image-hycd: url(/@/assets/images/home-container/configurable/dusthome/hycd.png);
     --image-dyfl: url(/@/assets/images/home-container/configurable/dusthome/dyfl.png);
@@ -474,6 +478,33 @@
     background-size: 100% 100%;
   }
 
+  .mini-board_P {
+    width: 97px;
+    height: 170px;
+    padding-top: 105px;
+    background-image: var(--image-board_bg_7), var(--image-board_bg_10);
+    background-size:
+      97px 105px,
+      100% 40px;
+    background-repeat: no-repeat;
+    background-position:
+      center top,
+      center bottom;
+  }
+  .mini-board_P:nth-of-type(2) {
+    width: 97px;
+    height: 170px;
+    padding-top: 105px;
+    background-image: var(--image-board_bg_8), var(--image-board_bg_9);
+    background-size:
+      97px 105px,
+      100% 40px;
+    background-repeat: no-repeat;
+    background-position:
+      center top,
+      center bottom;
+  }
+
   .mini-board__value_New {
     color: @vent-gas-primary-text;
     font-size: 15px;
@@ -655,6 +686,12 @@
     color: @vent-gas-primary-text;
   }
 
+  .mini-board__value_P {
+    font-family: 'douyuFont';
+    font-size: 20px;
+    margin-top: 10px;
+  }
+
   .mini-board_E:nth-child(1) {
     .mini-board__label_E {
       background-image: var(--image-hycd);

+ 16 - 39
src/views/vent/monitorManager/airDoor/components/door-content-r.vue

@@ -23,10 +23,13 @@
         </div>
         <div class="box-content">
           <!-- 二三维信息 -->
-          <gateDualSVG v-if="item.deviceType == 'gate_qd'" :ref="`modelRef${index}`" :indexCode="index"></gateDualSVG>
-          <gateSVG v-if="item.deviceType == 'gate_ss'" :ref="`modelRef${index}`" :indexCode="index"></gateSVG>
+          <!-- <gateDualSVG v-if="item.ndoorcount == '2'" :ref="`modelRef${index}`" :indexCode="index"></gateDualSVG>
+          <gateSVG v-if="item.ndoorcount == '1'" :ref="`modelRef${index}`" :indexCode="index"></gateSVG> -->
           <!-- <component :ref="`modelRef${index}`" :indexCode="index"
             :is="getModelComponent(globalConfig.is2DModel, item.deviceType)" /> -->
+          <gateDualSVG v-if="item.ndoorcount == '2'" :ref="(el) => setChildRef(el, index)" :indexCode="index">
+          </gateDualSVG>
+          <gateSVG v-if="item.ndoorcount == '1'" :ref="(el) => setChildRef(el, index)" :indexCode="index"></gateSVG>
         </div>
         <img src="@/assets/images/camera.png" alt="" @click="handlerCamera(item, index)" />
       </div>
@@ -43,13 +46,13 @@
       <CameraModal :cameraData="cameraData"></CameraModal>
     </Modal>
     <!-- 操作日志 -->
-    <operationModal :visible="visibleOperation"  @handleCancel="handleCancelOperation" >
+    <operationModal :visible="visibleOperation" @handleCancel="handleCancelOperation">
     </operationModal>
   </div>
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, inject, watch, onMounted, h, shallowRef } from 'vue';
+import { reactive, ref, inject, watch, nextTick } from 'vue';
 import syncModal from './syncModal.vue';
 import timeSetModal from './timeSetModal.vue';
 import CameraModal from './cameraModal.vue';
@@ -72,7 +75,6 @@ let props = defineProps({
     },
   },
 });
-
 const { createMessage } = useMessage();
 const globalConfig = inject<any>('globalConfig');
 let router = useRouter();
@@ -86,18 +88,12 @@ let Ids = ref('')
 //摄像头-控制显示与隐藏
 let modalVisible = ref(false);
 let cameraData = reactive({})
-/** 模型对应的组件,根据实际情况分为二维三维 */
-const modelRef0 = ref(null);
-const modelRef1 = ref(null);
-const modelRef2 = ref(null);
-const modelRef3 = ref(null);
-const modelRef4 = ref(null);
-const modelRef5 = ref(null);
-const modelRef6 = ref(null);
-const modelRef7 = ref(null);
-const modelRef8 = ref(null);
 //操作日志弹窗显示/关闭
 let visibleOperation = ref(false)
+const childRefs = ref<any[]>([])
+const setChildRef = (el, index) => {
+  childRefs.value[index] = el
+}
 
 
 
@@ -162,8 +158,8 @@ function handleCancelCamera(param) {
   modalVisible.value = param;
 }
 //操作日志弹窗-打开
-function handlerOperation(){
-    visibleOperation.value = true
+function handlerOperation() {
+  visibleOperation.value = true
 }
 
 //操作日志弹窗-关闭
@@ -172,25 +168,7 @@ function handleCancelOperation(param) {
 }
 //模型动画
 function monitorAnimation(selectData, index) {
-  if (index == 0) {
-    modelRef0.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  }else if (index == 1) {
-    modelRef1.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  } else if (index == 2) {
-    modelRef2.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  } else if (index == 3) {
-    modelRef3.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  } else if (index == 4) {
-    modelRef4.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  } else if (index == 5) {
-    modelRef5.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  } else if (index == 6) {
-    modelRef6.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  } else if (index == 7) {
-    modelRef7.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  } else if (index == 8) {
-    modelRef8.value[0]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-  }
+  childRefs.value[index]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
 }
 
 
@@ -198,13 +176,12 @@ watch(() => props.infoData, (newV, oldV) => {
   console.log(newV, 'new---')
   infoDatas.value = newV
   if (newV.length) {
-    setTimeout(() => {
+    nextTick(() => {
       newV.forEach((el: any, index: number) => {
         el = Object.assign(el, el.readData)
         monitorAnimation(el, index);
       })
-    }, 1000)
-
+    })
   }
 })
 

+ 1 - 1
src/views/vent/monitorManager/airDoor/components/door-menu-l.vue

@@ -57,7 +57,7 @@ watch(() => props.menuData, (newV, oldV) => {
     justify-content: space-between;
     align-items: center;
     width: 100%;
-    height: 48px;
+    height: 60px;
     color: #fff;
     margin-bottom: 10px;
     padding: 0px 15px 0px 40px;

+ 6 - 8
src/views/vent/monitorManager/airDoor/index.vue

@@ -31,22 +31,20 @@ function getMonitor(flag?) {
       await upcomingList()
       //获取左侧菜单数据
       await getMenuList()
+      if (timer) {
+        timer = null;
+      }
       getMonitor(false);
     },
-    flag ? 0 : 10000
+    flag ? 0 : 5000
   );
 }
+
 //左侧数据
 async function getMenuList() {
   let res = await getDevice({ devicetype: "gate", pagetype: "normal" })
   console.log(res, 'menuList')
   menuData.value = res.msgTxt[0].datalist || []
-  // menuData.value = [
-  //   { readData: { frontGateOpen: '1', midGateOpen: '0', rearGateOpen: '1' }, strinstallpos: '1020中段2#穿脉入口' },
-  //   { readData: { frontGateOpen: '1', midGateOpen: '0', rearGateOpen: '1' }, strinstallpos: '1020中段上盘沿脉巷' },
-  //   { readData: { frontGateOpen: '1', midGateOpen: '0', rearGateOpen: '1' }, strinstallpos: '1020中段上盘沿脉巷与0#穿交叉口中段上盘沿脉巷与0#穿交叉口' },
-  //   { readData: { frontGateOpen: '1', midGateOpen: '0', rearGateOpen: '1' }, strinstallpos: '1060到1020回风巷斜坡道入口' },
-  // ]
 }
 async function upcomingList() {
   let res = await upcoming({})
@@ -60,7 +58,7 @@ async function upcomingList() {
 
 onMounted(() => {
   getMenuList()
-  getMonitor()
+  getMonitor(true)
 })
 onUnmounted(() => {
   if (timer) {

+ 15 - 11
src/views/vent/monitorManager/balancePressMonitor/components/balancePressHandleHistory.vue

@@ -1,12 +1,16 @@
 <template>
   <div class="handle-history">
-    <HandlerHistoryTable columns-type="operator_history" device-type="sys_surface_caimei"
-      :device-list-api="getTableList.bind(null, { strtype: 'pressurefan' })" designScope="pressurefan_history" />
+    <HandlerHistoryTable
+      columns-type="operator_history"
+      device-type="sys_surface_caimei"
+      :device-list-api="getTableList.bind(null, { strtype: 'pressurefan' })"
+      designScope="pressurefan_history"
+    />
   </div>
 </template>
 <script setup lang="ts">
-import HandlerHistoryTable from '../../comment/HandlerHistoryTable.vue';
-import { getTableList } from '../balancePress.api'
+  import HandlerHistoryTable from '../../comment/HandlerHistoryTable.vue';
+  import { getTableList } from '../balancePress.api';
   const props = defineProps({
     deviceType: {
       type: String,
@@ -15,12 +19,12 @@ import { getTableList } from '../balancePress.api'
     deviceId: {
       type: String,
       required: true,
-    }
-  })
+    },
+  });
 </script>
 <style lang="less" scoped>
-.handle-history {
-  width: 100%;
-  pointer-events: auto;
-}
-</style>
+  .handle-history {
+    width: 100%;
+    pointer-events: auto;
+  }
+</style>

+ 1 - 1
src/views/vent/monitorManager/comment/HandlerHistoryTable.vue

@@ -27,7 +27,7 @@
     },
     deviceListApi: {
       type: Function,
-      required: true,
+      required: false,
     },
     designScope: {
       type: String,

+ 16 - 28
src/views/vent/monitorManager/footageMonitor/components/moduleCommon.vue

@@ -7,8 +7,7 @@
         </template>
         <template #container>
           <div class="container-t">
-            <ModuleHead :menuData="menuList" :timeDate="timeDate" :devID="devID" :devLabel="devLabel"
-              @changeMenu="changeMenu" @changeTime="changeTime"></ModuleHead>
+            <ModuleHead  :timeDate="timeDate" @changeTime="changeTime"></ModuleHead>
           </div>
           <div class="container-b">
             <SingLineArea :option="option" :chartData="chartData" height="280px"></SingLineArea>
@@ -21,7 +20,7 @@
         </template>
         <template #container>
           <div class="container-t">
-            <ModuleHead :menuData="gasMenuList" :timeDate="timeDate" :devLabel="gasDevLabel" @changeMenu="changeGasMenu"
+            <ModuleHead :isShowSelect="true" :menuData="gasMenuList" :timeDate="timeDate" :devLabel="gasDevLabel" @changeMenu="changeGasMenu"
               @changeTime="changeTime"></ModuleHead>
           </div>
           <div class="container-b">
@@ -52,7 +51,7 @@
 
 <script setup lang="ts">
 
-import { onBeforeMount, ref, onMounted, onUnmounted, reactive, defineProps, } from 'vue';
+import {  ref, onMounted, onUnmounted, reactive, defineProps,watchEffect } from 'vue';
 import ventBox1 from '/@/components/vent/ventBox1.vue'
 import SingLineArea from '@/components/chart/SingLineArea.vue'
 import BarAndLine from '@/components/chart/BarAndLine.vue';
@@ -61,17 +60,20 @@ import { option, gasMenuList, chartsColumns, echatsOption, optionGas } from '../
 import { list, getCurveGraphData } from '../footage.api';
 import dayjs from 'dayjs';
 
+let props=defineProps({
+  optionValue:{
+    type:String,
+    default:''
+  }
+})
 let paramData = ref<any[]>([])
-let menuList = ref<any[]>([])
-let devID = ref('')
-let devLabel = ref('')
 let timeDate = reactive({
   startTime: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
   endTime: dayjs().format('YYYY-MM-DD'),
 })
-const chartData = ref<any[]>([]);
 let gasDevLabel = ref('T0(上隅角)')
-const xAxisPropType = ref('ttime');
+let xAxisPropType = ref('ttime');
+let chartData = ref<any[]>([]);
 let gasList = ref<any[]>([]);
 let chartGasData = ref<any[]>([])
 
@@ -85,11 +87,11 @@ function getMonitor(flag?) {
         timer = null;
       }
       await getMonitor();
-    }, flag ? 0 : 10000);
+    }, flag ? 0 : 5000);
   }
 };
 async function getCurveGraphDataList() {
-  let res = await getCurveGraphData({ devId: devID.value, startTime: timeDate.startTime, endTime: timeDate.endTime })
+  let res = await getCurveGraphData({ devId: props.optionValue, startTime: timeDate.startTime, endTime: timeDate.endTime })
   if (res.length) {
     paramData.value = res
     chartData.value = res.map(el => {
@@ -121,21 +123,6 @@ async function getCurveGraphDataList() {
   }
 }
 
-// async function getMenuList() {
-//   let res = await list({ devicekind: 'footageGas' })
-//   menuList.value = res.records.map(el => {
-//     return {
-//       label: el.strname,
-//       value: el.id,
-//     }
-//   }) || []
-//   devID.value = menuList.value[0].value
-//   devLabel.value = menuList.value[0].label
-// }
-function changeMenu(param) {
-  devID.value = param.id
-  getCurveGraphDataList()
-}
 function changeGasMenu(param) {
   gasDevLabel.value = param.label
   switch (param.label) {
@@ -184,9 +171,10 @@ function changeTime(param) {
   getCurveGraphDataList()
 }
 
+watchEffect(()=>{
+  props.optionValue &&  getCurveGraphDataList()
+})
 onMounted(async () => {
-  timer = null
-  // await getMenuList()
   await getMonitor(true)
 });
 onUnmounted(() => {

+ 12 - 5
src/views/vent/monitorManager/footageMonitor/components/moduleHead.vue

@@ -3,7 +3,7 @@
   <!-- Header部分 -->
   <div class="w-100% flex costume-header">
     <!-- 选择下拉框,自动填充剩余空间,这种实现是因为 Select 不支持 suffix -->
-    <Dropdown class="flex-grow-1 costume-header_left" :trigger="['click']" :bordered="false"
+    <Dropdown v-if="isShowSelect" class="flex-grow-1 costume-header_left" :trigger="['click']" :bordered="false"
       @open-change="visible = $event">
       <div class="flex-basis-100% flex flex-items-center" @click.prevent>
         <SwapOutlined class="w-30px" />
@@ -22,7 +22,7 @@
       </template>
     </Dropdown>
 
-    <div class="flex-basis-100% flex flex-items-center flex-grow-1 costume-header_right">
+    <div class="flex-basis-100% flex flex-items-center flex-grow-1 costume-header_right " :class="isShowSelect ? '' : 'costume-header_left'">
       <a-range-picker :showTime="false" valueFormat="YYYY-MM-DD" :value="[chartParams.startTime, chartParams.endTime]"
         :bordered="false" @change="onRangeChange" />
     </div>
@@ -35,6 +35,11 @@ import { MenuItem, Menu, Dropdown } from 'ant-design-vue';
 import { SwapOutlined, CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons-vue';
 
 let props = defineProps({
+  //是否显示Select组件
+  isShowSelect: {
+    type: Boolean,
+    default: false
+  },
   menuData: {
     type: Array,
     default: () => {
@@ -67,7 +72,7 @@ const chartParams = reactive({
   startTime: '',
   endTime: '',
 });
-let $emit=defineEmits(['changeMenu','changeTime'])
+let $emit = defineEmits(['changeMenu', 'changeTime'])
 
 
 
@@ -78,13 +83,13 @@ let $emit=defineEmits(['changeMenu','changeTime'])
 function selectHandler({ key }) {
   selectedDeviceID.value = key;
   selectedDeviceLabel.value = props.menuData.find((el: any) => el.value == key)?.label || ''
- $emit('changeMenu',{id:selectedDeviceID.value,label:selectedDeviceLabel.value}) 
+  $emit('changeMenu', { id: selectedDeviceID.value, label: selectedDeviceLabel.value })
 }
 
 function onRangeChange(__, time) {
   chartParams.startTime = time[0];
   chartParams.endTime = time[1];
-  $emit('changeTime',chartParams)
+  $emit('changeTime', chartParams)
 }
 
 watchEffect(() => {
@@ -139,11 +144,13 @@ watchEffect(() => {
 }
 
 ::v-deep .zxm-picker {
+  width: 100%;
   padding: 4px 2px 4px;
   background: transparent;
 }
 
 ::v-deep .zxm-picker-input>input {
+  text-align: center;
   color: #fff;
 }
 

+ 4 - 26
src/views/vent/monitorManager/footageMonitor/index.vue

@@ -3,7 +3,7 @@
     <customHeader :fieldNames="{ label: 'label', value: 'value', options: 'children' }" :options='options'
       @change="getSelectRow" :optionValue="optionValue">进尺与瓦斯涌出分析</customHeader>
     <div class="box-container">
-      <ModuleCommon></ModuleCommon>
+      <ModuleCommon :optionValue="optionValue"></ModuleCommon>
     </div>
   </div>
 </template>
@@ -12,13 +12,10 @@
 import { onBeforeMount, ref, onMounted, onUnmounted, nextTick, provide } from 'vue';
 import customHeader from '/@/components/vent/customHeader.vue';
 import ModuleCommon from './components/moduleCommon.vue';
-import { getTableList, list } from './footage.api'
+import { list } from './footage.api'
 
 let options = ref<any[]>([])
 let optionValue = ref('')
-let menuList = ref<any[]>([])
-let devID = ref('')
-let devLabel = ref('')
 
 
 async function getSysDataSource() {
@@ -32,34 +29,15 @@ async function getSysDataSource() {
   if (!optionValue.value) {
     optionValue.value = options.value[0]['value']
   }
-  // devID.value = menuList.value[0].value
-  // devLabel.value = menuList.value[0].label
-
-  // const res = await getTableList({ strtype: 'sys_surface_juejin', pagetype: 'normal' });
-  // if (!options.value && res) {
-  //   // 初始时选择第一条数据
-  //   options.value = res.records || [];
-  //   if (!optionValue.value) {
-  //     optionValue.value = options.value[0]['id']
-  //     getMenuList()
-  //   }
-  // }
 };
 
 // 切换检测数据
 async function getSelectRow(deviceID) {
-  // const currentData = dataSource.value.find((item: any) => {
-  //   return item.deviceID == deviceID
-  // })
-  // if (currentData) {
-  //   optionValue.value = currentData['deviceID']
-  //   Object.assign(selectData, currentData)
-  //   await getDeviceList()
-  // }
+  console.log(deviceID, '选项切换')
+  optionValue.value = deviceID
 }
 
 onMounted(async () => {
-  // optionValue.value = currentRoute.value['query']['id']
   await getSysDataSource()
 })
 </script>

+ 136 - 78
src/views/vent/monitorManager/sprayMonitor/component.vue

@@ -7,55 +7,64 @@
   >
     {{ mainTitle }}
   </customHeader>
-  <div class="bg" style="width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; overflow: hidden">
-    <a-spin :spinning="loading" />
-    <div id="model3D" v-show="!loading" style="width: 100%; height: 100%; position: absolute; overflow: hidden"></div>
-    <slot name="monitorSlot3D"></slot>
-  </div>
+  <slot :monitor-data="monitorData"></slot>
   <div class="scene-box">
     <div class="center-container">
       <template v-if="activeKey == 'monitor'">
-        <!-- <balancePressHome v-if="activeKey == 'monitor'" :deviceId="optionValue" /> -->
+        <slot name="monitor">
+          <ModuleCommon
+            v-for="cfg in mainConfig.configs"
+            :key="cfg.deviceType"
+            :show-style="cfg.showStyle"
+            :module-data="cfg.moduleData"
+            :module-name="cfg.moduleName"
+            :device-type="cfg.deviceType"
+            :data="monitorData"
+            :visible="true"
+          />
+        </slot>
       </template>
       <div v-else class="history-group">
-        <div class="device-button-group" v-if="deviceList.length > 0">
+        <div v-if="showDeviceList && deviceList.length > 0" class="device-button-group">
           <div
-            class="device-button"
-            :class="{ 'device-active': deviceActive == device.deviceType }"
             v-for="(device, index) in deviceList"
+            :class="{ 'device-button': true, 'device-active': deviceActive == device.deviceType }"
             :key="index"
             @click="deviceChange(index)"
-            >{{ device.deviceName }}</div
           >
+            {{ device.deviceName }}
+          </div>
         </div>
         <div class="history-container">
-          <HistoryTable
-            v-if="activeKey == 'monitor_history'"
-            class="vent-margin-t-20"
-            :columns-type="`${deviceType}`"
-            :device-type="deviceType"
-            :sysId="optionValue"
-            :scroll="{ y: 650 }"
-            :only-bouned-devices="monitorHistoryConfig.onlyBounedDevices"
-            :show-history-curve="monitorHistoryConfig.showHistoryCurve"
-          />
-          <HandlerHistoryTable
-            v-if="activeKey == 'handler_history'"
-            class="vent-margin-t-20"
-            columns-type="operator_history"
-            :deviceType="deviceType"
-            :device-list-api="getHandlerList"
-          />
-
-          <AlarmHistoryTable
-            v-if="activeKey == 'faultRecord'"
-            columns-type="alarm"
-            :device-type="deviceType"
-            :list="getAlarmList"
-            :sys-id="optionValue"
-            :device-list-api="workFaceDeviceList.bind(null, { id: optionValue })"
-            designScope="alarm-history"
-          />
+          <slot name="history" :device-type="deviceType" :device-id="optionValue">
+            <HistoryTable
+              v-if="activeKey == 'monitor_history'"
+              class="vent-margin-t-20"
+              :columns-type="deviceType"
+              :device-type="deviceType"
+              :sysId="optionValue"
+              :scroll="{ y: 650 }"
+              v-bind="monitorHistoryConfig"
+            />
+          </slot>
+          <slot name="handler" :device-type="deviceType" :device-id="optionValue">
+            <HandlerHistoryTable
+              v-if="activeKey == 'handler_history'"
+              class="vent-margin-t-20"
+              columns-type="operator_history"
+              :deviceType="deviceType"
+              v-bind="handlerHistoryConfig"
+            />
+          </slot>
+          <slot name="alarm" :device-type="deviceType" :device-id="optionValue">
+            <AlarmHistoryTable
+              v-if="activeKey == 'faultRecord'"
+              columns-type="alarm"
+              :device-type="deviceType"
+              :sys-id="optionValue"
+              v-bind="alarmHistoryConfig"
+            />
+          </slot>
         </div>
       </div>
     </div>
@@ -68,58 +77,81 @@
   import { ref, onMounted, onUnmounted } from 'vue';
   import { getDevice, sysList } from '../comment/comment.api';
   import BottomMenu from '/@/views/vent/comment/components/bottomMenu.vue';
-  // import balancePressHome from './components/balancePressHome.vue';
+  import ModuleCommon from '../../home/configurable/components/ModuleCommon.vue';
   import HistoryTable from '../comment/HistoryTable.vue';
   import HandlerHistoryTable from '../comment/HandlerHistoryTable.vue';
+  import AlarmHistoryTable from '../comment/AlarmHistoryTable.vue';
   import { useRouter } from 'vue-router';
   import { Config } from '../../deviceManager/configurationTable/types';
-  import { defHttp } from '/@/utils/http/axios';
-  import { workFaceDeviceList } from '../../deviceManager/comment/warningTabel/warning.api';
+  import { message } from 'ant-design-vue';
+  import _ from 'lodash';
 
   type DeviceType = { deviceType: string; deviceName: string; datalist: any[] };
 
   const props = withDefaults(
     defineProps<{
       mainTitle: string;
+      /** 是否显示历史数据上方的设备列表 */
+      showDeviceList?: boolean;
+      /** 请求场景数据传入的类型字符 */
+      strtype: string;
+      /** 请求场景数据传入的页面类型字符 */
+      pagetype?: string;
       /** 获取各表格配置时依赖的设备类型 */
       // deviceType: string;
       /** 主要模块配置 */
       mainConfig: {
         configs: Config[];
+        /** 获取该场景所含设备及其监测信息的API */
+        monitorApi?: (params: { deviceType: string; deviceId: number | string }) => Promise<any>;
+        /** 定时获取监测信息的配置,单位为毫秒,不传入即默认,传0即停用 */
+        timer?: number;
       };
       /** 历史数据配置 */
       monitorHistoryConfig: {
         /** 请求历史数据时传入的类型字符 */
-        columnsType: string;
+        columnsType?: string;
+        /** 如果默认的设备类型不适用,可以传递固定的类型 */
+        deviceType?: string;
         /** 仅展示已绑定设备,选择是则从系统中获取sysId下已绑定设备。仅能查询到已绑定设备的历史数据 */
-        onlyBounedDevices: boolean;
+        onlyBounedDevices?: boolean;
         /** 显示历史数据曲线图 */
-        showHistoryCurve: boolean;
+        showHistoryCurve?: boolean;
       };
       /** 操作历史配置 */
       handlerHistoryConfig: {
         /** 请求操作历史时传入的类型字符 */
-        columnsType: string;
+        columnsType?: string;
+        /** 如果默认的设备类型不适用,可以传递固定的类型 */
+        deviceType?: string;
+        /** 获取操作历史的API,可以不提供以使用默认的请求 */
+        deviceListApi?: (params: any) => Promise<any[]>;
       };
       /** 报警历史配置 */
       alarmHistoryConfig: {
         /** 请求报警历史时传入的类型字符 */
-        columnsType: string;
+        columnsType?: string;
+        /** 如果默认的设备类型不适用,可以传递固定的类型 */
+        deviceType?: string;
+        /** 获取报警历史的API,可以不提供以使用默认的请求 */
+        list?: (params: any) => Promise<any[]>;
+        /** 获取设备以供报警历史过滤的API,可以不提供以使用默认的请求 */
+        deviceListApi?: (params: any) => Promise<any[]>;
       };
-      /** 请求场景数据传入的类型字符 */
-      strtype: string;
-      /** 请求场景数据传入的页面类型字符 */
-      pagetype: string;
     }>(),
     {
+      mainConfig: () => ({
+        configs: [],
+      }),
+      monitorHistoryConfig: () => ({}),
+      handlerHistoryConfig: () => ({}),
+      alarmHistoryConfig: () => ({}),
       pagetype: 'normal',
-      onlyBounedDevices: false,
-      showHistoryCurve: false,
+      showDeviceList: true,
     }
   );
 
   const { currentRoute } = useRouter();
-  const loading = ref(false);
 
   const activeKey = ref('monitor');
 
@@ -130,8 +162,11 @@
   const options = ref([]);
   const optionValue = ref('');
 
+  /** 获取左上角场景选择框数据的方法,如果此时初始场景未赋值则选择首项做初始化 */
   async function getSysDataSource() {
-    const res = await sysList({ strtype: props.strtype, pagetype: props.pagetype });
+    const res = await sysList({ strtype: props.strtype, pagetype: props.pagetype }).catch(() => {
+      message.error('获取场景数据时发生了错误');
+    });
     // 初始时选择第一条数据
     options.value = res.records || [];
     if (!optionValue.value) {
@@ -151,16 +186,18 @@
   const deviceType = ref('');
 
   function deviceChange(index) {
-    deviceType.value = deviceList.value[index].deviceType;
-    deviceActive.value = deviceList.value[index].deviceType;
+    deviceType.value = deviceList.value[index]?.deviceType || '';
+    deviceActive.value = deviceList.value[index]?.deviceType || '';
   }
 
   // 查询关联设备列表
   async function getDeviceList() {
-    const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value });
+    const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value }).catch(() => {
+      message.error('获取已绑定设备时发生了错误');
+    });
 
     deviceList.value = msgTxt.reduce((arr, item) => {
-      const data = item.datalist.forEach((data: any) => {
+      const data = item.datalist.map((data: any) => {
         return Object.assign(data, data.readData);
       });
       // sys代表场景本身,应该过滤掉去处理该场景下的关联设备
@@ -173,45 +210,66 @@
       }
 
       return arr;
-    });
+    }, []);
     if (!deviceActive.value) {
       deviceChange(0);
     }
   }
 
-  /** 获取操作历史 */
-  function getHandlerList() {
-    return sysList({ strtype: deviceType.value });
-  }
-
-  function getAlarmList() {
-    return (params) => defHttp.get({ url: '/safety/managesysAutoLog/list', params });
+  let timer: NodeJS.Timeout;
+  const monitorData = ref<any>({});
+  /** 获取本场景下所绑定的设备,将监测数据赋值 */
+  async function getMonitor() {
+    if (props.mainConfig.monitorApi) {
+      monitorData.value = await props.mainConfig
+        .monitorApi({
+          deviceType: deviceType.value,
+          deviceId: optionValue.value,
+        })
+        .catch(() => {
+          message.error('获取已绑定设备时发生了错误');
+        });
+    } else if (optionValue.value) {
+      const { msgTxt = [] } = await getDevice({ devicetype: 'sys', systemID: optionValue.value }).catch(() => {
+        message.error('获取已绑定设备时发生了错误');
+      });
+      msgTxt.forEach((item) => {
+        _.set(monitorData.value, item.type, item.datalist);
+      });
+    }
   }
 
-  onMounted(() => {
+  onMounted(async () => {
     if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
       optionValue.value = currentRoute.value['query']['id'] as string;
     }
-    getSysDataSource();
+    await getSysDataSource();
+    if (props.mainConfig.timer !== 0) {
+      timer = setInterval(() => {
+        getMonitor();
+      }, props.mainConfig.timer || 5000);
+    } else {
+      getMonitor();
+    }
   });
 
-  onUnmounted(() => {});
+  onUnmounted(() => {
+    clearInterval(timer);
+  });
 </script>
 <style lang="less" scoped>
   @import '/@/design/vent/modal.less';
   @ventSpace: zxm;
   .scene-box {
-    margin-top: 20px;
+    margin-top: 40px;
     pointer-events: none;
     .history-group {
-      padding: 0 20px;
+      margin-top: 80px;
+      padding: 0 10px;
       .history-container {
         pointer-events: auto;
-        position: relative;
         background: #6195af1a;
-        width: calc(100% + 10px);
-        top: 0px;
-        left: -10px;
+        // width: 100%;
         border: 1px solid #00fffd22;
         padding: 10px 0;
         box-shadow: 0 0 20px #44b4ff33 inset;
@@ -219,17 +277,17 @@
     }
     .device-button-group {
       // margin: 0 20px;
+      padding: 0 10px;
       display: flex;
       pointer-events: auto;
       position: relative;
-      margin-top: 90px;
       &::after {
         position: absolute;
         content: '';
-        width: calc(100% + 10px);
+        width: 100%;
         height: 2px;
         top: 30px;
-        left: -10px;
+        left: -1px;
         border-bottom: 1px solid #0efcff;
       }
       .device-button {
@@ -268,7 +326,7 @@
   }
   .center-container {
     width: 100%;
-    height: calc(100% - 200px);
+    height: calc(100% - 150px);
   }
 
   .input-box {

+ 308 - 3
src/views/vent/monitorManager/sprayMonitor/index.vue

@@ -1,6 +1,311 @@
+<!-- eslint-disable vue/multi-word-component-names -->
 <template>
-  <div></div>
+  <div class="spray-wrapper">
+    <MonitorComponent
+      main-title="凝胶防灭火材料自动喷洒系统"
+      :main-config="{
+        configs: defaultConfigs,
+      }"
+      :monitor-history-config="{}"
+      :handler-history-config="{}"
+      :alarm-history-config="{}"
+      strtype="sys_surface_juejin"
+    >
+      <template #default="{ monitorData }">
+        <div id="spray3D" class="w-full h-full">
+          <a-spin :spinning="loading" />
+          <div id="sprayCSS3D" v-show="!loading" style="width: 100%; height: 100%; position: absolute; overflow: hidden">
+            <FourBorderBg id="sprayCSS3DEnvA">
+              <div>送料电机</div>
+              <div>电压US:{{ monitorData.slus }}</div>
+              <div>电流LA:{{ monitorData.slla }}</div>
+              <div>电流LB:{{ monitorData.sllb }}</div>
+              <div>电流LC:{{ monitorData.sllc }}</div>
+            </FourBorderBg>
+            <FourBorderBg id="sprayCSS3DEnvB">
+              <div>输送电机</div>
+              <div>电压US:{{ monitorData.ssus }}</div>
+              <div>电流LA:{{ monitorData.ssla }}</div>
+              <div>电流LB:{{ monitorData.sslb }}</div>
+              <div>电流LC:{{ monitorData.sslc }}</div>
+            </FourBorderBg>
+          </div>
+        </div>
+      </template>
+    </MonitorComponent>
+  </div>
 </template>
-<script setup lang="ts"></script>
+<script setup lang="ts">
+  import { onMounted, ref } from 'vue';
+  import FourBorderBg from '/@/views/vent/comment/components/fourBorderBg.vue';
+  import MonitorComponent from './component.vue';
+  import { Config } from '../../deviceManager/configurationTable/types';
+  import { useInitConfigs } from '../../home/configurable/hooks/useInit';
+  import { mountedThree, setModelType } from './spray.three';
 
-<style lang="scss" scoped></style>
+  const loading = ref(false);
+  const defaultConfigs: Config[] = [
+    {
+      deviceType: '',
+      moduleName: '压力传感器',
+      pageType: 'spray',
+      moduleData: {
+        header: {
+          show: false,
+          readFrom: '',
+          selector: {
+            show: false,
+            value: '',
+          },
+          slot: {
+            show: false,
+            value: '',
+          },
+        },
+        background: {
+          show: false,
+          type: 'video',
+          link: '',
+        },
+        layout: {
+          direction: 'row',
+          items: [
+            {
+              name: 'board',
+              basis: '100%',
+            },
+          ],
+        },
+        board: [
+          {
+            type: 'P',
+            layout: 'label-top',
+            items: [
+              {
+                label: '在线数量',
+                value: '99',
+              },
+              {
+                label: '异常数量',
+                value: '0',
+              },
+            ],
+            readFrom: '',
+          },
+        ],
+      },
+      showStyle: {
+        size: 'width:440px;height:250px;',
+        version: '保德',
+        position: 'top:80px;left:10px;',
+      },
+    },
+    {
+      deviceType: '',
+      moduleName: '喷洒实况',
+      pageType: 'spray',
+      moduleData: {
+        header: {
+          show: false,
+          readFrom: '',
+          selector: {
+            show: false,
+            value: '',
+          },
+          slot: {
+            show: false,
+            value: '',
+          },
+        },
+        background: {
+          show: false,
+          type: 'video',
+          link: '',
+        },
+        layout: {
+          direction: 'row',
+          items: [
+            {
+              name: 'table',
+              basis: '100%',
+            },
+          ],
+        },
+        table: [
+          {
+            type: 'A',
+            columns: [
+              {
+                name: '支架',
+                prop: 'strinstallpos',
+              },
+              {
+                name: '工作状态',
+                prop: 'airStatus_str',
+              },
+            ],
+            readFrom: '',
+          },
+        ],
+        preset: [],
+      },
+      showStyle: {
+        size: 'width:440px;height:430px;',
+        version: '保德',
+        position: 'top:350px;left:10px;',
+      },
+    },
+    {
+      deviceType: '',
+      moduleName: '场景信息总览',
+      pageType: 'spray',
+      moduleData: {
+        header: { show: false, readFrom: '', selector: { show: false, value: '' }, slot: { show: false, value: '' } },
+        background: { show: false, type: 'video', link: '' },
+        layout: {
+          direction: 'column',
+          items: [
+            {
+              name: 'list',
+              basis: '50%',
+              overflow: false,
+            },
+            {
+              name: 'partition',
+              basis: 'auto',
+              overflow: false,
+            },
+            {
+              name: 'list',
+              basis: 'auto',
+              overflow: false,
+            },
+            {
+              name: 'partition',
+              basis: 'auto',
+              overflow: false,
+            },
+            {
+              name: 'list',
+              basis: 'auto',
+              overflow: false,
+            },
+          ],
+        },
+        list: [
+          {
+            type: 'L',
+            readFrom: '',
+            items: [
+              {
+                label: '绑定控制箱名称',
+                value: '${cumulativeFlow}',
+              },
+              {
+                label: '控制箱地址',
+                value: '${heaterTemperature}',
+              },
+              {
+                label: '控制箱在线状态',
+                value: '${nitrogen}',
+              },
+              {
+                label: '故障从机',
+                value: '${nitrogenContent}',
+              },
+              {
+                label: '浮球液位报警',
+                value: '${nitrogenContent}',
+              },
+              {
+                label: '输送管道压力',
+                value: '${nitrogenContent}',
+              },
+            ],
+          },
+          {
+            type: 'N',
+            readFrom: '',
+            items: [
+              {
+                label: '启动状态',
+                value: '0.97',
+                color: 'blue',
+              },
+              {
+                label: '报警状态',
+                value: '84.4',
+              },
+            ],
+          },
+          {
+            type: 'N',
+            readFrom: '',
+            items: [
+              {
+                label: '启动状态',
+                value: '0.97',
+                color: 'blue',
+              },
+              {
+                label: '故障状态',
+                value: '84.4',
+              },
+            ],
+          },
+        ],
+        partition: [
+          {
+            type: 'A',
+            readFrom: '',
+            layout: 'icon-pre',
+            label: '送料电机',
+            icon: '/src/assets/images/home-container/configurable/tashanhome/partition-icon-1.png',
+          },
+          {
+            type: 'A',
+            readFrom: '',
+            layout: 'icon-pre',
+            label: '输送电机',
+            icon: '/src/assets/images/home-container/configurable/tashanhome/partition-icon-2.png',
+          },
+        ],
+        mock: {},
+      },
+      showStyle: {
+        size: 'width:440px;height:700px;',
+        version: '原版',
+        position: 'top:80px;right:10px;',
+      },
+    },
+  ];
+
+  const { configs, fetchConfigs } = useInitConfigs();
+
+  onMounted(() => {
+    fetchConfigs('spray');
+    // loading.value = true;
+    // mountedThree('#spray3D', ['#sprayCSS3D', '#sprayCSS3DEnvA', '#sprayCSS3DEnvB']).then(() => {
+    //   setModelType('spray').finally(() => {
+    //     loading.value = false;
+    //   });
+    // });
+  });
+</script>
+
+<style lang="less" scoped>
+  .spray-wrapper {
+    width: 100%;
+    height: 100%;
+  }
+
+  .spray-wrapper :deep(.list-item_L .list-item__icon_L) {
+    background-image: url('/@/assets/images/home-container/configurable/minehome/list-icon-file.png');
+  }
+  .spray-wrapper :deep(.list-item_N:nth-child(1)) {
+    background-image: url('/@/assets/images/home-container/configurable/minehome/list-bg-n5.png');
+  }
+  .spray-wrapper :deep(.list-item_N:nth-child(2)) {
+    background-image: url('/@/assets/images/home-container/configurable/minehome/list-bg-n6.png');
+  }
+</style>

+ 113 - 0
src/views/vent/monitorManager/sprayMonitor/spray.three.ts

@@ -0,0 +1,113 @@
+import * as THREE from 'three';
+import UseThree from '../../../../utils/threejs/useThree';
+import ModelContext from './spray.threejs.base';
+import { animateCamera } from '/@/utils/threejs/util';
+import useEvent from '../../../../utils/threejs/useEvent';
+
+/** 模型总控制器 */
+let model: UseThree;
+/** 当前展示的具体模型的 Object3D 对象 */
+let group: THREE.Object3D;
+/** 具体模型内容列表,包含此模型总控制器下的所有可用的具体模型内容 */
+const modelContextList: {
+  /** 当前模型类型,在控制器下有多个具体模型时分辨它们 */
+  type: string;
+  /** 模型的具体内容,即负责模型导入、绘制的上下文对象,一个控制器可以新建多个 */
+  context?: ModelContext;
+}[] = [];
+const { mouseDownFn } = useEvent();
+
+/** 分发鼠标事件到具体实现方法 */
+function dispatchMouseEvent(event) {
+  if (event.button == 0 && model && group) {
+    mouseDownFn(model, group, event, () => {});
+  }
+}
+
+/** 初始化模型CSS展示框的鼠标事件,应该在模型总控制器初始化后调用 */
+function initEventListender() {
+  if (!model) return;
+  model.canvasContainer?.addEventListener('mousedown', (e) => dispatchMouseEvent(e));
+  // model.orbitControls?.addEventListener('change', () => render());
+}
+
+/** 渲染并更新总模型 */
+// function render() {
+//   if (model && model.isRender && model.renderer) {
+//     // model.animationId = requestAnimationFrame(render);
+//     model.css3dRender?.render(model.scene as THREE.Scene, model.camera as THREE.PerspectiveCamera);
+//     model.renderer.render(model.scene as THREE.Scene, model.camera as THREE.PerspectiveCamera);
+//     model.stats?.update();
+//   }
+// }
+
+/** 刷新(再渲染)总模型 */
+// export function refreshModal() {
+//   render();
+//   // modelContextList.forEach((item) => {
+//   //   if (item.context) {
+//   //     item.context.render();
+//   //   }
+//   // });
+// }
+
+/** 设置模型类型并切换,不同的类型通常对应不同的具体模型,在模型总控制器下的具体模型会根据传入的参数彼此交互、切换 */
+export function setModelType(modelType: 'spray') {
+  return new Promise((resolve, reject) => {
+    if (!model) return reject('模型未初始化');
+    modelContextList.forEach(({ type, context }) => {
+      if (!context) return reject('模型未初始化');
+      if (modelType === type) {
+        group = context?.group as THREE.Object3D;
+        setTimeout(async () => {
+          if (!model.scene?.getObjectByName(group.name) && group) {
+            model.scene?.add(group);
+          }
+          const oldCameraPosition = { x: -693, y: 474, z: 398 };
+          const position = { x: 14.826074594663222, y: 16.901762713393836, z: 36.459944037951004 };
+          await animateCamera(
+            oldCameraPosition,
+            { x: 0, y: 0, z: 0 },
+            { x: position.x, y: position.y, z: position.z },
+            { x: 0, y: 0, z: 0 },
+            model,
+            0.8
+          );
+          resolve(null);
+        }, 400);
+      }
+    });
+  });
+}
+
+/** 挂载模型控制器,sceneSelctor表示放置模型的元素选择器,cssSelectors表示放置类似详情框的元素选择器,其中第一项需要是根元素选择器 */
+export function mountedThree(sceneSelctor: string, cssSelectors: string[]) {
+  return new Promise(async (resolve) => {
+    const [rootSelector, ...selectors] = cssSelectors;
+    model = new UseThree(sceneSelctor, rootSelector);
+    model.setEnvMap('test1.hdr');
+    /** @ts-ignore-next-line */
+    model.renderer.toneMappingExposure = 1.0;
+
+    const model1 = new ModelContext(model);
+    await model1.mountedThree();
+    model1.initCssElement(selectors);
+    modelContextList.push({
+      type: 'spray',
+      context: model1,
+    });
+
+    initEventListender();
+    model.animate();
+    resolve(null);
+  });
+}
+
+export const destroy = () => {
+  if (!model) return;
+  model.isRender = false;
+  modelContextList.forEach((item) => {
+    if (item.context) item.context.destroy();
+  });
+  model.destroy();
+};

+ 95 - 0
src/views/vent/monitorManager/sprayMonitor/spray.threejs.base.ts

@@ -0,0 +1,95 @@
+import * as THREE from 'three';
+import { CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer';
+import { setModalCenter } from '/@/utils/threejs/util';
+// import { setModalCenter } from '/@/utils/threejs/util';
+// import * as dat from 'dat.gui';
+// const gui = new dat.GUI();
+// gui.domElement.style = 'position:absolute;top:100px;left:10px;z-index:99999999999999';
+
+class ModelContext {
+  model;
+  modelName = 'tunFace';
+  // modelName = 'dedust';
+  group: THREE.Object3D | null = null;
+  /** 本模型内支持的锚点位置数组 */
+  anchors: [number, number, number][] = [
+    [-46.1, -12.8, 8],
+    // [0, 0, 0],
+    // 右侧传感器
+    [11.7, -14, 8],
+    // [-25.847, 8.783, 3.267],
+    // [31.142, 8.783, 3.267],
+  ];
+  /** 本模型显示的css详情元素数组 */
+  cssSprites: CSS3DSprite[] = [];
+
+  constructor(model) {
+    this.model = model;
+  }
+
+  addLight() {
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
+    directionalLight.position.set(6.3, 28, 20);
+    this.group?.add(directionalLight);
+    directionalLight.target = this.group as THREE.Object3D;
+
+    const pointLight = new THREE.PointLight(0xffffff, 1, 1000);
+    pointLight.position.set(45, 51, -4.1);
+    pointLight.shadow.bias = 0.05;
+    this.model.scene.add(pointLight);
+  }
+
+  /** 初始化css元素,将css元素选择器传入,该方法会将这些元素按顺序放入本模型支持的锚点中 */
+  initCssElement(selectors: string[]) {
+    selectors.forEach((selector, index) => {
+      const element = document.querySelector(selector) as HTMLElement;
+      if (element) {
+        const css3D = new CSS3DSprite(element);
+        this.cssSprites.push(css3D);
+        css3D.name = selector;
+        css3D.scale.set(0.05, 0.05, 0.05);
+        // const ff = gui.addFolder(`css元素${index}`);
+        // ff.add(css3D.position, 'x', -100, 100);
+        // ff.add(css3D.position, 'y', -100, 100);
+        // ff.add(css3D.position, 'z', -100, 100);
+        if (index < this.anchors.length) {
+          const [x, y, z] = this.anchors[index];
+          css3D.position.set(x, y, z);
+          this.group?.add(css3D);
+        } else {
+          console.warn(`指定的元素${selector}没有合适的位置放置`);
+        }
+      }
+    });
+  }
+
+  /** 清除css元素 */
+  clearCssElement() {
+    this.cssSprites.forEach((sprite) => {
+      this.group?.remove(sprite);
+    });
+    this.cssSprites = [];
+  }
+
+  mountedThree() {
+    return new Promise((resolve) => {
+      this.model.setGLTFModel([this.modelName]).then(async (gltf) => {
+        this.group = gltf[0];
+        if (this.group) {
+          setModalCenter(this.group);
+          resolve(null);
+          this.addLight();
+        }
+      });
+    });
+  }
+
+  destroy() {
+    if (this.model) {
+      this.model.clearGroup(this.group);
+      this.model = null;
+      this.group = null;
+    }
+  }
+}
+export default ModelContext;