Răsfoiți Sursa

解决冲突

hongrunxia 5 luni în urmă
părinte
comite
fba5af382a
50 a modificat fișierele cu 5174 adăugiri și 2437 ștergeri
  1. 1 1
      .env
  2. 2 2
      .env.development
  3. 2 0
      package.json
  4. BIN
      src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-1.png
  5. BIN
      src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-2.png
  6. BIN
      src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-3.png
  7. BIN
      src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-4.png
  8. BIN
      src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-5.png
  9. BIN
      src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb1.png
  10. BIN
      src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb2.png
  11. BIN
      src/assets/images/sealedGoaf/configurable/table/table1-label.png
  12. BIN
      src/assets/images/sealedGoaf/views/home/module-title.png
  13. 1245 1208
      src/components/AIChat/MiniChat.vue
  14. 1 1
      src/layouts/default/header/index.vue
  15. 9 2
      src/views/monitor/quartz/QuartzModal1.vue
  16. 2 11
      src/views/monitor/quartz/index1.vue
  17. 21 60
      src/views/monitor/quartz/quartz.api.ts
  18. 39 1
      src/views/monitor/quartz/quartz.data.ts
  19. 747 0
      src/views/sealedGoafSys/home/configurable.data.sealedGoaf.ts
  20. 138 0
      src/views/sealedGoafSys/home/sealedGoaf.vue
  21. 299 299
      src/views/vent/comment/history/HistoryTable.vue
  22. 1 1
      src/views/vent/dataCenter/APICenter/index.vue
  23. 496 0
      src/views/vent/dataCenter/deviceCenter/history/HistoryTable.vue
  24. 289 0
      src/views/vent/dataCenter/deviceCenter/history/HistoryTableFan.vue
  25. 78 0
      src/views/vent/dataCenter/deviceCenter/history/history.api.ts
  26. 86 0
      src/views/vent/dataCenter/deviceCenter/history/history.data.ts
  27. 95 94
      src/views/vent/dataCenter/deviceCenter/index.vue
  28. 20 2
      src/views/vent/dataCenter/infoCenter/index.vue
  29. 35 5
      src/views/vent/dataCenter/infoCenter/infoCenter.data.ts
  30. 32 46
      src/views/vent/dataCenter/stationCenter/index.vue
  31. 257 0
      src/views/vent/dataCenter/statsCenter/index.vue
  32. 18 0
      src/views/vent/dataCenter/statsCenter/stats.api.ts
  33. 11 2
      src/views/vent/deviceManager/configurationTable/types.ts
  34. 4 0
      src/views/vent/home/configurable/components/detail/CustomChart.vue
  35. 37 0
      src/views/vent/home/configurable/components/detail/CustomTable.vue
  36. 149 0
      src/views/vent/home/configurable/components/detail/MiniBoard.vue
  37. 12 6
      src/views/vent/monitorManager/airDoor/airdoor.api.ts
  38. 6 0
      src/views/vent/monitorManager/airDoor/airdoor.data.ts
  39. 86 0
      src/views/vent/monitorManager/airDoor/components/deviceControl.vue
  40. 133 26
      src/views/vent/monitorManager/airDoor/components/door-content-r.vue
  41. 17 18
      src/views/vent/monitorManager/airDoor/components/door-menu-l.vue
  42. 5 1
      src/views/vent/monitorManager/airDoor/components/syncModal.vue
  43. 9 9
      src/views/vent/monitorManager/airDoor/components/timeSetModal.vue
  44. 8 4
      src/views/vent/monitorManager/airDoor/index.vue
  45. 159 172
      src/views/vent/monitorManager/footageMonitor/components/moduleCommon.vue
  46. 100 0
      src/views/vent/monitorManager/safetyMonitor/WarnHistoryTable.vue
  47. 437 465
      src/views/vent/monitorManager/safetyMonitor/index.vue
  48. 7 0
      src/views/vent/monitorManager/safetyMonitor/safety.api.ts
  49. 80 0
      src/views/vent/monitorManager/safetyMonitor/safety.data.ts
  50. 1 1
      src/views/vent/performance/approvalPend/index.vue

+ 1 - 1
.env

@@ -19,4 +19,4 @@ VITE_GLOB_APP_OPEN_SSO=true
 VITE_GLOB_APP_OPEN_QIANKUN=true
 
 # 文件预览地址
-VITE_GLOB_ONLINE_VIEW_URL=http://fileview.jeecg.com/onlinePreview
+VITE_GLOB_ONLINE_VIEW_URL=http://fileview.jeecg.com/onlinePreview

+ 2 - 2
.env.development

@@ -30,6 +30,6 @@ VITE_GLOB_API_URL_PREFIX=
 #微前端qiankun应用,命名必须以VITE_APP_SUB_开头,jeecg-app-1为子应用的项目名称,也是子应用的路由父路径
 #VITE_APP_SUB_APP = [["micro-need-air", "//10.10.150.72:8099/"], ["micro-vent-3dModal", "//localhost:8091/"], ["micro-fire-front", "//localhost:8090/"]]
 # VITE_APP_SUB_APP = [["micro-vent-3dModal", "//192.168.183.154:8091/", "micro-vent-3dModal"], ["micro-need-air", "//192.168.183.88:8093/", "micro-need-air"], ["micro-fire-front", "//localhost:8097/", "fire-Micro"]]
-VITE_APP_SUB_APP = [["micro-vent-3dModal", "//192.168.1.16:8091/", "micro-vent-3dModal"], ["micro-need-air", "//192.168.183.88:8093/", "micro-need-air"], ["micro-fire-front", "//localhost:8097/", "fire-Micro"]]
-# VITE_APP_SUB_APP = [["micro-vent-3dModal", "//182.92.126.35:8091/", "micro-vent-3dModal"],["micro-vent-2dModal", "//localhost:8088/", "micro-vent-2dModal"],["micro-vent-doc", "//localhost:5173/", "micro-vent-doc"],["micro-need-air", "//182.92.126.35:8093/", "micro-need-air"], ["micro-fire-front", "//182.92.126.35:8097/", "fire-Micro"]]
+# VITE_APP_SUB_APP = [["micro-vent-3dModal", "//192.168.1.16:8091/", "micro-vent-3dModal"], ["micro-need-air", "//192.168.183.88:8093/", "micro-need-air"], ["micro-fire-front", "//localhost:8097/", "fire-Micro"]]
+VITE_APP_SUB_APP = [["micro-vent-3dModal", "//182.92.126.35:8091/", "micro-vent-3dModal"],["micro-vent-2dModal", "//localhost:8088/", "micro-vent-2dModal"],["micro-vent-doc", "//localhost:5173/", "micro-vent-doc"],["micro-need-air", "//182.92.126.35:8093/", "micro-need-air"], ["micro-fire-front", "//182.92.126.35:8097/", "fire-Micro"]]
 # VITE_APP_SUB_APP = [["micro-vent-3dModal", "//localhost:8091/"], ["micro-need-air", "//localhost:8099/"], ["micro-fire-front", "//localhost:8090/"]]

+ 2 - 0
package.json

@@ -132,6 +132,8 @@
     "@vitejs/plugin-legacy": "^2.0.0",
     "@vitejs/plugin-vue": "^4.3.3",
     "@vitejs/plugin-vue-jsx": "^3.0.2",
+    "@vue-office/docx": "^1.6.3",
+    "@vue-office/pdf": "^2.0.10",
     "@vue/compiler-sfc": "^3.3.4",
     "@vue/test-utils": "^2.4.1",
     "autoprefixer": "^10.4.16",

BIN
src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-1.png


BIN
src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-2.png


BIN
src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-3.png


BIN
src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-4.png


BIN
src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-5.png


BIN
src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb1.png


BIN
src/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb2.png


BIN
src/assets/images/sealedGoaf/configurable/table/table1-label.png


BIN
src/assets/images/sealedGoaf/views/home/module-title.png


+ 1245 - 1208
src/components/AIChat/MiniChat.vue

@@ -96,9 +96,9 @@
                         >思考过程:<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="message.parsedContentR1"></div>
+                    <div v-show="message.isShowThink" class="color-gray font-size-12px" v-html="formatMessage(message.parsedContentR1)"></div>
                   </div>
-                  <div v-if="message.parsedContent" v-html="message.parsedContent"> </div>
+                  <div v-if="message.parsedContent" v-html="formatMessage(message.parsedContent)"> </div>
                 </div>
                 <div class="copy-icon-container">
                   <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
@@ -115,6 +115,12 @@
         </div>
         <!-- 底部输入区 -->
         <div class="input-area">
+          <!-- <div class="file-preview" v-if="currentFile">
+            <div class="file-info">
+              📄 已选择文件:{{ currentFile.name }} (大小:{{ (currentFile.size / 1024).toFixed(2) }}KB)
+              <button @click="clearFile" class="clear-btn">×</button>
+            </div>
+          </div> -->
           <a-textarea v-model:value="inputText" placeholder="请输入你的问题" @keyup.enter="handleSend(inputText)" class="ant-input" auto-size />
           <div class="ctrl-btn">
             <div class="input-controls">
@@ -149,7 +155,7 @@
               accept=".pdf,.docx,.xlsx,.xls"
             >
               <a-button class="upload-btn">
-                <UploadOutlined />
+                <UploadOutlined></UploadOutlined>
                 从本地上传
               </a-button>
             </a-upload>
@@ -172,1341 +178,1372 @@
         </div>
       </div>
       <!-- 预览内容 -->
+      <div class="pre-container">
+        <!-- PDF预览 -->
+        <VueOfficePdf v-if="fileType === 'pdf'" :src="fileUrl" @rendered="handleRendered" @error="handleError" />
+        <!-- Word文档预览 -->
+        <VueOfficeDocx v-else-if="fileType === 'docx'" :src="fileUrl" @rendered="handleRendered" @error="handleError" />
+        <!-- Excel预览 -->
+        <VueOfficeExcel
+          v-else-if="fileType === 'xlsx' || fileType === 'xls'"
+          :options="{ xls: true }"
+          :src="fileUrl"
+          @rendered="handleRendered"
+          @error="handleError"
+        />
+        <!-- 不支持的文件类型 -->
+        <div v-else class="unsupported">
+          <p>不支持预览该文件类型: {{ fileType }}</p>
+        </div>
+        <!-- 加载状态 -->
+        <div v-if="loading" class="loading">
+          <p>文件加载中...</p>
+        </div>
+      </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-  // import VueOfficePdf from '@vue-office/pdf';
-  // import VueOfficeDocx from '@vue-office/docx';
-  // import VueOfficeExcel from '@vue-office/excel';
-  import { ref, onMounted, nextTick } from 'vue';
-  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,
-    RightOutlined,
-    DownOutlined,
-    FastForwardFilled,
-    RedoOutlined,
-  } from '@ant-design/icons-vue';
-  import { createVNode } from 'vue';
-  import { marked } from 'marked';
-  import katex from 'katex';
-  import 'katex/dist/katex.min.css';
-  // import '@vue-office/excel/lib/index.css';
-  // import '@vue-office/docx/lib/index.css';
-  const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
-  const inputText = ref(''); // 输入框内容
-  const refreshText = ref(''); //重新生成文本
-  const sessionHistory = ref([]);
-  const isShowChatBroad = ref(false);
-  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 isShowDoc = ref(false);
-  const fileType = ref('');
-  const fileUrl = ref('');
-  const APIKEY = ref('Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd');
-  interface ListItem {
-    id: number;
-    name?: string;
-  }
-  interface Message {
-    id: string; // 唯一标识(可用时间戳生成)
-    type: 'user' | 'system' | 'response';
-    content: String; // 原始 Markdown 字符串(用于拼接)
-    parsedContent: String; // 解析后的 HTML(用于渲染)
-    contentR1: String; // 原始思考过程 Markdown
-    parsedContentR1: String; // 解析后的思考过程 HTML
-    timestamp: number; // 排序依据
-    isShowThink: boolean; //深度思考展示
-  }
-  // 定义消息历史数组类型
-  const messageHistory = ref<Message[]>([]);
-  const isFold = ref(true); // 是否折叠
-  const userid = useUserStore().getUserInfo.id as string;
-  const filePath = ref(''); // 绑定输入框值
-  const uploadedFiles = ref([]);
-  const showConfirmBtn = ref(false); // 控制确认按钮显示状态
-  const fileList = ref([]);
-  const suggestList = ref([]); //建议列表
-  const loading = ref(false);
-  // 文件预览
-  const handleRendered = () => {
-    loading.value = false;
-    console.log('文件渲染完成');
-  };
-  const handleError = (error) => {
-    loading.value = false;
-    console.error('文件预览错误:', error);
-  };
-  function previewFile(data) {
-    fileType.value = data.name.split('.').pop().toLowerCase();
-    fileUrl.value = data.source_url;
-  }
-  function deleteFile(fileId) {
-    // 确认删除
-    if (confirm('确定要删除这个文件吗?')) {
-      uploadedFiles.value = uploadedFiles.value.filter((file) => file.id !== fileId);
-    }
-  }
-  //启用深度思考
-  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 dialogAreaRef = ref(null);
-  // 滚动到底部的方法
-  const scrollToBottom = async () => {
-    // 等待 DOM 更新(如消息渲染完成)
-    await nextTick();
-    if (dialogAreaRef.value) {
-      const el = dialogAreaRef.value;
-      // 关键:scrollTop = scrollHeight(滚动内容总高度)
-      el.scrollTop = el.scrollHeight;
-    }
-  };
+import VueOfficePdf from '@vue-office/pdf/lib/v3/vue-office-pdf.mjs';
+import VueOfficeDocx from '@vue-office/docx/lib/v3/vue-office-docx.mjs';
+import VueOfficeExcel from '@vue-office/excel/lib/v3/vue-office-excel.mjs';
+import { ref, onMounted, nextTick } from 'vue';
+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,
+  RightOutlined,
+  DownOutlined,
+  FastForwardFilled,
+  RedoOutlined,
+} from '@ant-design/icons-vue';
+import { createVNode } from 'vue';
+import { marked } from 'marked';
+import katex from 'katex';
+import 'katex/dist/katex.min.css';
+const TextArea = Input.TextArea; // 直接导入TextArea组件使用时打包报错
+const inputText = ref(''); // 输入框内容
+const refreshText = ref(''); //重新生成文本
+const sessionHistory = ref([]);
+const isShowChatBroad = ref(false);
+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 isShowDoc = ref(false);
+const fileType = ref('');
+const fileUrl = ref('');
+const APIKEY = ref('Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd');
+interface ListItem {
+  id: number;
+  name?: string;
+}
+interface Message {
+  id: string; // 唯一标识(可用时间戳生成)
+  type: 'user' | 'system' | 'response';
+  content: String; // 原始 Markdown 字符串(用于拼接)
+  parsedContent: String; // 解析后的 HTML(用于渲染)
+  contentR1: String; // 原始思考过程 Markdown
+  parsedContentR1: String; // 解析后的思考过程 HTML
+  timestamp: number; // 排序依据
+  isShowThink: boolean; //深度思考展示
+}
+// 定义消息历史数组类型
+const messageHistory = ref<Message[]>([]);
+const isFold = ref(true); // 是否折叠
+const userid = useUserStore().getUserInfo.id as string;
+const filePath = ref(''); // 绑定输入框值
+const uploadedFiles = ref([]);
+const showConfirmBtn = ref(false); // 控制确认按钮显示状态
+const fileList = ref([]);
+const suggestList = ref([]); //建议列表
+const loading = ref(false);
+// 文件预览
+const handleRendered = () => {
+  loading.value = false;
+  console.log('文件渲染完成');
+};
+const handleError = (error) => {
+  loading.value = false;
+  console.error('文件预览错误:', error);
+};
+function previewFile(data) {
+  fileType.value = data.name.split('.').pop().toLowerCase();
+  fileUrl.value = data.source_url;
+}
+function deleteFile(fileId) {
+  // 确认删除
+  if (confirm('确定要删除这个文件吗?')) {
+    uploadedFiles.value = uploadedFiles.value.filter((file) => file.id !== fileId);
+  }
+}
+//启用深度思考
+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 dialogAreaRef = ref(null);
+// 滚动到底部的方法
+const scrollToBottom = async () => {
+  // 等待 DOM 更新(如消息渲染完成)
+  await nextTick();
+  if (dialogAreaRef.value) {
+    const el = dialogAreaRef.value;
+    // 关键:scrollTop = scrollHeight(滚动内容总高度)
+    el.scrollTop = el.scrollHeight;
+  }
+};
 
-  // 点击建议项时的处理函数
-  const handleSuggestClick = (text) => {
-    // 将选中的建议填充到文本框
-    inputText.value = text;
-  };
-  function showAIChat() {
-    isShowChatBroad.value = !isShowChatBroad.value;
-    if (isShowChatBroad.value) {
-      isShowDoc.value = false;
-    }
+// 点击建议项时的处理函数
+const handleSuggestClick = (text) => {
+  // 将选中的建议填充到文本框
+  inputText.value = text;
+};
+function showAIChat() {
+  isShowChatBroad.value = !isShowChatBroad.value;
+  if (isShowChatBroad) {
+    isShowDoc.value = false;
   }
-  //复制消息
-  function copyToClipboard(text) {
-    if (!text || text.trim() === '') {
-      message.warn('没有可复制的内容');
-      return;
-    }
-    // 2. 创建临时textarea 元素
-    const textarea = document.createElement('textarea');
-    textarea.value = text;
-    textarea.style.position = 'fixed';
-    textarea.style.top = '-999px';
-    textarea.style.left = '-999px';
-    textarea.style.width = '200px';
-    textarea.style.height = '200px';
-    document.body.appendChild(textarea);
-    try {
-      textarea.select();
-      textarea.setSelectionRange(0, text.length);
-      const isSuccessful = document.execCommand('copy');
-      if (isSuccessful) {
-        message.success('复制成功!');
-      } else {
-        throw new Error('复制命令执行失败');
-      }
-    } catch (err) {
-      console.error('复制失败:', err);
-      message.error('复制失败,请手动复制');
-    } finally {
-      document.body.removeChild(textarea);
+}
+//复制消息
+function copyToClipboard(text) {
+  if (!text || text.trim() === '') {
+    message.warn('没有可复制的内容');
+    return;
+  }
+  // 2. 创建临时textarea 元素
+  const textarea = document.createElement('textarea');
+  textarea.value = text;
+  textarea.style.position = 'fixed';
+  textarea.style.top = '-999px';
+  textarea.style.left = '-999px';
+  textarea.style.width = '200px';
+  textarea.style.height = '200px';
+  document.body.appendChild(textarea);
+  try {
+    textarea.select();
+    textarea.setSelectionRange(0, text.length);
+    const isSuccessful = document.execCommand('copy');
+    if (isSuccessful) {
+      message.success('复制成功!');
+    } else {
+      throw new Error('复制命令执行失败');
     }
-  }
-  const initMarked = () => {
-    marked.setOptions({
-      gfm: true, // 启用GitHub风格的Markdown,包含表格
-      breaks: false, // 禁用换行符转换
-    });
-  };
-  // LaTeX 公式渲染(内部使用)
-  const renderLatexInHtml = (html) => {
-    // 匹配块级公式($$...$$)
-    const blockRegex = /\$\$(.*?)\$\$/gs;
-    // 匹配行内公式($...$)
-    const inlineRegex = /\$(.*?)\$/g;
-    // // 替换块级公式(居中显示)
-    html = html.replace(blockRegex, (match, formula) => {
-      matchCount++;
-      console.log(`块级公式匹配第 ${matchCount} 次:`, formula); // 查看匹配次数和内容
-      return katex.renderToString(formula.trim(), {
-        displayMode: true,
-        throwOnError: false,
-        strict: false,
-        trust: true,
-      });
+  } catch (err) {
+    console.error('复制失败:', err);
+    message.error('复制失败,请手动复制');
+  } finally {
+    document.body.removeChild(textarea);
+  }
+}
+const initMarked = () => {
+  marked.setOptions({
+    gfm: true, // 启用GitHub风格的Markdown,包含表格
+    breaks: false, // 禁用换行符转换
+  });
+};
+// LaTeX 公式渲染(内部使用)
+const renderLatexInHtml = (html) => {
+  // 匹配块级公式($$...$$)
+  const blockRegex = /\$\$(.*?)\$\$/gs;
+  // 匹配行内公式($...$)
+  const inlineRegex = /\$(.*?)\$/g;
+  // // 替换块级公式(居中显示)
+  html = html.replace(blockRegex, (match, formula) => {
+    return katex.renderToString(formula.trim(), {
+      displayMode: true,
+      throwOnError: false,
+      strict: false,
+      trust: true,
     });
-    // 替换行内公式(行内显示)
-    html = html.replace(inlineRegex, (match, formula) => {
-      return katex.renderToString(formula.trim(), {
-        displayMode: false,
-        throwOnError: false,
-        strict: false,
-      });
+  });
+  // 替换行内公式(行内显示)
+  html = html.replace(inlineRegex, (match, formula) => {
+    return katex.renderToString(formula.trim(), {
+      displayMode: false,
+      throwOnError: false,
+      strict: false,
     });
+  });
+  return html;
+};
+// Markdown + LaTeX 解析
+const parseMarkdownWithLatex = (mdStr) => {
+  if (!mdStr) return '';
+  try {
+    let html = marked(mdStr); // Markdown → HTML
+    html = renderLatexInHtml(html); // 替换公式
     return html;
-  };
-  // Markdown + LaTeX 解析
-  const parseMarkdownWithLatex = (mdStr) => {
-    if (!mdStr) return '';
-    try {
-      let html = marked(mdStr); // Markdown → HTML
-      html = renderLatexInHtml(html); // 替换公式
-      return html;
-    } catch (error) {
-      console.error('解析失败:', error);
-      return mdStr; // 降级显示原始字符串
+  } catch (error) {
+    console.error('解析失败:', error);
+    return mdStr; // 降级显示原始字符串
+  }
+};
+//重新生成
+const refresh = () => {
+  handleSend(refreshText.value);
+};
+//重新编辑
+const editAsk = (data) => {
+  inputText.value = data;
+};
+//获取消息列表
+async function handleSend(data) {
+  refreshText.value = data;
+  inputText.value = '';
+  if (isThinking) {
+    messageHistory.value.push({
+      id: `user_${Date.now()}`,
+      type: 'user',
+      content: '',
+      parsedContent: data,
+      contentR1: '',
+      parsedContentR1: '',
+      timestamp: Date.now(),
+      isShowThink: true,
+    });
+  } else {
+    messageHistory.value.push({
+      id: `user_${Date.now()}`,
+      type: 'user',
+      content: '',
+      parsedContent: data,
+      contentR1: '',
+      parsedContentR1: '',
+      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: APIKEY.value,
+      },
+      body: JSON.stringify({
+        conversation_id: currentSessionID.value ? currentSessionID.value : '',
+        query: data,
+        response_mode: 'streaming',
+        user: userid,
+        inputs: {},
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP error! status: ${response.status}`);
     }
-  };
-  //重新生成
-  const refresh = () => {
-    handleSend(refreshText.value);
-  };
-  //重新编辑
-  const editAsk = (data) => {
-    inputText.value = data;
-  };
-  //获取消息列表
-  async function handleSend(data) {
-    refreshText.value = data;
-    inputText.value = '';
-    if (isThinking.value) {
-      messageHistory.value.push({
-        id: `user_${Date.now()}`,
-        type: 'user',
+
+    const decoder = new TextDecoder('utf-8');
+    const reader = response.body.getReader();
+    let textBuffer = ''; // 使用字符串缓冲区来累积数据
+    // 在组件中定义
+    const currentProcessingMessage = ref(null);
+    if (isThinking) {
+      const newMessage = {
+        id: `response_${Date.now()}`,
+        type: 'response',
         content: '',
-        parsedContent: data,
+        parsedContent: '',
         contentR1: '',
         parsedContentR1: '',
         timestamp: Date.now(),
         isShowThink: true,
-      });
+      };
+      messageHistory.value.push(newMessage);
+      currentProcessingMessage.value = newMessage; // 保存引用
     } else {
-      messageHistory.value.push({
-        id: `user_${Date.now()}`,
-        type: 'user',
+      const newMessage = {
+        id: `response_${Date.now()}`,
+        type: 'response',
         content: '',
-        parsedContent: data,
+        parsedContent: '',
         contentR1: '',
         parsedContentR1: '',
         timestamp: Date.now(),
         isShowThink: false,
-      });
+      };
+      messageHistory.value.push(newMessage);
+      currentProcessingMessage.value = newMessage; // 保存引用
     }
-    try {
-      const response = await fetch('http://39.97.59.228:8000/v1/chat-messages', {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-          Authorization: APIKEY.value,
-        },
-        body: JSON.stringify({
-          conversation_id: currentSessionID.value ? currentSessionID.value : '',
-          query: data,
-          response_mode: 'streaming',
-          user: userid,
-          inputs: {},
-        }),
-      });
-
-      if (!response.ok) {
-        throw new Error(`HTTP error! status: ${response.status}`);
-      }
-
-      const decoder = new TextDecoder('utf-8');
-      const reader = response.body.getReader();
-      let textBuffer = ''; // 使用字符串缓冲区来累积数据
-      // 在组件中定义
-      const currentProcessingMessage = ref(null);
-      if (isThinking.value) {
-        const newMessage = {
-          id: `response_${Date.now()}`,
-          type: 'response',
-          content: '',
-          parsedContent: '',
-          contentR1: '',
-          parsedContentR1: '',
-          timestamp: Date.now(),
-          isShowThink: true,
-        };
-        messageHistory.value.push(newMessage);
-        currentProcessingMessage.value = newMessage; // 保存引用
-      } else {
-        const newMessage = {
-          id: `response_${Date.now()}`,
-          type: 'response',
-          content: '',
-          parsedContent: '',
-          contentR1: '',
-          parsedContentR1: '',
-          timestamp: Date.now(),
-          isShowThink: false,
-        };
-        messageHistory.value.push(newMessage);
-        currentProcessingMessage.value = newMessage; // 保存引用
-      }
-      while (true) {
-        const { done, value } = await reader.read();
-        if (done) {
-          if (textBuffer) {
-            processLine(textBuffer);
-          }
-          break;
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done) {
+        if (textBuffer) {
+          processLine(textBuffer);
         }
-        textBuffer += decoder.decode(value, { stream: true });
-        // 处理每一行数据
-        let lineIndex;
-        while ((lineIndex = textBuffer.indexOf('\n')) !== -1) {
-          const line = textBuffer.substring(0, lineIndex).trim();
-          textBuffer = textBuffer.substring(lineIndex + 1);
+        break;
+      }
+      textBuffer += decoder.decode(value, { stream: true });
+      // 处理每一行数据
+      let lineIndex;
+      while ((lineIndex = textBuffer.indexOf('\n')) !== -1) {
+        const line = textBuffer.substring(0, lineIndex).trim();
+        textBuffer = textBuffer.substring(lineIndex + 1);
 
-          if (line) {
-            processLine(line);
-          }
+        if (line) {
+          processLine(line);
         }
       }
-      function processLine(line) {
-        if (line.startsWith('data: ')) {
-          try {
-            const jsonStr = line.substring('data: '.length);
-            const data = JSON.parse(jsonStr);
-            switch (data.event) {
-              case 'message':
-                if (data.answer) {
-                  const targetMessage = messageHistory.value.find((msg) => msg.id === currentProcessingMessage.value.id);
+    }
+    function processLine(line) {
+      if (line.startsWith('data: ')) {
+        try {
+          const jsonStr = line.substring('data: '.length);
+          const data = JSON.parse(jsonStr);
+          switch (data.event) {
+            case 'message':
+              if (data.answer) {
+                const targetMessage = messageHistory.value.find((msg) => msg.id === currentProcessingMessage.value.id);
 
-                  if (!targetMessage) break;
-                  let currentChunk = data.answer;
+                if (!targetMessage) break;
+                let currentChunk = data.answer;
 
-                  // 检查是否包含起始标签
-                  const startIndex = currentChunk.indexOf('<think>');
-                  if (startIndex !== -1) {
-                    // 找到起始标签:将标签前的内容作为正文
-                    if (startIndex > 0) {
-                      targetMessage.content += currentChunk.substring(0, startIndex);
-                      targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
-                    }
-                    // 进入思考模式,将标签后的内容追加到contentR1
-                    Thinking.value = true;
-                    const remainingContent = currentChunk.substring(startIndex + '<think>'.length);
-                    if (remainingContent) {
-                      targetMessage.contentR1 += remainingContent;
+                // 检查是否包含起始标签
+                const startIndex = currentChunk.indexOf('<think>');
+                if (startIndex !== -1) {
+                  // 找到起始标签:将标签前的内容作为正文
+                  if (startIndex > 0) {
+                    targetMessage.content += currentChunk.substring(0, startIndex);
+                    targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
+                  }
+                  // 进入思考模式,将标签后的内容追加到contentR1
+                  Thinking.value = true;
+                  const remainingContent = currentChunk.substring(startIndex + '<think>'.length);
+                  if (remainingContent) {
+                    targetMessage.contentR1 += remainingContent;
+                    targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
+                  }
+                } else if (Thinking.value) {
+                  // 结束标签
+                  const endIndex = currentChunk.indexOf('</think>');
+                  if (endIndex !== -1) {
+                    // 找到结束标签:标签前的内容追加到contentR1
+                    if (endIndex > 0) {
+                      targetMessage.contentR1 += currentChunk.substring(0, endIndex);
                       targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
                     }
-                  } else if (Thinking.value) {
-                    // 结束标签
-                    const endIndex = currentChunk.indexOf('</think>');
-                    if (endIndex !== -1) {
-                      // 找到结束标签:标签前的内容追加到contentR1
-                      if (endIndex > 0) {
-                        targetMessage.contentR1 += currentChunk.substring(0, endIndex);
-                        targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
-                      }
-                      // 将标签后的内容作为正文
-                      const remainingContent = currentChunk.substring(endIndex + '</think>'.length);
-                      if (remainingContent) {
-                        targetMessage.content += remainingContent;
-                        targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
-                      }
-                      targetMessage.content += currentChunk;
-                      Thinking.value = false;
-                      targetMessage.isShowThink = false;
-                    } else {
-                      // 没有结束标签,继续追加到contentR1
-                      targetMessage.contentR1 += currentChunk;
-                      targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
+                    // 将标签后的内容作为正文
+                    const remainingContent = currentChunk.substring(endIndex + '</think>'.length);
+                    if (remainingContent) {
+                      targetMessage.content += remainingContent;
+                      targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
                     }
-                  } else {
                     targetMessage.content += currentChunk;
-                    targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
+                    Thinking.value = false;
+                    targetMessage.isShowThink = false;
+                  } else {
+                    // 没有结束标签,继续追加到contentR1
+                    targetMessage.contentR1 += currentChunk;
+                    targetMessage.parsedContentR1 = parseMarkdownWithLatex(targetMessage.contentR1);
                   }
-                  scrollToBottom(); // 每次接收消息都滚动
-                }
-                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;
+                } else {
+                  targetMessage.content += currentChunk;
+                  targetMessage.parsedContent = parseMarkdownWithLatex(targetMessage.content);
                 }
-                break;
-            }
-          } catch (error) {
-            console.warn('Error parsing stream chunk:', error, 'Chunk:', line);
+                scrollToBottom(); // 每次接收消息都滚动
+              }
+              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) {
+          console.warn('Error parsing stream chunk:', error, 'Chunk:', line);
         }
       }
-      getNextSuggest();
-    } catch (error) {
-      console.error('Error in handleSend:', error);
-      // 在 UI 上显示错误信息
-      messageHistory.value.push({
-        id: `system_${Date.now()}`,
-        type: 'system',
-        content: '',
-        parsedContent: '请求错误',
-        contentR1: '',
-        parsedContentR1: '',
-        timestamp: Date.now(),
-        isShowThink: false,
-      });
     }
+    getNextSuggest();
+  } catch (error) {
+    console.error('Error in handleSend:', error);
+    // 在 UI 上显示错误信息
+    messageHistory.value.push({
+      id: `system_${Date.now()}`,
+      type: 'system',
+      content: '',
+      parsedContent: '请求错误',
+      contentR1: '',
+      parsedContentR1: '',
+      timestamp: Date.now(),
+      isShowThink: false,
+    });
   }
-  //创建新对话
-  async function addNew() {
-    messageHistory.value = [];
-    currentSessionID.value = '';
-    taskID.value = '';
-    messageID.value = '';
-  }
-  // 上传文件
-  const showModal = (data) => {
-    open.value = data;
-  };
-  const close = () => {
-    isShowDoc.value = false;
-  };
-  // 上传文件
-  const handleBeforeUpload = async (file) => {
-    const formData = new FormData();
-    formData.append('file', file);
-    formData.append('user', userid);
+}
+//创建新对话
+async function addNew() {
+  messageHistory.value = [];
+  currentSessionID.value = '';
+  taskID.value = '';
+  messageID.value = '';
+}
+// 上传文件
+const showModal = (data) => {
+  open.value = data;
+};
+const close = () => {
+  isShowDoc.value = false;
+};
+// 上传文件
+const handleBeforeUpload = async (file) => {
+  const formData = new FormData();
+  formData.append('file', file);
+  formData.append('user', userid);
 
-    try {
-      const response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
-        method: 'POST',
-        headers: {
-          Authorization: APIKEY.value,
-        },
-        body: formData,
-      });
+  try {
+    const response = await fetch(`http://39.97.59.228:8000/v1/files/upload`, {
+      method: 'POST',
+      headers: {
+        Authorization: APIKEY.value,
+      },
+      body: formData,
+    });
 
-      const result = await response.json();
-      if (response.ok) {
-        const linkText = `[${result.name}](${result.source_url})`;
-        // inputText.value += `\n${linkText}`;
-        uploadedFiles.value.push(result);
-        isShowDoc.value = true;
-        previewFile(result);
-      } else {
-        message.error(`上传失败: ${result.message || '网络错误'}`);
-      }
-    } catch (error) {
-      console.error('保存失败:', error);
-      message.error('上传失败,请重试');
+    const result = await response.json();
+    if (response.ok) {
+      const linkText = `[${result.name}](${result.source_url})`;
+      // inputText.value += `\n${linkText}`;
+      uploadedFiles.value.push(result);
+      isShowDoc.value = true;
+      previewFile(result);
+    } else {
+      message.error(`上传失败: ${result.message || '网络错误'}`);
     }
-    return false;
-  };
+  } 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);
+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 {
+    let response = await fetch(`http://39.97.59.228:8000/v1/chat-messages/${taskID.value}/stop`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: APIKEY.value,
+      },
+      body: JSON.stringify({
+        user: userid,
+      }),
+    });
+    if (!response) {
+      throw new Error('Network response was not ok');
     }
-  };
-  //停止响应
-  async function stopReq() {
-    try {
-      let response = await fetch(`http://39.97.59.228:8000/v1/chat-messages/${taskID.value}/stop`, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-          Authorization: APIKEY.value,
-        },
-        body: JSON.stringify({
-          user: userid,
-        }),
-      });
-      if (!response) {
-        throw new Error('Network response was not ok');
-      }
-    } catch (error) {
-      console.error('保存失败:', error);
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+}
+//获取具体会话记录
+async function sessionsHistory(id: string, retryCount = 0) {
+  const API_KEYS = ['Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd', 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN'];
+  // 最大重试次数
+  const maxRetries = API_KEYS.length - 1;
+  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: API_KEYS[retryCount],
+      },
+    });
+    if (!response.ok) {
+      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
     }
-  }
-  //获取具体会话记录
-  async function sessionsHistory(id: string, retryCount = 0) {
-    const API_KEYS = ['Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd', 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN'];
-    // 最大重试次数
-    const maxRetries = API_KEYS.length - 1;
-    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: API_KEYS[retryCount],
-        },
-      });
-      if (!response.ok) {
-        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
-      }
-      const data = await response.json();
-      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: '',
-            parsedContent: item.query,
-            contentR1: '',
-            parsedContentR1: '',
-            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: '',
-            parsedContent: parseMarkdownWithLatex(content.trim()),
-            contentR1: '',
-            parsedContentR1: parseMarkdownWithLatex(contentR1.trim()),
-            timestamp: Date.now(),
-            isShowThink: contentR1.length > 0, // 如果有思考内容则显示
-          });
+    const data = await response.json();
+    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: '',
+          parsedContent: item.query,
+          contentR1: '',
+          parsedContentR1: '',
+          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: '',
+          parsedContent: parseMarkdownWithLatex(content.trim()),
+          contentR1: '',
+          parsedContentR1: parseMarkdownWithLatex(contentR1.trim()),
+          timestamp: Date.now(),
+          isShowThink: contentR1.length > 0, // 如果有思考内容则显示
         });
-      }
-    } catch (error) {
-      // 判断是否达到最大重试次数
-      if (retryCount < maxRetries) {
-        console.warn(`请求失败,正在尝试第 ${retryCount + 2} 个 API Key...`);
-        return sessionsHistory(id, retryCount + 1);
-      } else {
-        console.error('所有 API Key 均尝试失败:', error);
-        throw error;
-      }
-    }
-    editingId.value = null;
-  }
-  //获取下一轮建议问题列表
-  //停止响应
-  async function getNextSuggest() {
-    console.log(messageID.value, '123');
-    try {
-      let response = await fetch(`http://39.97.59.228:8000/v1/messages/${messageID.value}/suggested?user=${userid}`, {
-        method: 'GET',
-        headers: {
-          'Content-Type': 'application/json',
-          Authorization: APIKEY.value,
-        },
       });
-      const data = await response.json();
-      suggestList.value = data.data;
-      if (!response) {
-        throw new Error('Network response was not ok');
-      }
-    } catch (error) {
-      console.error('保存失败:', error);
+    }
+  } catch (error) {
+    // 判断是否达到最大重试次数
+    if (retryCount < maxRetries) {
+      console.warn(`请求失败,正在尝试第 ${retryCount + 2} 个 API Key...`);
+      return sessionsHistory(id, retryCount + 1);
+    } else {
+      console.error('所有 API Key 均尝试失败:', error);
+      throw error;
     }
   }
-  //编辑标题
-  const startEditing = (item: ListItem) => {
-    editingId.value = item.id;
-    editText.value = item.name || '';
-  };
-  // 保存修改
-  const handleSave = async (item: ListItem) => {
-    try {
-      let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}/name`, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-          Authorization: APIKEY.value,
-        },
-        body: JSON.stringify({
-          name: editText.value,
-          user: userid,
-        }),
-      });
-      if (!response.ok) {
-        throw new Error('Network response was not ok');
-      }
-      item.name = editText.value;
-    } catch (error) {
-      console.error('保存失败:', error);
+  editingId.value = null;
+}
+//获取下一轮建议问题列表
+//停止响应
+async function getNextSuggest() {
+  console.log(messageID.value, '123');
+  try {
+    let response = await fetch(`http://39.97.59.228:8000/v1/messages/${messageID.value}/suggested?user=${userid}`, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: APIKEY.value,
+      },
+    });
+    const data = await response.json();
+    suggestList.value = data.data;
+    if (!response) {
+      throw new Error('Network response was not ok');
     }
-    editingId.value = null;
-  };
-  // 删除会话
-  const startDelete = async (item: ListItem) => {
-    Modal.confirm({
-      title: '确认删除',
-      content: `确定要删除会话 "${item.name || '新会话'}" 吗?此操作不可撤销。`,
-      okText: '确认',
-      cancelText: '取消',
-      onOk: async () => {
-        // 原有删除逻辑不变
-        try {
-          let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}`, {
-            method: 'DELETE',
-            headers: {
-              'Content-Type': 'application/json',
-              Authorization: APIKEY.value,
-            },
-            body: JSON.stringify({
-              user: userid,
-            }),
-          });
-          if (!response.ok) {
-            throw new Error('Network response was not ok');
-          }
-          getHistoryList();
-        } catch (error) {
-          console.error('删除失败:', error);
-          Modal.error({
-            title: '删除失败',
-            content: '删除会话时出现错误,请稍后重试。',
-          });
-        }
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+}
+//编辑标题
+const startEditing = (item: ListItem) => {
+  editingId.value = item.id;
+  editText.value = item.name || '';
+};
+// 保存修改
+const handleSave = async (item: ListItem) => {
+  try {
+    let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}/name`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: APIKEY.value,
       },
+      body: JSON.stringify({
+        name: editText.value,
+        user: userid,
+      }),
     });
-  };
-  const fold = () => {
-    isFold.value = !isFold.value;
-    if (!isFold.value) {
-      getHistoryList();
+    if (!response.ok) {
+      throw new Error('Network response was not ok');
     }
-  };
-  // 获取历史会话列表
-  async function getHistoryList() {
-    sessionHistory.value = [];
-    try {
-      // 并行请求两个接口,提升效率
-      const [response1, response2] = await Promise.all([
-        // 第一个请求
-        fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
-          method: 'get',
+    item.name = editText.value;
+  } catch (error) {
+    console.error('保存失败:', error);
+  }
+  editingId.value = null;
+};
+// 删除会话
+const startDelete = async (item: ListItem) => {
+  Modal.confirm({
+    title: '确认删除',
+    content: `确定要删除会话 "${item.name || '新会话'}" 吗?此操作不可撤销。`,
+    okText: '确认',
+    cancelText: '取消',
+    onOk: async () => {
+      // 原有删除逻辑不变
+      try {
+        let response = await fetch(`http://39.97.59.228:8000/v1/conversations/${item.id}`, {
+          method: 'DELETE',
           headers: {
             'Content-Type': 'application/json',
-            Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+            Authorization: APIKEY.value,
           },
-        }),
-        // 第二个请求
-        fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
-          method: 'get',
-          headers: {
-            'Content-Type': 'application/json',
-            Authorization: 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN',
-          },
-        }),
-      ]);
-
-      // 检查响应是否成功
-      if (!response1.ok || !response2.ok) {
-        throw new Error('接口请求失败');
+          body: JSON.stringify({
+            user: userid,
+          }),
+        });
+        if (!response.ok) {
+          throw new Error('Network response was not ok');
+        }
+        getHistoryList();
+      } catch (error) {
+        console.error('删除失败:', error);
+        Modal.error({
+          title: '删除失败',
+          content: '删除会话时出现错误,请稍后重试。',
+        });
       }
+    },
+  });
+};
+const fold = () => {
+  isFold.value = !isFold.value;
+  if (!isFold.value) {
+    getHistoryList();
+  }
+};
+// 获取历史会话列表
+async function getHistoryList() {
+  sessionHistory.value = [];
+  try {
+    // 并行请求两个接口,提升效率
+    const [response1, response2] = await Promise.all([
+      // 第一个请求
+      fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
+        method: 'get',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: 'Bearer app-tSFRUnv0Qkbtik1dwtlhnpkd',
+        },
+      }),
+      // 第二个请求
+      fetch(`http://39.97.59.228:8000/v1/conversations?user=${userid}`, {
+        method: 'get',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: 'Bearer app-kprgsFKtySM4Wjxs0ZGzaNFN',
+        },
+      }),
+    ]);
 
-      // 解析两个响应的JSON数据
-      const [data1, data2] = await Promise.all([response1.json(), response2.json()]);
+    // 检查响应是否成功
+    if (!response1.ok || !response2.ok) {
+      throw new Error('接口请求失败');
+    }
 
-      // 合并两个数组(核心修改:用扩展运算符合并)
-      // 确保 data1.data 和 data2.data 都是数组(防止接口返回异常)
-      const list1 = Array.isArray(data1.data) ? data1.data : [];
-      const list2 = Array.isArray(data2.data) ? data2.data : [];
+    // 解析两个响应的JSON数据
+    const [data1, data2] = await Promise.all([response1.json(), response2.json()]);
 
-      // 合并数组并赋值给 sessionHistory
-      sessionHistory.value = [...list1, ...list2];
-      sessionHistory.value.forEach((item) => {
-        currentSessionID.value = item.id;
-      });
-      console.log(sessionHistory.value, '历史数据');
-    } catch (error) {
-      console.error('获取历史数据失败:', error);
-      // 错误处理(比如提示用户)
-    }
-  }
-  //格式化消息
-  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();
-    // 初始化(仅执行一次)
-    initMarked();
-  });
+    // 合并两个数组(核心修改:用扩展运算符合并)
+    // 确保 data1.data 和 data2.data 都是数组(防止接口返回异常)
+    const list1 = Array.isArray(data1.data) ? data1.data : [];
+    const list2 = Array.isArray(data2.data) ? data2.data : [];
+
+    // 合并数组并赋值给 sessionHistory
+    sessionHistory.value = [...list1, ...list2];
+    sessionHistory.value.forEach((item) => {
+      currentSessionID.value = item.id;
+    });
+    console.log(sessionHistory.value, '历史数据');
+  } catch (error) {
+    console.error('获取历史数据失败:', error);
+    // 错误处理(比如提示用户)
+  }
+}
+//格式化消息
+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();
+  // 初始化(仅执行一次)
+  initMarked();
+});
 </script>
 
 <style lang="less" scoped>
-  .btn-header {
-    width: 40px;
-    height: 40px;
-    margin-right: 5px;
-    margin-bottom: 5px;
-  }
-  .container {
-    display: flex;
-    flex-direction: row;
+.btn-header {
+  width: 40px;
+  height: 40px;
+  margin-right: 5px;
+  margin-bottom: 5px;
+}
+.container {
+  display: flex;
+  flex-direction: row;
+}
+.mini-chat {
+  display: flex;
+  min-width: none;
+  width: 950px;
+  height: 85%;
+  border-radius: 4px;
+  position: fixed;
+  top: 50px;
+  right: 20px;
+  padding: 10px;
+  background-color: #0a1a2f;
+  background-image: linear-gradient(to bottom, #0b69b6 1%, #0a1a2f 100%), linear-gradient(to left, #0b69b6, #0a1a2f),
+    linear-gradient(to top, #0b69b6, #0a1a2f), linear-gradient(to right, #0b69b6, #0a1a2f);
+  background-size: 100% 5px, 5px 100%, 100% 5px, 5px 100%;
+  background-position: top left, top right, bottom right, bottom left;
+  background-repeat: no-repeat;
+  z-index: 9999999;
+  color: #fff;
+}
+.doc {
+  display: flex;
+  flex-direction: column;
+  min-width: 600px;
+  height: 85%;
+  border-radius: 4px;
+  position: fixed;
+  top: 50px;
+  right: 51%;
+  padding: 10px;
+  background-color: #0a1a2f;
+  background-image: linear-gradient(to bottom, #0b69b6 1%, #0a1a2f 100%), linear-gradient(to left, #0b69b6, #0a1a2f),
+    linear-gradient(to top, #0b69b6, #0a1a2f), linear-gradient(to right, #0b69b6, #0a1a2f);
+  background-size: 100% 5px, 5px 100%, 100% 5px, 5px 100%;
+  background-position: top left, top right, bottom right, bottom left;
+  background-repeat: no-repeat;
+  z-index: 9999999;
+  color: #fff;
+}
+
+.close {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  width: 100%;
+  padding: 1px;
+  border-radius: 8px;
+}
+
+.closeBtn {
+  background-color: #007bff;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.closeBtn:hover {
+  background-color: #0056b3;
+}
+.left-side {
+  background: #0c2842;
+  transition: width 0.5s ease; /* 平滑过渡动画 */
+  width: 140px; /* 展开时宽度 */
+  position: relative; /* 用于按钮定位 */
+}
+.left-side.collapsed {
+  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: 650px;
+  overflow-y: auto;
+}
+.text-container {
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  overflow: hidden;
+}
+.btn-container {
+  display: flex;
+}
+.jeecg-layout-header-action span[role='img'] {
+  padding: 0;
+}
+.text-ellipsis {
+  flex: 1;
+}
+.edit-text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 90px;
+  color: #fff;
+  font-size: 12px;
+  cursor: pointer;
+}
+.edit-icon {
+  flex-shrink: 0;
+  margin-left: auto;
+  line-height: 23px;
+}
+.delete-icon {
+  flex-shrink: 0;
+  margin-left: auto;
+  line-height: 23px;
+}
+.edit-icon:hover {
+  color: #1890ff !important;
+  cursor: pointer;
+}
+.delete-icon:hover {
+  color: #1890ff !important;
+  cursor: pointer;
+}
+.edit-input {
+  font-size: 10px;
+}
+.btn-text-bg {
+  width: 14px;
+  height: 14px;
+  position: absolute;
+  background-size: 100% 100%;
+  right: 10px;
+  top: 10px;
+  left: 10px;
+  bottom: 10px;
+}
+.btn-text {
+  margin-left: 3px;
+  font-size: 12px;
+  color: #fff;
+  white-space: nowrap;
+  margin-left: 30px;
+  line-height: 29px;
+}
+.historyBtn {
+  width: 20px;
+  height: 20px;
+  position: absolute;
+  background-size: 100% 100%;
+  background-position: center;
+  padding: 2px;
+  right: 10px;
+  top: 10px;
+}
+.historyBtn1 {
+  width: 20px;
+  height: 20px;
+  position: absolute;
+  background-size: 100% 100%;
+  background-position: center;
+  left: 3px;
+}
+.divider0 {
+  border-bottom: 1px solid #1074c1;
+  width: auto;
+  margin: 0 10px;
+  height: 13%;
+  display: block;
+  background: transparent;
+}
+.foldBtn {
+  width: 20px;
+  height: 20px;
+  position: absolute;
+  background-size: 100% 100%;
+  background-position: center;
+  padding: 2px;
+  right: 10px;
+  bottom: 10px;
+  cursor: pointer;
+}
+
+.right-side {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  width: calc(100% - 134px) !important;
+  .title {
+    text-align: center;
+    font-size: 14px;
+    padding: 5px;
   }
-  .mini-chat {
+  .dialog-area {
+    flex: 1;
+    gap: 30px;
+    overflow-y: auto;
+    padding: 5px;
     display: flex;
-    min-width: none;
-    width: 950px;
-    height: 85%;
-    border-radius: 4px;
-    position: fixed;
-    top: 50px;
-    right: 20px;
-    padding: 10px;
-    background-color: #0a1a2f;
-    background-image:
-      linear-gradient(to bottom, #0b69b6 1%, #0a1a2f 100%), linear-gradient(to left, #0b69b6, #0a1a2f), linear-gradient(to top, #0b69b6, #0a1a2f),
-      linear-gradient(to right, #0b69b6, #0a1a2f);
-    background-size:
-      100% 5px,
-      5px 100%,
-      100% 5px,
-      5px 100%;
-    background-position:
-      top left,
-      top right,
-      bottom right,
-      bottom left;
-    background-repeat: no-repeat;
-    z-index: 9999999;
+    flex-direction: column;
     color: #fff;
+
+    .ask-message {
+      padding: 10px;
+      border-radius: 5px;
+      background: #0c2842;
+      max-width: 300px;
+      min-width: 50px;
+      white-space: pre-wrap;
+      word-wrap: break-word;
+      overflow-wrap: break-word;
+      overflow: auto;
+      line-height: 1.5;
+    }
+    .answer-message {
+      padding: 10px;
+      border-radius: 5px;
+      background: #0c2842;
+      max-width: 90%;
+    }
   }
-  .doc {
+
+  .input-area {
+    margin: 10px 10px 20px 10px;
+    padding: 10px;
+    background-color: #043256;
+    border: 1px solid #2cb6ff;
+    border-radius: 5px;
     display: flex;
     flex-direction: column;
-    min-width: 600px;
-    height: 85%;
+    justify-content: space-between;
+    gap: 5px;
+    height: 25%;
+  }
+  /* 文件展示区域 */
+  .file-preview {
+    margin-bottom: 10px;
+    padding: 8px;
+    border: 1px solid #e5e7eb;
     border-radius: 4px;
-    position: fixed;
-    top: 50px;
-    right: 51%;
-    padding: 10px;
-    background-color: #0a1a2f;
-    background-image:
-      linear-gradient(to bottom, #0b69b6 1%, #0a1a2f 100%), linear-gradient(to left, #0b69b6, #0a1a2f), linear-gradient(to top, #0b69b6, #0a1a2f),
-      linear-gradient(to right, #0b69b6, #0a1a2f);
-    background-size:
-      100% 5px,
-      5px 100%,
-      100% 5px,
-      5px 100%;
-    background-position:
-      top left,
-      top right,
-      bottom right,
-      bottom left;
-    background-repeat: no-repeat;
-    z-index: 9999999;
-    color: #fff;
+    background: #f9fafb;
   }
-
-  .close {
+  .file-info {
     display: flex;
-    justify-content: flex-end;
+    justify-content: space-between;
     align-items: center;
-    width: 100%;
-    padding: 1px;
-    border-radius: 8px;
+    font-size: 14px;
+    color: #333;
   }
-
-  .closeBtn {
-    background-color: #007bff;
-    color: white;
+  .clear-btn {
     border: none;
-    border-radius: 4px;
+    background: #ff4d4f;
+    color: white;
+    border-radius: 50%;
+    width: 20px;
+    height: 20px;
     cursor: pointer;
-    transition: all 0.3s ease;
-  }
-
-  .closeBtn:hover {
-    background-color: #0056b3;
+    font-size: 12px;
   }
-  .left-side {
-    background: #0c2842;
-    transition: width 0.5s ease; /* 平滑过渡动画 */
-    width: 140px; /* 展开时宽度 */
-    position: relative; /* 用于按钮定位 */
+  .file-content {
+    margin-top: 6px;
+    font-size: 13px;
+    color: #666;
+    line-height: 1.4;
   }
-  .left-side.collapsed {
-    width: 40px; /* 折叠时宽度 */
+  /* 文件列表容器 */
+  .uploaded-files {
+    padding: 8px;
+    background-color: #f5f5f5;
+    border-radius: 4px;
+    min-height: 40px;
   }
 
-  .addBtn {
-    height: 30px;
-    position: absolute;
-    background-size: 100% 100%;
-    background-position: center;
-    padding: 2px;
-    right: 10px;
-    bottom: 40px;
-    left: 10px;
+  /* 单个文件项 */
+  .file-item {
+    display: inline-flex;
     align-items: center;
-    border-radius: 3px;
-    cursor: pointer;
-  }
-  .custom-list {
-    height: 650px;
-    overflow-y: auto;
-  }
-  .text-container {
-    display: flex;
-    justify-content: space-between;
-    width: 100%;
-    overflow: hidden;
-  }
-  .btn-container {
-    display: flex;
-  }
-  .jeecg-layout-header-action span[role='img'] {
-    padding: 0;
-  }
-  .text-ellipsis {
-    flex: 1;
+    padding: 4px 8px;
+    margin-right: 8px;
+    margin-bottom: 8px;
+    background-color: #fff;
+    border: 1px solid #e9e9e9;
+    border-radius: 4px;
   }
-  .edit-text {
+
+  /* 文件名 */
+  .file-name {
+    margin-left: 8px;
+    margin-right: 8px;
+    max-width: 150px;
+    white-space: nowrap;
     overflow: hidden;
     text-overflow: ellipsis;
-    white-space: nowrap;
-    width: 90px;
-    color: #fff;
-    font-size: 12px;
-    cursor: pointer;
   }
-  .edit-icon {
-    flex-shrink: 0;
-    margin-left: auto;
-    line-height: 23px;
+
+  .ant-input {
+    border: none;
+    background-color: rgba(255, 255, 255, 0) !important;
+    color: #fff;
   }
-  .delete-icon {
-    flex-shrink: 0;
-    margin-left: auto;
-    line-height: 23px;
+  .ant-input:focus {
+    border: none; /* 聚焦时无边框 */
+    outline: none; /* 聚焦时无轮廓 */
+    box-shadow: none; /* 移除可能存在的阴影效果 */
   }
-  .edit-icon:hover {
-    color: #1890ff !important;
-    cursor: pointer;
-  }
-  .delete-icon:hover {
-    color: #1890ff !important;
-    cursor: pointer;
+  .ctrl-btn {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
   }
-  .edit-input {
-    font-size: 10px;
+  .question-input {
+    background-color: #1e293b !important;
+    border-color: #334155 !important;
+    color: #e2e8f0 !important;
+    border-radius: 8px !important;
+    padding: 12px 16px !important;
+    font-size: 14px !important;
   }
-  .btn-text-bg {
-    width: 14px;
-    height: 14px;
-    position: absolute;
-    background-size: 100% 100%;
-    right: 10px;
-    top: 10px;
-    left: 10px;
-    bottom: 10px;
+
+  .question-input::placeholder {
+    color: #64748b !important;
   }
-  .btn-text {
-    margin-left: 3px;
-    font-size: 12px;
+
+  .control-btn {
+    height: 25px;
+    background-color: #043256;
+    border: 1px solid #2cb6ff;
     color: #fff;
-    white-space: nowrap;
-    margin-left: 30px;
-    line-height: 29px;
+    font-size: 10px;
+    margin-right: 10px;
+    cursor: pointer;
+    transition: background-color 0.2s ease; /* 平滑过渡效果 */
   }
-  .historyBtn {
-    width: 20px;
-    height: 20px;
-    position: absolute;
-    background-size: 100% 100%;
-    background-position: center;
-    padding: 2px;
-    right: 10px;
-    top: 10px;
+  /* 激活状态样式(点击后) */
+  .control-btn.active {
+    background-color: #2cb6ff; /* 蓝色背景(Ant Design 主色) */
+    color: white; /* 白色文字 */
   }
-  .historyBtn1 {
-    width: 20px;
+  .control-btn1 {
     height: 20px;
-    position: absolute;
-    background-size: 100% 100%;
-    background-position: center;
-    left: 3px;
-  }
-  .divider0 {
-    border-bottom: 1px solid #1074c1;
-    width: auto;
-    margin: 0 10px;
-    height: 13%;
-    display: block;
-    background: transparent;
+    background-color: #234a6b;
+    border: 1px solid #234a6b;
+    color: #fff;
+    font-size: 10px;
+    margin-right: 10px;
+    cursor: pointer;
+    transition: all 0.2s;
   }
-  .foldBtn {
-    width: 20px;
+  .control-btn2 {
     height: 20px;
-    position: absolute;
-    background-size: 100% 100%;
-    background-position: center;
-    padding: 2px;
-    right: 10px;
-    bottom: 10px;
+    background-color: #2cb6ff;
+    border: 1px solid #2cb6ff;
+    color: #fff;
+    font-size: 10px;
+    margin-right: 10px;
     cursor: pointer;
+    transition: all 0.2s;
   }
-
-  .right-side {
-    flex: 1;
+  /* 文件上传区 */
+  .file-upload {
+    position: absolute;
+    right: 20px;
+    bottom: 70px;
+    width: 180px;
     display: flex;
     flex-direction: column;
-    width: calc(100% - 134px) !important;
-    .title {
-      text-align: center;
-      font-size: 14px;
-      padding: 5px;
-    }
-    .dialog-area {
-      flex: 1;
-      gap: 30px;
-      overflow-y: auto;
-      padding: 5px;
-      display: flex;
-      flex-direction: column;
-      color: #fff;
-
-      .ask-message {
-        padding: 10px;
-        border-radius: 5px;
-        background: #0c2842;
-        max-width: 300px;
-        min-width: 50px;
-        white-space: pre-wrap;
-        word-wrap: break-word;
-        overflow-wrap: break-word;
-        overflow: auto;
-        line-height: 1.5;
-      }
-      .answer-message {
-        padding: 10px;
-        border-radius: 5px;
-        background: #0c2842;
-        max-width: 90%;
-      }
-    }
-
-    .input-area {
-      margin: 10px 10px 20px 10px;
-      padding: 10px;
-      background-color: #043256;
-      border: 1px solid #2cb6ff;
-      border-radius: 5px;
-      display: flex;
-      flex-direction: column;
-      justify-content: space-between;
-      gap: 5px;
-      height: 25%;
-    }
-    /* 文件列表容器 */
-    .uploaded-files {
-      padding: 8px;
-      background-color: #f5f5f5;
-      border-radius: 4px;
-      min-height: 40px;
-    }
-
-    /* 单个文件项 */
-    .file-item {
-      display: inline-flex;
-      align-items: center;
-      padding: 4px 8px;
-      margin-right: 8px;
-      margin-bottom: 8px;
-      background-color: #fff;
-      border: 1px solid #e9e9e9;
-      border-radius: 4px;
-    }
-
-    /* 文件名 */
-    .file-name {
-      margin-left: 8px;
-      margin-right: 8px;
-      max-width: 150px;
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
-    }
-
-    .ant-input {
-      border: none;
-      background-color: rgba(255, 255, 255, 0) !important;
-      color: #fff;
-    }
-    .ant-input:focus {
-      border: none; /* 聚焦时无边框 */
-      outline: none; /* 聚焦时无轮廓 */
-      box-shadow: none; /* 移除可能存在的阴影效果 */
-    }
-    .ctrl-btn {
-      display: flex;
-      flex-direction: row;
-      justify-content: space-between;
-    }
-    .question-input {
-      background-color: #1e293b !important;
-      border-color: #334155 !important;
-      color: #e2e8f0 !important;
-      border-radius: 8px !important;
-      padding: 12px 16px !important;
-      font-size: 14px !important;
-    }
-
-    .question-input::placeholder {
-      color: #64748b !important;
-    }
-
-    .control-btn {
-      height: 25px;
-      background-color: #043256;
-      border: 1px solid #2cb6ff;
-      color: #fff;
-      font-size: 10px;
-      margin-right: 10px;
-      cursor: pointer;
-      transition: background-color 0.2s ease; /* 平滑过渡效果 */
-    }
-    /* 激活状态样式(点击后) */
-    .control-btn.active {
-      background-color: #2cb6ff; /* 蓝色背景(Ant Design 主色) */
-      color: white; /* 白色文字 */
-    }
-    .control-btn1 {
-      height: 20px;
-      background-color: #234a6b;
-      border: 1px solid #234a6b;
-      color: #fff;
-      font-size: 10px;
-      margin-right: 10px;
-      cursor: pointer;
-      transition: all 0.2s;
-    }
-    .control-btn2 {
-      height: 20px;
-      background-color: #2cb6ff;
-      border: 1px solid #2cb6ff;
-      color: #fff;
-      font-size: 10px;
-      margin-right: 10px;
-      cursor: pointer;
-      transition: all 0.2s;
-    }
-    /* 文件上传区 */
-    .file-upload {
-      position: absolute;
-      right: 20px;
-      bottom: 70px;
-      width: 180px;
-      display: flex;
-      flex-direction: column;
-      gap: 10px;
-      border: 1px solid #2cb6ff;
-      background-color: #234a6b;
-      border-radius: 6px;
-      padding: 10px;
-    }
-
-    .input-container {
-      position: relative;
-      display: flex;
-      align-items: center;
-      width: 100%;
-    }
-
-    .file-input {
-      flex: 1;
-      background-color: #234a6b;
-      border-color: #2cb6ff !important;
-      color: #e2e8f0 !important;
-      border-radius: 6px !important;
-      font-size: 10px !important;
-      padding-right: 70px !important;
-      height: 36px !important;
-      width: 100% !important;
-    }
-
-    .confirm-btn {
-      position: absolute;
-      right: 5px;
-      background-color: #2cb6ff;
-      border: none;
-      color: #fff;
-      border-radius: 4px;
-      font-size: 12px;
-      padding: 4px 10px;
-      cursor: pointer;
-      transition: all 0.2s;
-      height: 28px;
-    }
-
-    .confirm-btn:hover {
-      background-color: #2cb6ff;
-    }
-
-    .custom-upload {
-      width: 100%;
-      padding: 0 !important;
-    }
-
-    .upload-btn {
-      background-color: #234a6b !important;
-      border: 1px solid #2188c3 !important;
-      color: #dbeafe !important;
-      border-radius: 6px !important;
-      font-size: 12px !important;
-      cursor: pointer;
-      transition: all 0.2s;
-      padding: 8px 0 !important;
-      width: 190% !important;
-      height: 36px !important;
-      box-sizing: border-box !important;
-    }
-    .upload-btn:hover {
-      background-color: #1f84bd !important;
-      color: #fff !important;
-    }
-    .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>
-  .zxm-popover-inner-content {
-    padding: 1px;
+    gap: 10px;
+    border: 1px solid #2cb6ff;
+    background-color: #234a6b;
+    border-radius: 6px;
+    padding: 10px;
   }
-  .message-wrapper {
-    display: flex;
-    align-items: flex-start;
+
+  .input-container {
     position: relative;
+    display: flex;
+    align-items: center;
+    width: 100%;
   }
-  .user-message-wrapper {
-    flex-direction: row-reverse;
-  }
-  .ai-message-wrapper {
-    flex-direction: row;
-  }
-  /* 鼠标滑过 .message-wrapper 时,显示图标 */
-  .ai-message-wrapper:hover .copy-icon-container {
-    opacity: 1;
-    visibility: visible;
-  }
-  .user-message-wrapper:hover .copy-icon-container {
-    opacity: 1;
-    visibility: visible;
-  }
-  .copy-icon {
-    font-size: 16px;
-    color: #666;
-    cursor: pointer;
+
+  .file-input {
+    flex: 1;
+    background-color: #234a6b;
+    border-color: #2cb6ff !important;
+    color: #e2e8f0 !important;
+    border-radius: 6px !important;
+    font-size: 10px !important;
+    padding-right: 70px !important;
+    height: 36px !important;
+    width: 100% !important;
   }
-  /* 默认隐藏,hover 时显示 */
-  .copy-icon-container {
+
+  .confirm-btn {
     position: absolute;
-    display: flex;
-    flex-direction: row;
-    bottom: -20px;
-    right: 15%;
-    opacity: 0;
-    visibility: hidden;
-    transition: all 0.3s ease;
-    z-index: 10;
-  }
-  /* 复制图标样式 */
-  .copy-icon {
-    font-size: 16px;
-    cursor: pointer;
-    margin-right: 20%;
-    float: right;
+    right: 5px;
+    background-color: #2cb6ff;
+    border: none;
     color: #fff;
+    border-radius: 4px;
+    font-size: 12px;
+    padding: 4px 10px;
+    cursor: pointer;
+    transition: all 0.2s;
+    height: 28px;
   }
-  .message-wrapper:hover .copy-icon {
-    opacity: 1;
-  }
-  .copy-icon:hover {
-    color: #1890ff;
+
+  .confirm-btn:hover {
+    background-color: #2cb6ff;
   }
-  ::v-deep table {
-    border-collapse: collapse;
+
+  .custom-upload {
     width: 100%;
-    margin: 10px 0;
-    border: 1px solid #333;
+    padding: 0 !important;
   }
-  ::v-deep th {
-    border: 1px solid #333;
-    background-color: #234a6b;
-    padding: 5px;
-    text-align: center;
+
+  .upload-btn {
+    background-color: #234a6b !important;
+    border: 1px solid #2188c3 !important;
+    color: #dbeafe !important;
+    border-radius: 6px !important;
+    font-size: 12px !important;
+    cursor: pointer;
+    transition: all 0.2s;
+    padding: 8px 0 !important;
+    width: 190% !important;
+    height: 36px !important;
+    box-sizing: border-box !important;
   }
-  ::v-deep td {
-    border: 1px solid #ddd;
-    padding: 8px 12px;
-    text-align: center;
+  .upload-btn:hover {
+    background-color: #1f84bd !important;
+    color: #fff !important;
   }
-</style>
-<style scoped>
-  /* 已上传文件列表 */
-  .file-list {
-    margin-top: 20px;
+  .custom-upload .ant-upload-select:hover .ant-btn {
+    border-color: #1f84bd !important;
   }
-  .pre-container {
-    flex: 1;
-    width: 100%;
-    height: 100%;
-    overflow: auto;
+  .message-wrapper.ai-message-wrapper {
+    display: flex;
+    align-items: flex-start;
   }
-  .vue-office-excel {
-    background: #fff !important;
+  .answerIcon {
+    flex: 0 0 45px;
   }
 
-  .file-item {
+  .suggestion-item {
+    height: 30px;
+    margin-left: 45px;
     display: flex;
     align-items: center;
-    padding: 12px 15px;
-    background-color: #ddd;
-    border-radius: 6px;
-    margin-bottom: 10px;
-    transition: background-color 0.2s ease;
-  }
-
-  .file-item:hover {
-    background-color: #f0f2f5;
-  }
-
-  .file-info {
-    flex: 1;
-    overflow: hidden;
+    justify-content: space-between;
+    padding: 10px 16px;
+    border: 1px solid #1890ff;
+    color: white;
+    border-radius: 4px;
+    cursor: pointer;
+    width: 33%;
+    font-size: 12px;
   }
-
-  .file-name {
-    font-size: 15px;
-    color: #1d2129;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
-    margin-bottom: 3px;
+  .thinking-section {
+    border-left: 3px solid #e5e7eb;
+    padding-left: 12px;
+    margin-bottom: 16px;
   }
 
-  .file-actions {
+  .thinking-header {
     display: flex;
-    gap: 10px;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px 0;
+    cursor: pointer;
+    user-select: none;
   }
 
-  .btn {
-    padding: 0px 15px;
-    margin-left: 10px;
-    border-radius: 4px;
-    border: none;
+  .thinking-title {
     font-size: 14px;
-    cursor: pointer;
-    transition: all 0.2s ease;
+    font-weight: 500;
+    color: #6b7280;
   }
+}
+</style>
+<style scoped>
+.zxm-popover-inner-content {
+  padding: 1px;
+}
+.message-wrapper {
+  display: flex;
+  align-items: flex-start;
+  position: relative;
+}
+.user-message-wrapper {
+  flex-direction: row-reverse;
+}
+.ai-message-wrapper {
+  flex-direction: row;
+}
+/* 鼠标滑过 .message-wrapper 时,显示图标 */
+.ai-message-wrapper:hover .copy-icon-container {
+  opacity: 1;
+  visibility: visible;
+}
+.user-message-wrapper:hover .copy-icon-container {
+  opacity: 1;
+  visibility: visible;
+}
+.copy-icon {
+  font-size: 16px;
+  color: #666;
+  cursor: pointer;
+}
+/* 默认隐藏,hover 时显示 */
+.copy-icon-container {
+  position: absolute;
+  display: flex;
+  flex-direction: row;
+  bottom: -20px;
+  right: 15%;
+  opacity: 0;
+  visibility: hidden;
+  transition: all 0.3s ease;
+  z-index: 10;
+}
+/* 复制图标样式 */
+.copy-icon {
+  font-size: 16px;
+  cursor: pointer;
+  margin-right: 20%;
+  float: right;
+  color: #fff;
+}
+.message-wrapper:hover .copy-icon {
+  opacity: 1;
+}
+.copy-icon:hover {
+  color: #1890ff;
+}
+::v-deep table {
+  border-collapse: collapse;
+  width: 100%;
+  margin: 10px 0;
+  border: 1px solid #333;
+}
+::v-deep th {
+  border: 1px solid #333;
+  background-color: #234a6b;
+  padding: 5px;
+  text-align: center;
+}
+::v-deep td {
+  border: 1px solid #ddd;
+  padding: 8px 12px;
+  text-align: center;
+}
+</style>
+<style scoped>
+/* 已上传文件列表 */
+.file-list {
+  margin-top: 20px;
+}
+.pre-container {
+  flex: 1;
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+}
+.vue-office-excel {
+  background: #fff !important;
+}
 
-  .btn-preview {
-    background-color: #165dff;
-    color: white;
-  }
+.file-item {
+  display: flex;
+  align-items: center;
+  padding: 12px 15px;
+  background-color: #ddd;
+  border-radius: 6px;
+  margin-bottom: 10px;
+  transition: background-color 0.2s ease;
+}
 
-  .btn-preview:hover {
-    background-color: #0d47a1;
-  }
+.file-item:hover {
+  background-color: #f0f2f5;
+}
 
-  .btn-delete {
-    background-color: #f2f3f5;
-    color: #4e5969;
-  }
+.file-info {
+  flex: 1;
+  overflow: hidden;
+}
 
-  .btn-delete:hover {
-    background-color: #e5e6eb;
-    color: #1d2129;
-  }
+.file-name {
+  font-size: 15px;
+  color: #1d2129;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin-bottom: 3px;
+}
+
+.file-actions {
+  display: flex;
+  gap: 10px;
+}
+
+.btn {
+  padding: 0px 15px;
+  margin-left: 10px;
+  border-radius: 4px;
+  border: none;
+  font-size: 14px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.btn-preview {
+  background-color: #165dff;
+  color: white;
+}
+
+.btn-preview:hover {
+  background-color: #0d47a1;
+}
+
+.btn-delete {
+  background-color: #f2f3f5;
+  color: #4e5969;
+}
+
+.btn-delete:hover {
+  background-color: #e5e6eb;
+  color: #1d2129;
+}
 </style>

+ 1 - 1
src/layouts/default/header/index.vue

@@ -60,7 +60,7 @@
       <!-- 公司端不显示语音播报功能 weatherBroadcast.vue-->
       <WeatherBroadcast v-if="sysOrgCode != 'sdmtjtgsd' && isShowQy && portValue != '8062'" />
       <VoiceBroadcast v-if="sysOrgCode != 'sdmtjtgsd' && portValue != '8062'" />
-      <AIChat></AIChat>
+      <AIChat v-if="hasPermission('show:AIChat')"></AIChat>
       <VoiceBroadcastGsd v-if="sysOrgCode == 'sdmtjtgsd'" />
       <UserDropDown v-if="showUserDropdown" :theme="getHeaderTheme" />
       <LoginSelect ref="loginSelectRef" @success="loginSelectOk" />

+ 9 - 2
src/views/monitor/quartz/QuartzModal1.vue

@@ -8,11 +8,12 @@ import { ref, computed, unref } from 'vue';
 import { BasicModal, useModalInner } from '/@/components/Modal';
 import { BasicForm, useForm } from '/@/components/Form/index';
 import { formSchema } from './quartz.data';
-import { dataCenterSaveOrUpdateQuartz, getDataCenterQuartzById } from './quartz.api';
+import { dataCenterSaveQuartz, dataCenterUpdateQuartz } from './quartz.api';
 import { isJsonObjectString } from '/@/utils/is';
 // Emits声明
 const emit = defineEmits(['register', 'success']);
 const isUpdate = ref(true);
+const sysType = ref('');
 //表单配置
 const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
   // labelWidth: 150,
@@ -35,6 +36,8 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
     //data.record = await getQuartzById({id: data.record.id});
     try {
       data.record.paramterType = isJsonObjectString(data?.record?.parameter) ? 'json' : 'string';
+      sysType.value = data.record.sysType;
+      console.log(sysType.value, '123123');
     } catch (e) {
       console.log(e);
     }
@@ -52,7 +55,11 @@ async function handleSubmit(v) {
     let values = await validate();
     setModalProps({ confirmLoading: true });
     //提交表单
-    await dataCenterSaveOrUpdateQuartz(values, isUpdate.value);
+    if (isUpdate.value) {
+      await dataCenterUpdateQuartz(values, sysType.value, 'edit');
+    } else {
+      await dataCenterSaveQuartz(values, sysType.value, 'save');
+    }
     //关闭弹窗
     closeModal();
     //刷新列表

+ 2 - 11
src/views/monitor/quartz/index1.vue

@@ -42,15 +42,13 @@ import { useModal } from '/@/components/Modal';
 import { useListPage } from '/@/hooks/system/useListPage';
 import {
   getDataCenterList,
-  getDataCenterExportUrl,
-  getDataCenterImportUrl,
   deleteDataCenterQuartz,
   batchDataCenterDeleteQuartz,
   resumeDataCenterJob,
   pauseDataCenterJob,
   excuteDataCenterJob,
 } from './quartz.api';
-import { columns, searchFormSchema } from './quartz.data';
+import { dataCenterColumns, searchFormSchema } from './quartz.data';
 import QuartzModal from './QuartzModal1.vue';
 import { useMessage } from '/@/hooks/web/useMessage';
 
@@ -62,7 +60,7 @@ const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
   tableProps: {
     title: '任务列表',
     api: getDataCenterList,
-    columns: columns,
+    columns: dataCenterColumns,
     actionColumn: {
       width: 180,
     },
@@ -72,13 +70,6 @@ const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
       fieldMapToTime: [['fieldTime', ['beginDate', 'endDate'], 'YYYY-MM-DD HH:mm:ss']],
     },
   },
-  exportConfig: {
-    name: '定时任务列表',
-    url: getDataCenterExportUrl,
-  },
-  importConfig: {
-    url: getDataCenterImportUrl,
-  },
 });
 
 const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;

+ 21 - 60
src/views/monitor/quartz/quartz.api.ts

@@ -15,16 +15,8 @@ enum Api {
   deleteBatch = '/ventanaly/quartzJob/deleteBatch',
   //数据中心
   dataCenterList = '/dataCenter/ventanaly/quartzJob/list',
-  //数据采集 collect  数据分析  compute  应用  monitor    ------启动  resume   停止  pause  立即执行 execute
-  collect = '/dataCenter/collect/quartzJob/resume',
-  compute = '/dataCenter/compute/quartzJob/pause',
   dataCentersave = '/dataCenter/ventanaly/quartzJob/add',
   dataCenteredit = '/dataCenter/ventanaly/quartzJob/edit',
-  dataCenterget = '/dataCenter/ventanaly/quartzJob/queryById',
-  dataCenterdelete = '/dataCenter/ventanaly/quartzJob/delete',
-  dataCenterexportXlsUrl = '/dataCenter/ventanaly/quartzJob/exportXls',
-  dataCenterimportExcelUrl = '/dataCenter/ventanaly/quartzJob/importExcel',
-  dataCenterdeleteBatch = '/dataCenter/ventanaly/quartzJob/deleteBatch',
 }
 
 /**
@@ -123,37 +115,9 @@ export const batchDeleteQuartz = (params, handleSuccess) => {
  */
 export const getDataCenterList = (params) =>
   defHttp.get({
-    headers: {
-      'X-Access-Token':
-        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-    },
     url: Api.dataCenterList,
     params,
   });
-/**
- * 导出api
- */
-export const getDataCenterExportUrl = Api.exportXlsUrl;
-/**
- * 导入api
- */
-export const getDataCenterImportUrl = Api.importExcelUrl;
-
-/**
- * 保存或者更新任务
- * @param params
- */
-export const dataCenterSaveOrUpdateQuartz = (params, isUpdate) => {
-  let url = isUpdate ? Api.dataCenteredit : Api.dataCentersave;
-  return defHttp.post({
-    headers: {
-      'X-Access-Token':
-        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-    },
-    url: url,
-    params,
-  });
-};
 
 /**
  * 查询任务详情
@@ -161,10 +125,6 @@ export const dataCenterSaveOrUpdateQuartz = (params, isUpdate) => {
  */
 export const getDataCenterQuartzById = (params) => {
   return defHttp.get({
-    headers: {
-      'X-Access-Token':
-        'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-    },
     url: Api.get,
     params,
   });
@@ -178,10 +138,6 @@ export const deleteDataCenterQuartz = (params, handleSuccess) => {
   return defHttp
     .delete(
       {
-        headers: {
-          'X-Access-Token':
-            'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-        },
         url: Api.delete,
         data: params,
       },
@@ -205,10 +161,6 @@ export const batchDataCenterDeleteQuartz = (params, handleSuccess) => {
       return defHttp
         .delete(
           {
-            headers: {
-              'X-Access-Token':
-                'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-            },
             url: Api.deleteBatch,
             data: params,
           },
@@ -231,10 +183,6 @@ export const resumeDataCenterJob = (params: any, sysType: string, operation: str
   const apiPath = generateApiPath(currentModule, operation);
   return defHttp
     .get({
-      headers: {
-        'X-Access-Token':
-          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-      },
       url: apiPath,
       params,
     })
@@ -249,10 +197,6 @@ export const pauseDataCenterJob = (params: any, sysType: String, operation: Stri
   const apiPath = generateApiPath(currentModule, operation);
   return defHttp
     .get({
-      headers: {
-        'X-Access-Token':
-          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-      },
       url: apiPath,
       params,
     })
@@ -267,10 +211,6 @@ export const excuteDataCenterJob = (params: any, sysType: String, operation: Str
   const apiPath = generateApiPath(currentModule, operation);
   return defHttp
     .get({
-      headers: {
-        'X-Access-Token':
-          'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzU5ODQxMTcwfQ.fHaM3l-d88tUSLOs8sstXsDuSCn6Agbj_EMZMgV6waw',
-      },
       url: apiPath,
       params,
     })
@@ -278,3 +218,24 @@ export const excuteDataCenterJob = (params: any, sysType: String, operation: Str
       handleSuccess();
     });
 };
+
+/**
+ * 保存或者更新任务
+ * @param params
+ */
+export const dataCenterUpdateQuartz = (params, sysType: String, operation: String) => {
+  const currentModule = sysType;
+  const apiPath = generateApiPath(currentModule, operation);
+  return defHttp.post({
+    url: apiPath,
+    params,
+  });
+};
+export const dataCenterSaveQuartz = (params, sysType: String, operation: String) => {
+  const currentModule = sysType;
+  const apiPath = generateApiPath(currentModule, operation);
+  return defHttp.post({
+    url: apiPath,
+    params,
+  });
+};

+ 39 - 1
src/views/monitor/quartz/quartz.data.ts

@@ -34,6 +34,43 @@ export const columns: BasicColumn[] = [
     },
   },
 ];
+export const dataCenterColumns: BasicColumn[] = [
+  {
+    title: '任务类名',
+    dataIndex: 'jobClassName',
+    width: 200,
+    align: 'left',
+  },
+  {
+    title: 'Cron表达式',
+    dataIndex: 'cronExpression',
+    width: 200,
+  },
+  {
+    title: '参数',
+    dataIndex: 'parameter',
+    width: 200,
+  },
+  {
+    title: '系统类型',
+    dataIndex: 'sysType_dictText',
+    width: 200,
+  },
+  {
+    title: '描述',
+    dataIndex: 'description',
+    width: 200,
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 100,
+    customRender: ({ text }) => {
+      const color = text == '0' ? 'green' : text == '-1' ? 'red' : 'gray';
+      return render.renderTag(render.renderDict(text, 'quartz_status'), color);
+    },
+  },
+];
 
 export const searchFormSchema: FormSchema[] = [
   {
@@ -69,12 +106,13 @@ export const formSchema: FormSchema[] = [
   },
   {
     label: '系统类型',
-    field: 'jobSysType',
+    field: 'sysType',
     component: 'JDictSelectTag',
     componentProps: {
       dictCode: 'jobSysType',
       placeholder: '请选择系统类型',
     },
+    required: true,
   },
   {
     field: 'cronExpression',

+ 747 - 0
src/views/sealedGoafSys/home/configurable.data.sealedGoaf.ts

@@ -0,0 +1,747 @@
+import type { Config } from '@/views/vent/deviceManager/configurationTable/types';
+
+export const testConfigSealedGoaf: Config[] = [
+  // 1. 矿井状况
+  {
+    deviceType: 'goafsInfo',
+    moduleName: '矿井状况',
+    pageType: 'sealed_goaf',
+    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%' }],
+      },
+      list: [],
+      board: [
+        {
+          type: 'CKQMB',
+          readFrom: '',
+          layout: 'label-top',
+          items: [
+            {
+              label: '生产矿井',
+              value: '219',
+            },
+            {
+              label: '建设矿井',
+              value: '219',
+            },
+            {
+              label: '停产停建',
+              value: '17',
+            },
+            {
+              label: '长期停产停建',
+              value: '17',
+            },
+            {
+              label: '实施关闭',
+              value: '2',
+            },
+            {
+              label: '情况变化',
+              value: '1',
+            },
+          ],
+        },
+      ],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      complex_list: [],
+      preset: [],
+    },
+    showStyle: {
+      size: 'width:440px;height:200px;',
+      version: '原版',
+      position: 'top:30px;left:10px;',
+      headerPosition: 'centerBottom',
+    },
+  },
+  // 2. 当日生产状态
+  {
+    deviceType: 'spray_auto',
+    moduleName: '当日生产状态',
+    pageType: 'sealed_goaf',
+    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%' }],
+      },
+      list: [],
+      board: [
+        {
+          type: 'CKQMB',
+          readFrom: '',
+          layout: 'label-top',
+          items: [
+            {
+              label: '正在生产',
+              value: '210',
+            },
+            {
+              label: '正在建设',
+              value: '27',
+            },
+            {
+              label: '停产',
+              value: '17',
+            },
+            {
+              label: '停工',
+              value: '1',
+            },
+          ],
+        },
+      ],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      complex_list: [],
+      preset: [],
+    },
+    showStyle: {
+      size: 'width:440px;height:150px;',
+      version: '原版',
+      position: 'top:225px;left:10px;',
+    },
+  },
+  // 3. 煤层自燃倾向性
+  {
+    deviceType: 'deviceInfo',
+    moduleName: '煤层自燃倾向性',
+    pageType: 'sealed_goaf',
+    moduleData: {
+      header: {
+        show: false,
+        readFrom: '',
+        selector: {
+          show: false,
+          value: '',
+        },
+        slot: {
+          show: false,
+          value: '',
+        },
+      },
+      background: {
+        show: false,
+        type: 'video',
+        link: '',
+      },
+      layout: {
+        direction: 'column',
+        items: [
+          {
+            name: 'chart',
+            basis: '100%',
+          },
+        ],
+      },
+      chart: [
+        {
+          type: 'pie_drag',
+          readFrom: '',
+          legend: { show: false, formatter: '{b}:{c}\n{d}%' },
+          xAxis: [{ show: false }],
+          yAxis: [{ show: false, name: '风量', position: 'left' }],
+          series: [{ readFrom: 'piechart', xprop: 'pos', yprop: 'val', label: '' }],
+        },
+      ],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      list: [],
+      complex_list: [],
+      preset: [],
+      mock: {
+        piechart: [
+          { pos: 'Ⅰ类容易自燃', val: 50 },
+          { pos: 'Ⅱ类自燃', val: 80 },
+          { pos: 'Ⅲ类不易自燃', val: 40 },
+        ],
+      },
+    },
+    showStyle: {
+      size: 'width:440px;height:210px;',
+      version: '原版',
+      position: 'top:370px;left:10px;',
+      headerPosition: 'centerBottom',
+    },
+  },
+  // 4. 联网状态
+  {
+    deviceType: 'coalFireFeature',
+    moduleName: '联网状态',
+    pageType: 'sealed_goaf',
+    moduleData: {
+      header: {
+        show: false,
+        readFrom: '',
+        selector: {
+          show: true,
+          value: '',
+        },
+        slot: {
+          show: false,
+          value: '',
+        },
+      },
+      background: {
+        show: false,
+        type: 'video',
+        link: '',
+      },
+      layout: {
+        direction: 'column',
+        items: [
+          {
+            name: 'board',
+            basis: '40%',
+            overflow: true,
+          },
+          {
+            name: 'table',
+            basis: '60%',
+            overflow: true,
+          },
+        ],
+      },
+      board: [
+        {
+          type: 'CKQMB0',
+          readFrom: '',
+          layout: 'val-top',
+          items: [
+            {
+              label: '应接入',
+              value: '210',
+            },
+            {
+              label: '在线',
+              value: '27',
+            },
+            {
+              label: '中断',
+              value: '17',
+            },
+            {
+              label: '未接入',
+              value: '1',
+            },
+          ],
+        },
+      ],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [
+        {
+          type: 'CKQMB',
+          // parser: 'json',
+          readFrom: 'tableArray',
+          columns: [
+            {
+              name: ' ',
+              prop: 'index',
+            },
+            {
+              name: '应接入',
+              prop: 'yjr',
+            },
+            {
+              name: '在线',
+              prop: 'zx',
+            },
+            {
+              name: '中断',
+              prop: 'zd',
+            },
+            {
+              name: '未接入',
+              prop: 'wjr',
+            },
+          ],
+        },
+      ],
+      list: [],
+      complex_list: [],
+      preset: [],
+      mock: {
+        boardArray: [
+          {
+            label: '低风险',
+            value: '210',
+          },
+          {
+            label: '一般风险',
+            value: '27',
+          },
+          {
+            label: '较高风险',
+            value: '17',
+          },
+          {
+            label: '高风险',
+            value: '1',
+          },
+        ],
+        tableArray: [
+          {
+            index: '执法一处',
+            yjr: '73',
+            zx: '73',
+            zd: '0',
+            wjr: '0',
+          },
+          {
+            index: '执法二处',
+            yjr: '73',
+            zx: '73',
+            zd: '0',
+            wjr: '0',
+          },
+          {
+            index: '执法三处',
+            yjr: '73',
+            zx: '73',
+            zd: '0',
+            wjr: '0',
+          },
+          {
+            index: '执法四处',
+            yjr: '73',
+            zx: '73',
+            zd: '0',
+            wjr: '0',
+          },
+        ],
+      },
+    },
+    showStyle: {
+      size: 'width:440px;height:290px;',
+      version: '原版',
+      position: 'top:570px;left:10px;',
+    },
+  },
+  // 5. 密闭情况总览
+  {
+    deviceType: '',
+    moduleName: '密闭情况总览',
+    pageType: 'sealed_goaf',
+    moduleData: {
+      header: {
+        show: false,
+        readFrom: '',
+        selector: {
+          show: true,
+          value: '',
+        },
+        slot: {
+          show: false,
+          value: '',
+        },
+      },
+      background: {
+        show: false,
+        type: 'video',
+        link: '',
+      },
+      layout: {
+        direction: 'column',
+        items: [
+          {
+            name: 'board',
+            basis: '30%',
+            overflow: true,
+          },
+          {
+            name: 'table',
+            basis: '70%',
+            overflow: true,
+          },
+        ],
+      },
+      board: [
+        {
+          type: 'CKQMB1',
+          readFrom: '',
+          layout: 'val-top',
+          items: [
+            {
+              label: '低风险',
+              value: '210',
+            },
+            {
+              label: '一般风险',
+              value: '27',
+            },
+            {
+              label: '较高风险',
+              value: '17',
+            },
+            {
+              label: '高风险',
+              value: '1',
+            },
+          ],
+        },
+      ],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [
+        {
+          type: 'CKQMB',
+          // parser: 'json',
+          readFrom: 'tableArray',
+          columns: [
+            {
+              name: ' ',
+              prop: 'index',
+            },
+            {
+              name: '低风险',
+              prop: 'dfx',
+            },
+            {
+              name: '一般风险',
+              prop: 'ybfx',
+            },
+            {
+              name: '较高风险',
+              prop: 'jgfx',
+            },
+            {
+              name: '高风险',
+              prop: 'gfx',
+            },
+          ],
+        },
+      ],
+      list: [],
+      complex_list: [],
+      preset: [],
+      mock: {
+        boardArray: [
+          {
+            label: '低风险',
+            value: '210',
+          },
+          {
+            label: '一般风险',
+            value: '27',
+          },
+          {
+            label: '较高风险',
+            value: '17',
+          },
+          {
+            label: '高风险',
+            value: '1',
+          },
+        ],
+        tableArray: [
+          {
+            index: '执法一处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+          {
+            index: '执法二处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+          {
+            index: '执法三处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+          {
+            index: '执法四处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+          {
+            index: '执法五处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+          {
+            index: '执法六处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+        ],
+      },
+    },
+    showStyle: {
+      size: 'width:440px;height:355px;',
+      version: '原版',
+      position: 'top:10px;right:10px;',
+    },
+  },
+  // 6. 采空区密闭预警分析
+  {
+    deviceType: 'goafMonitoring',
+    moduleName: '采空区密闭预警分析',
+    pageType: 'sealed_goaf',
+    moduleData: {
+      header: {
+        show: false,
+        readFrom: '',
+        selector: {
+          show: true,
+          value: '',
+        },
+        slot: {
+          show: false,
+          value: '',
+        },
+      },
+      background: {
+        show: false,
+        type: 'video',
+        link: '',
+      },
+      layout: {
+        direction: 'column',
+        items: [
+          {
+            name: 'board',
+            basis: '40%',
+            overflow: true,
+          },
+          {
+            name: 'table',
+            basis: '60%',
+            overflow: true,
+          },
+        ],
+      },
+      board: [
+        {
+          type: 'CKQMB1',
+          readFrom: '',
+          layout: 'val-top',
+          items: [
+            {
+              label: '低风险',
+              value: '210',
+            },
+            {
+              label: '一般风险',
+              value: '27',
+            },
+            {
+              label: '较高风险',
+              value: '17',
+            },
+            {
+              label: '高风险',
+              value: '1',
+            },
+          ],
+        },
+      ],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [
+        {
+          type: 'CKQMB',
+          // parser: 'json',
+          readFrom: 'tableArray',
+          columns: [
+            {
+              name: ' ',
+              prop: 'index',
+            },
+            {
+              name: '低风险',
+              prop: 'dfx',
+            },
+            {
+              name: '一般风险',
+              prop: 'ybfx',
+            },
+            {
+              name: '较高风险',
+              prop: 'jgfx',
+            },
+            {
+              name: '高风险',
+              prop: 'gfx',
+            },
+          ],
+        },
+      ],
+      list: [],
+      complex_list: [],
+      preset: [],
+      mock: {
+        boardArray: [
+          {
+            label: '低风险',
+            value: '210',
+          },
+          {
+            label: '一般风险',
+            value: '27',
+          },
+          {
+            label: '较高风险',
+            value: '17',
+          },
+          {
+            label: '高风险',
+            value: '1',
+          },
+        ],
+        tableArray: [
+          {
+            index: '执法一处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+          {
+            index: '执法二处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+          {
+            index: '执法三处',
+            dfx: '50',
+            ybfx: '20',
+            jgfx: '0',
+            gfx: '0',
+          },
+        ],
+      },
+    },
+    showStyle: {
+      size: 'width:440px;height:300px;',
+      version: '原版',
+      position: 'top:365px;right:10px;',
+    },
+  },
+  // 7. 当日报警情况统计
+  {
+    deviceType: 'RealtimeMonitor',
+    moduleName: '当日报警情况统计',
+    pageType: 'sealed_goaf',
+    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: 'CKQMB2',
+          readFrom: '',
+          layout: 'val-top',
+          items: [
+            {
+              label: 'CO超限',
+              value: '210',
+            },
+            {
+              label: 'CH4超限',
+              value: '27',
+            },
+            {
+              label: 'C2H4超限',
+              value: '17',
+            },
+            {
+              label: 'C2H2超限',
+              value: '1',
+            },
+            {
+              label: 'O2超限',
+              value: '210',
+            },
+            {
+              label: '温度超限',
+              value: '27',
+            },
+            {
+              label: '压差超限',
+              value: '17',
+            },
+            {
+              label: '设备断连',
+              value: '1',
+            },
+          ],
+        },
+      ],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      list: [],
+      complex_list: [],
+      preset: [],
+    },
+    showStyle: {
+      size: 'width:440px;height:210px;',
+      version: '原版',
+      position: 'top:660px;right:10px;',
+    },
+  },
+];

+ 138 - 0
src/views/sealedGoafSys/home/sealedGoaf.vue

@@ -0,0 +1,138 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="company-home">
+    <div class="title-select-area">
+      <a-select
+        class="title-select"
+        ref="select"
+        v-model:value="deviceId"
+        @change="handleDeviceChange"
+        popupClassName="drop"
+        :field-names="fieldNames"
+        :options="selectorOptions"
+        :dropdownStyle="{
+          width: '380px',
+          background: 'transparent',
+          borderBottom: '1px solid #ececec66',
+          backdropFilter: 'blur(50px)',
+          color: '#fff',
+        }"
+      />
+    </div>
+    <!-- 渲染所有模块 -->
+    <ModuleCommon
+      v-for="cfg in cfgs"
+      :key="cfg.deviceType + cfg.moduleName"
+      :show-style="cfg.showStyle"
+      :module-data="cfg.moduleData"
+      :module-name="cfg.moduleName"
+      :device-type="cfg.deviceType"
+      :data="data"
+      :visible="true"
+    />
+    <!-- <Three3D :modal-name="modalName" /> -->
+  </div>
+</template>
+<script lang="ts" setup>
+  import { computed, onMounted, onUnmounted, ref } from 'vue';
+  import { useInitConfigs, useInitPage } from '@/views/vent/home/configurable/hooks/useInit';
+  import { testConfigSealedGoaf } from './configurable.data.sealedGoaf';
+  import ModuleCommon from '@/views/vent/home/configurable/components/ModuleCommon.vue';
+  import { useGlobSetting } from '/@/hooks/setting';
+
+  const { title = '省局采空区密闭监测与分析系统' } = useGlobSetting();
+  const { data, updateData, mainTitle } = useInitPage(title);
+
+  const cfgs = computed(() => configs.value);
+  const { configs, fetchConfigs } = useInitConfigs();
+
+  const deviceId = ref('0'); // 当前选中设备ID
+  const fieldNames = { label: 'name', value: 'id' }; // 下拉框字段映射
+  const selectorOptions = ref([
+    { name: '采空区密闭分析', id: '0' },
+    { name: '采空区密闭监测', id: '1' },
+  ]);
+  // 切换设备事件
+  function handleDeviceChange(param) {
+    console.log('切换下拉框选项');
+  }
+  onMounted(() => {
+    fetchConfigs('sealed_goaf').then(() => {
+      configs.value = testConfigSealedGoaf;
+    });
+  });
+
+  // 数据处理函数
+  onUnmounted(() => {});
+</script>
+<style lang="less" scoped>
+  @import '/@/design/theme.less';
+
+  @font-face {
+    font-family: 'douyuFont';
+    src: url('/@/assets/font/douyuFont.otf');
+  }
+  @select-single: ~'@{namespace}-select-single';
+  @select-selector: ~'@{namespace}-select-selector';
+  @selection-item: ~'@{namespace}--select-selection-item';
+  .company-home {
+    --image-module-title: url('/@/assets/images/sealedGoaf/views/home/module-title.png');
+
+    width: 100%;
+    height: 100%;
+    color: @white;
+    position: relative;
+    background: #d7effe;
+    :deep(.vent-box1-bg) {
+      .box1-top {
+        background: unset;
+        height: 45px;
+        padding-top: 10px;
+        .title {
+          height: 25px;
+          background: var(--image-module-title) no-repeat;
+          color: #234e8e;
+          justify-content: left;
+          background-size: cover;
+          font-size: 20px;
+          padding-left: 10px;
+          div {
+            margin-top: -12px;
+          }
+        }
+      }
+      .box-container {
+        background-color: unset;
+        &::before {
+          background-image: none;
+        }
+        &::after {
+          background-image: none;
+        }
+      }
+      .box1-bottom {
+        background: none;
+      }
+    }
+    .title-select {
+      :deep(.@{select-selector}) {
+        width: 300px;
+        background-color: #2b6ff0;
+        box-shadow: none !important;
+        border: 1px solid #b3d8ff;
+        border-radius: 4px; /* 圆角 */
+        padding: 8px 12px; /* 内边距 */
+        font-size: 14px;
+        color: #333; /* 文字颜色 */
+
+        /* 交互样式 */
+        cursor: pointer;
+        width: auto;
+        :deep(.@{selection-item}) {
+          color: #fff !important;
+          font-size: 20px;
+        }
+      }
+    }
+  }
+</style>

+ 299 - 299
src/views/vent/comment/history/HistoryTable.vue

@@ -18,336 +18,336 @@
 </template>
 
 <script lang="ts" setup>
-  // 场景类历史数据公共组件!
-  // 用于服务带子设备的设备历史数据业务,这类数据通常由一条数据返回多个子设备的信息,例如:{ forcFan1Temp, forcFan2Temp };
-  // 而此组件可以将这些数据分类,例如:表格只有 温度 一列,但可以根据所选的子设备展示子设备1的数据:forcFan1Temp;
-  // 综上所述,此组件在基础的历史数据组件上添加了子设备下拉框(由字典驱动),用户选择子设备后表头将动态调整以适配数据展示;
-  //
-  // 使用方法如下:
-  // 设有历史数据2条,[{ name, forcFan1Temp, forcFan2Temp }, { name, forcFan1Temp, forcFan2Temp }]。
-  //
-  // 1、配置设备字段(参考公司端综合设备管理-设备字段管理)
-  // 以压风机为例,设压风机设备的历史数据编码为forcFan_history。
-  // 那么需要把所有字段悉数配置,例子如下:
-  //  显示字段    字段code
-  //   温度     forcFanTemp
-  //  安装位置      name
-  //
-  // 2、配置数据字典(参考系统管理-数据字典),为上述设备配置子设备
-  // 同以压风机为例,设压风机子设备字典编码为forcFan_dict,且已经新增到系统中。
-  // 则字典配置的例子如下:
-  //  名称    数据值
-  // 压风机1 forcFan1
-  // 压风机2 forcFan2
-  //
-  // 3、运维人员应配合前端开发人员,使用指定的编码配置内容。
-  // 同以压风机为例,需使用device-code(forcFan)、dict-code(forcFan_dict)。
-  //
-  // 4、其他内容说明
-  // 同以压风机为例,当子设备没有数据时,不进行过滤,此时展示的数据是:
-  //      温度    安装位置
-  // 取forcFanTemp    取name
-  // 同以压风机为例,当子设备选择压风机1时,过滤压风机1相关的表头,此时展示的数据是:
-  //      温度        安装位置
-  // 取forcFan1Temp    取name
-  // 当设备字段不含数据字典关键词(forcFan)时不做处理,当设备字段包含关键词但已指定编号(即字段包含数字)时不做处理
-  //
-  // 5、历史数据模式选择
-  // 历史数据的请求分为redis分站模式、其他模式,redis分站模式将不支持子设备选择,参考基础的历史数据组件逻辑(本组件已做兼容)
-  // 历史数据查询模式分为多设备查询模式、单设备查询模式,单设备查询模式将不支持设备多选,两种模式本组件都支持
-  import { computed, onMounted, ref, shallowRef } from 'vue';
-  import { BasicColumn, PaginationProps, BasicTable } from '/@/components/Table';
-  import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
-  import { getDefaultSchemas } from './history.data';
-  import { adaptFormData, getDeviceList, getExportUrl, list } from './history.api';
-  import { useListPage } from '/@/hooks/system/useListPage';
-  import { initDictOptions } from '/@/utils/dict';
+// 场景类历史数据公共组件!
+// 用于服务带子设备的设备历史数据业务,这类数据通常由一条数据返回多个子设备的信息,例如:{ forcFan1Temp, forcFan2Temp };
+// 而此组件可以将这些数据分类,例如:表格只有 温度 一列,但可以根据所选的子设备展示子设备1的数据:forcFan1Temp;
+// 综上所述,此组件在基础的历史数据组件上添加了子设备下拉框(由字典驱动),用户选择子设备后表头将动态调整以适配数据展示;
+//
+// 使用方法如下:
+// 设有历史数据2条,[{ name, forcFan1Temp, forcFan2Temp }, { name, forcFan1Temp, forcFan2Temp }]。
+//
+// 1、配置设备字段(参考公司端综合设备管理-设备字段管理)
+// 以压风机为例,设压风机设备的历史数据编码为forcFan_history。
+// 那么需要把所有字段悉数配置,例子如下:
+//  显示字段    字段code
+//   温度     forcFanTemp
+//  安装位置      name
+//
+// 2、配置数据字典(参考系统管理-数据字典),为上述设备配置子设备
+// 同以压风机为例,设压风机子设备字典编码为forcFan_dict,且已经新增到系统中。
+// 则字典配置的例子如下:
+//  名称    数据值
+// 压风机1 forcFan1
+// 压风机2 forcFan2
+//
+// 3、运维人员应配合前端开发人员,使用指定的编码配置内容。
+// 同以压风机为例,需使用device-code(forcFan)、dict-code(forcFan_dict)。
+//
+// 4、其他内容说明
+// 同以压风机为例,当子设备没有数据时,不进行过滤,此时展示的数据是:
+//      温度    安装位置
+// 取forcFanTemp    取name
+// 同以压风机为例,当子设备选择压风机1时,过滤压风机1相关的表头,此时展示的数据是:
+//      温度        安装位置
+// 取forcFan1Temp    取name
+// 当设备字段不含数据字典关键词(forcFan)时不做处理,当设备字段包含关键词但已指定编号(即字段包含数字)时不做处理
+//
+// 5、历史数据模式选择
+// 历史数据的请求分为redis分站模式、其他模式,redis分站模式将不支持子设备选择,参考基础的历史数据组件逻辑(本组件已做兼容)
+// 历史数据查询模式分为多设备查询模式、单设备查询模式,单设备查询模式将不支持设备多选,两种模式本组件都支持
+import { computed, onMounted, ref, shallowRef } from 'vue';
+import { BasicColumn, PaginationProps, BasicTable } from '/@/components/Table';
+import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+import { getDefaultSchemas } from './history.data';
+import { adaptFormData, getDeviceList, getExportUrl, list } from './history.api';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { initDictOptions } from '/@/utils/dict';
 
-  const props = withDefaults(
-    defineProps<{
-      /** 表格项配置,默认由deviceCode获取且联动dictCode,可以覆写,覆写后将不支持联动,参考BaiscTable */
-      // columns?: BasicColumn[];
-      /** 表格操作项配置,默认为空,可以覆写 */
-      // actionColumns?: BasicColumn;
-      /** 查询表单项配置,默认联动dictCode,可以覆写,覆写后将不支持联动,提供formProps时此项无效,参考BaiscTable */
-      // schemas?: FormSchema[];
-      /** 表格分页配置,可以覆写,参考BaiscTable */
-      pagination?: PaginationProps;
-      /** 设备编码,该编码用于请求设备信息,示例:forcFan */
-      deviceCode: string;
-      /** 字典编码,该编码用于从字典配置中读出设备项,示例:forcFan_dict */
-      dictCode: string;
-      /** 字段编码,该编码用于从设备字段配置中读取默认表头信息,示例:forcFan_history */
-      columnsCode: string;
+const props = withDefaults(
+  defineProps<{
+    /** 表格项配置,默认由deviceCode获取且联动dictCode,可以覆写,覆写后将不支持联动,参考BaiscTable */
+    // columns?: BasicColumn[];
+    /** 表格操作项配置,默认为空,可以覆写 */
+    // actionColumns?: BasicColumn;
+    /** 查询表单项配置,默认联动dictCode,可以覆写,覆写后将不支持联动,提供formProps时此项无效,参考BaiscTable */
+    // schemas?: FormSchema[];
+    /** 表格分页配置,可以覆写,参考BaiscTable */
+    pagination?: PaginationProps;
+    /** 设备编码,该编码用于请求设备信息,示例:forcFan */
+    deviceCode: string;
+    /** 字典编码,该编码用于从字典配置中读出设备项,示例:forcFan_dict */
+    dictCode: string;
+    /** 字段编码,该编码用于从设备字段配置中读取默认表头信息,示例:forcFan_history */
+    columnsCode: string;
 
-      scroll: { x: number | true; y: number };
-      /** 表格配置,参考BaiscTable,该值会与默认的配置进行浅合并,这里提供的任何配置都是优先的 */
-      // tableProps?: BasicTableProps;
-      /** 查询表单配置,参考BaiscTable */
-      // formProps?: FormProps;
-    }>(),
-    {
-      deviceCode: '',
-      dictCode: '',
-      pagination: (): PaginationProps => ({
-        current: 1,
-        pageSize: 10,
-        pageSizeOptions: ['10', '30', '50', '100'],
-        showQuickJumper: false,
-      }),
-    }
-  );
+    scroll: { x: number | true; y: number };
+    /** 表格配置,参考BaiscTable,该值会与默认的配置进行浅合并,这里提供的任何配置都是优先的 */
+    // tableProps?: BasicTableProps;
+    /** 查询表单配置,参考BaiscTable */
+    // formProps?: FormProps;
+  }>(),
+  {
+    deviceCode: '',
+    dictCode: '',
+    pagination: (): PaginationProps => ({
+      current: 1,
+      pageSize: 10,
+      pageSizeOptions: ['10', '30', '50', '100'],
+      showQuickJumper: false,
+    }),
+  }
+);
 
-  // 未经过处理的原始表头,即项目配置的表头
-  let originColumns: BasicColumn[] = [];
-  // 表格数据
-  const data = shallowRef([]);
+// 未经过处理的原始表头,即项目配置的表头
+let originColumns: BasicColumn[] = [];
+// 表格数据
+const data = shallowRef([]);
 
-  const { tableContext, onExportXls, onExportXlsPost } = useListPage({
-    tableProps: {
-      columns: [
-        {
-          align: 'center',
-          dataIndex: 'strinstallpos',
-          defaultHidden: false,
-          title: '安装位置',
-          width: 80,
-        },
-      ],
-      formConfig: {
-        labelAlign: 'left',
-        labelWidth: 80,
-        showAdvancedButton: false,
-        showSubmitButton: false,
-        showResetButton: false,
-        actionColOptions: {
-          xxl: 4,
-        },
+const { tableContext, onExportXls, onExportXlsPost } = useListPage({
+  tableProps: {
+    columns: [
+      {
+        align: 'center',
+        dataIndex: 'strinstallpos',
+        defaultHidden: false,
+        title: '安装位置',
+        width: 80,
+      },
+    ],
+    formConfig: {
+      labelAlign: 'left',
+      labelWidth: 80,
+      showAdvancedButton: false,
+      showSubmitButton: false,
+      showResetButton: false,
+      actionColOptions: {
+        xxl: 4,
       },
-      canResize: true,
-      showTableSetting: false,
-      showActionColumn: false,
-      bordered: false,
-      size: 'small',
-      showIndexColumn: true,
-      tableLayout: 'auto',
-      scroll: computed(() => {
-        return { ...props.scroll, y: props.scroll.y - 100 };
-      }),
-      pagination: props.pagination,
-    },
-    exportConfig: {
-      name: '设备历史列表',
-      url: () => getExportUrl(deviceInfo.value),
     },
-  });
-  const [register, { getForm, setLoading, getPaginationRef, setPagination, setColumns }] = tableContext;
+    canResize: true,
+    showTableSetting: false,
+    showActionColumn: false,
+    bordered: false,
+    size: 'small',
+    showIndexColumn: true,
+    tableLayout: 'auto',
+    scroll: computed(() => {
+      return { ...props.scroll, y: props.scroll.y - 100 };
+    }),
+    pagination: props.pagination,
+  },
+  exportConfig: {
+    name: '设备历史列表',
+    url: () => getExportUrl(deviceInfo.value),
+  },
+});
+const [register, { getForm, setLoading, getPaginationRef, setPagination, setColumns }] = tableContext;
 
-  // 已选中的设备信息
-  const deviceInfo = ref<Record<string, unknown>>({});
-  // 设备下拉框选项
-  const deviceOptions = ref<Record<string, unknown>[]>([]);
-  // 子设备下拉框选项
-  const dictOptions = ref<Record<string, unknown>[]>([]);
+// 已选中的设备信息
+const deviceInfo = ref<Record<string, unknown>>({});
+// 设备下拉框选项
+const deviceOptions = ref<Record<string, unknown>[]>([]);
+// 子设备下拉框选项
+const dictOptions = ref<Record<string, unknown>[]>([]);
 
-  /**
-   * 获取设备信息列表、子设备字典,初始化设备可选项、子设备可选项并选中首个设备
-   */
-  async function fetchDevice() {
-    const results = await getDeviceList({ devicetype: props.deviceCode, pageSize: 10000 });
-    const dicts = await initDictOptions(props.dictCode);
+/**
+ * 获取设备信息列表、子设备字典,初始化设备可选项、子设备可选项并选中首个设备
+ */
+async function fetchDevice() {
+  const results = await getDeviceList({ devicetype: props.deviceCode, pageSize: 10000 });
+  const dicts = await initDictOptions(props.dictCode);
 
-    const options = results.map((item) => {
-      return {
-        label: item.strinstallpos,
-        value: item.id || item.deviceID,
-        deviceType: item.strtype || item.deviceType,
-        devicekind: item.devicekind,
-        stationtype: item.stationtype,
-      };
-    });
-    deviceOptions.value = options;
-    dictOptions.value = dicts;
-    onDeviceChangeCallback(null, options[0]);
-  }
+  const options = results.map((item) => {
+    return {
+      label: item.strinstallpos,
+      value: item.id || item.deviceID,
+      deviceType: item.strtype || item.deviceType,
+      devicekind: item.devicekind,
+      stationtype: item.stationtype,
+    };
+  });
+  deviceOptions.value = options;
+  dictOptions.value = dicts;
+  onDeviceChangeCallback(null, options[0]);
+}
 
-  /**
-   * 选择任意设备后的回调,根据设备信息刷新表格、表头及其数据
-   */
-  function onDeviceChangeCallback(__, option) {
-    // 生成所有需要查询的表头编码,例如 forcFan_auto 对应 forcFan_auto_history、forcFan_history 这两个
-    const codes: string[] = [];
-    deviceInfo.value = option;
-    if (deviceInfo.value && deviceInfo.value.deviceType) {
-      const arr = (deviceInfo.value.deviceType as string).split('_');
-      while (arr.length) {
-        codes.push(arr.join('_').concat('_history'));
-        arr.pop();
-      }
+/**
+ * 选择任意设备后的回调,根据设备信息刷新表格、表头及其数据
+ */
+function onDeviceChangeCallback(__, option) {
+  // 生成所有需要查询的表头编码,例如 forcFan_auto 对应 forcFan_auto_history、forcFan_history 这两个
+  const codes: string[] = [];
+  deviceInfo.value = option;
+  if (deviceInfo.value && deviceInfo.value.deviceType) {
+    const arr = (deviceInfo.value.deviceType as string).split('_');
+    while (arr.length) {
+      codes.push(arr.join('_').concat('_history'));
+      arr.pop();
     }
-    // 如此,例如deviceType为forcFan_auto, 则查询表头按 forcFan_auto_history、forcFan_history、columnsCode 为顺序
-    initTable(codes.concat(props.columnsCode));
-    search();
   }
+  // 如此,例如deviceType为forcFan_auto, 则查询表头按 forcFan_auto_history、forcFan_history、columnsCode 为顺序
+  initTable(codes.concat(props.columnsCode));
+  search();
+}
 
-  /**
-   * 初始化表格,该方法将根据参数设定新的表头、表单。
-   *
-   * 需要有设备信息之后再初始化表格。
-   *
-   * @param deviceCodes 获取表头所用的编码,从左到右依次尝试,直到找到第一个有表头信息的为止
-   */
-  function initTable(deviceCodes: string[]) {
-    const defaultSchemas = getDefaultSchemas(dictOptions.value, deviceOptions.value, onDeviceChangeCallback);
-    for (const code of deviceCodes) {
-      const cols = getTableHeaderColumns(code);
-      if (cols.length) {
-        originColumns = cols;
-        break;
-      }
+/**
+ * 初始化表格,该方法将根据参数设定新的表头、表单。
+ *
+ * 需要有设备信息之后再初始化表格。
+ *
+ * @param deviceCodes 获取表头所用的编码,从左到右依次尝试,直到找到第一个有表头信息的为止
+ */
+function initTable(deviceCodes: string[]) {
+  const defaultSchemas = getDefaultSchemas(dictOptions.value, deviceOptions.value, onDeviceChangeCallback);
+  for (const code of deviceCodes) {
+    const cols = getTableHeaderColumns(code);
+    if (cols.length) {
+      originColumns = cols;
+      break;
     }
-    getForm().setProps({
-      schemas: defaultSchemas,
-    });
   }
+  getForm().setProps({
+    schemas: defaultSchemas,
+  });
+}
 
-  /**
-   * 搜索,核心方法
-   *
-   * 该方法获取表单、处理表单数据后尝试获取数据并设置表头
-   */
-  async function search() {
-    if (!deviceInfo.value) return;
-
-    const form = getForm();
-    await form.validate();
-    const formData = form.getFieldsValue();
+/**
+ * 搜索,核心方法
+ *
+ * 该方法获取表单、处理表单数据后尝试获取数据并设置表头
+ */
+async function search() {
+  if (!deviceInfo.value) return;
 
-    setLoading(true);
-    const pagination = getPaginationRef() as PaginationProps;
-    const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
-    const { records, total, current } = await list(deviceCode, deviceInfo.value, formData, pagination).finally(() => {
-      setLoading(false);
-    });
-    setPagination({
-      current,
-      total,
-    });
-    records.forEach((item) => {
-      Object.assign(item, item.readData);
-    });
-    data.value = records;
-    updateColumns(formData.deviceNum);
-  }
+  const form = getForm();
+  await form.validate();
+  const formData = form.getFieldsValue();
 
-  /**
-   * 更新表头,表头默认情况下需要和子设备联动
-   * @param prefix 子设备的值,即为表头取数据时字段的前缀
-   */
-  function updateColumns(prefix?: string) {
-    if (!prefix) return setColumns(originColumns);
-    // 如果有子设备信息,筛选符合规范的表头
-    const cols = originColumns.map((col) => {
-      const dataIndex = col.dataIndex as string;
-      // 获取到子设备编码的前缀及编码,正则例子:forcFan1 => [forcFan1, forcFan, 1]
-      const [_, pfx] = prefix.match(/([A-Za-z]+)([0-9]+)/) || [];
-      // 同时,如若已经在前缀后配置了编号则不要更改
-      const reg = new RegExp(`${pfx}[0-9]`);
-      if (dataIndex.search(reg) !== -1) return col;
-      if (dataIndex.includes(pfx)) {
-        return {
-          ...col,
-          dataIndex: dataIndex.replace(pfx, prefix),
-        };
-      }
-      // 默认直接放行
-      return col;
-    });
-    setColumns(cols);
-  }
+  setLoading(true);
+  const pagination = getPaginationRef() as PaginationProps;
+  const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
+  const { records, total, current } = await list(deviceCode, deviceInfo.value, formData, pagination).finally(() => {
+    setLoading(false);
+  });
+  setPagination({
+    current,
+    total,
+  });
+  records.forEach((item) => {
+    Object.assign(item, item.readData);
+  });
+  data.value = records;
+  updateColumns(formData.deviceNum);
+}
 
-  /** 导出表格内容为excel */
-  async function exportXls() {
-    const form = getForm();
-    await form.validate();
-    const formData = form.getFieldsValue();
-    const pagination = getPaginationRef() as PaginationProps;
-    const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
-    const params = adaptFormData(deviceCode, deviceInfo.value, formData, pagination);
-    if (deviceInfo.value.stationtype === 'redis') {
-      return onExportXlsPost(params);
-    } else {
-      return onExportXls(params);
+/**
+ * 更新表头,表头默认情况下需要和子设备联动
+ * @param prefix 子设备的值,即为表头取数据时字段的前缀
+ */
+function updateColumns(prefix?: string) {
+  if (!prefix) return setColumns(originColumns);
+  // 如果有子设备信息,筛选符合规范的表头
+  const cols = originColumns.map((col) => {
+    const dataIndex = col.dataIndex as string;
+    // 获取到子设备编码的前缀及编码,正则例子:forcFan1 => [forcFan1, forcFan, 1]
+    const [_, pfx] = prefix.match(/([A-Za-z]+)([0-9]+)/) || [];
+    // 同时,如若已经在前缀后配置了编号则不要更改
+    const reg = new RegExp(`${pfx}[0-9]`);
+    if (dataIndex.search(reg) !== -1) return col;
+    if (dataIndex.includes(pfx)) {
+      return {
+        ...col,
+        dataIndex: dataIndex.replace(pfx, prefix),
+      };
     }
+    // 默认直接放行
+    return col;
+  });
+  setColumns(cols);
+}
+
+/** 导出表格内容为excel */
+async function exportXls() {
+  const form = getForm();
+  await form.validate();
+  const formData = form.getFieldsValue();
+  const pagination = getPaginationRef() as PaginationProps;
+  const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
+  const params = adaptFormData(deviceCode, deviceInfo.value, formData, pagination);
+  if (deviceInfo.value.stationtype === 'redis') {
+    return onExportXlsPost(params);
+  } else {
+    return onExportXls(params);
   }
+}
 
-  // watch(
-  //   () => props.deviceCode,
-  //   async () => {
-  //     await fetchDevice();
-  //     onDeviceChangeCallback(null, deviceInfo.value);
-  //   }
-  // );
+// watch(
+//   () => props.deviceCode,
+//   async () => {
+//     await fetchDevice();
+//     onDeviceChangeCallback(null, deviceInfo.value);
+//   }
+// );
 
-  onMounted(async () => {
-    await fetchDevice();
-    onDeviceChangeCallback(null, deviceInfo.value);
-  });
+onMounted(async () => {
+  await fetchDevice();
+  onDeviceChangeCallback(null, deviceInfo.value);
+});
 </script>
 
 <style scoped lang="less">
-  @import '/@/design/theme.less';
+@import '/@/design/theme.less';
 
-  :deep(.@{ventSpace}-table-body) {
-    height: auto !important;
+:deep(.@{ventSpace}-table-body) {
+  height: auto !important;
+}
+:deep(.zxm-picker) {
+  height: 30px !important;
+}
+:deep(.zxm-table) {
+  .zxm-table-title {
+    display: none !important;
   }
-  :deep(.zxm-picker) {
-    height: 30px !important;
-  }
-  :deep(.zxm-table) {
-    .zxm-table-title {
-      display: none !important;
-    }
-  }
-  .history-table {
-    width: 100%;
-    :deep(.jeecg-basic-table-form-container) {
-      .@{ventSpace}-form {
-        padding: 0 !important;
-        border: none !important;
-        margin-bottom: 0 !important;
-        .@{ventSpace}-picker,
-        .@{ventSpace}-select-selector {
-          width: 100% !important;
-          background: #00000017;
-          border: 1px solid #b7b7b7;
-          input,
-          .@{ventSpace}-select-selection-item,
-          .@{ventSpace}-picker-suffix {
-            color: #fff;
-          }
-          .@{ventSpace}-select-selection-placeholder {
-            color: #ffffffaa;
-          }
+}
+.history-table {
+  width: 100%;
+  :deep(.jeecg-basic-table-form-container) {
+    .@{ventSpace}-form {
+      padding: 0 !important;
+      border: none !important;
+      margin-bottom: 0 !important;
+      .@{ventSpace}-picker,
+      .@{ventSpace}-select-selector {
+        width: 100% !important;
+        background: #00000017;
+        border: 1px solid #b7b7b7;
+        input,
+        .@{ventSpace}-select-selection-item,
+        .@{ventSpace}-picker-suffix {
+          color: #fff;
+        }
+        .@{ventSpace}-select-selection-placeholder {
+          color: #ffffffaa;
         }
-      }
-      .@{ventSpace}-table-title {
-        min-height: 0 !important;
       }
     }
-    .pagination-box {
-      display: flex;
-      justify-content: flex-end;
-      align-items: center;
-      .page-num {
-        border: 1px solid #0090d8;
-        padding: 4px 8px;
-        margin-right: 5px;
-        color: #0090d8;
-      }
-      .btn {
-        margin-right: 10px;
-      }
+    .@{ventSpace}-table-title {
+      min-height: 0 !important;
+    }
+  }
+  .pagination-box {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    .page-num {
+      border: 1px solid #0090d8;
+      padding: 4px 8px;
+      margin-right: 5px;
+      color: #0090d8;
+    }
+    .btn {
+      margin-right: 10px;
     }
   }
+}
 </style>

+ 1 - 1
src/views/vent/dataCenter/APICenter/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="safetyList">
-    <customHeader>数据中心-API管理</customHeader>
+    <customHeader>API管理</customHeader>
     <div class="device-manager-box">
       <!-- 分站监测 -->
       <BasicTable @register="registerTable">

+ 496 - 0
src/views/vent/dataCenter/deviceCenter/history/HistoryTable.vue

@@ -0,0 +1,496 @@
+<template>
+  <div class="history-table" v-if="loading">
+    <BasicTable ref="historyTable" @register="registerTable" :data-source="dataSource" :scroll="tableScroll">
+      <template #form-submitBefore>
+        <a-button type="primary" preIcon="ant-design:search-outlined" @click="getDataSource">查询</a-button>
+      </template>
+    </BasicTable>
+  </div>
+</template>
+
+<script lang="ts" setup>
+//ts语法
+import { watchEffect, ref, watch, defineExpose, inject, nextTick, onMounted, computed } from 'vue';
+import { FormSchema } from '/@/components/Form/index';
+import { BasicTable } from '/@/components/Table';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+import { defHttp } from '/@/utils/http/axios';
+import dayjs from 'dayjs';
+import { getAutoScrollContainer } from '/@/utils/common/compUtils';
+import { render } from '/@/utils/common/renderUtils';
+import { useMethods } from '/@/hooks/system/useMethods';
+import { getDeviceList, getHistoryList } from './history.api';
+import { getDictItemsByCode } from '/@/utils/dict';
+import { get } from 'lodash-es';
+
+const globalConfig = inject('globalConfig');
+const props = defineProps({
+  columnsType: {
+    type: String,
+  },
+  columns: {
+    type: Array,
+    // required: true,
+    default: () => [],
+  },
+  deviceType: {
+    type: String,
+    required: true,
+  },
+  deviceListApi: {
+    type: Function,
+  },
+  deviceArr: {
+    type: Array,
+    // required: true,
+    default: () => [],
+  },
+  designScope: {
+    type: String,
+  },
+  sysId: {
+    type: String,
+  },
+  deviceId: {
+    type: String,
+  },
+  scroll: {
+    type: Object,
+    default: { y: 0 },
+  },
+  formSchemas: {
+    type: Array<FormSchema>,
+    default: () => [],
+  },
+  /** 仅展示已绑定设备,选择是则从系统中获取sysId下已绑定设备。仅能查询到已绑定设备的历史数据 */
+  onlyBounedDevices: {
+    type: Boolean,
+    default: false,
+  },
+  showHistoryCurve: {
+    type: Boolean,
+    default: false,
+  },
+});
+const getDeviceListApi = (params) => defHttp.post({ url: '/monitor/device', params });
+const historyTable = ref();
+const loading = ref(false);
+const stationType = ref('plc1');
+const dataSource = ref([]);
+const emit = defineEmits(['change']);
+
+const historyType = ref('');
+const deviceKide = ref('');
+const columns = ref([]);
+let deviceOptions = ref([]);
+const deviceTypeStr = ref('');
+const deviceTypeName = ref('');
+const deviceType = ref('');
+loading.value = true;
+
+const selectedOption = computed<Record<string, any> | undefined>(() => {
+  let idval: string | undefined = getForm()?.getFieldsValue()?.gdeviceids;
+  if (VENT_PARAM.historyIsMultiple && idval) {
+    const arr = idval.split(',');
+    idval = arr[arr.length - 1];
+  }
+  return deviceOptions.value.find((e: any) => {
+    return e.value === idval;
+  });
+});
+
+watch(
+  () => {
+    return props.columnsType;
+  },
+  async (newVal) => {
+    if (!newVal) return;
+    deviceKide.value = newVal;
+    if (historyTable.value) {
+      getForm().resetFields();
+      // getForm().updateSchema();
+      // getForm();
+    }
+    dataSource.value = [];
+    // const column = getTableHeaderColumns(newVal.includes('_history') ? newVal : newVal + '_history');
+    // if (column && column.length < 1) {
+    //   const arr = newVal.split('_');
+    //   console.log('历史记录列表表头------------>', arr[0] + '_monitor');
+    //   columns.value = getTableHeaderColumns(arr[0] + '_history');
+    //   if (columns.value.length < 1) {
+    //     if (historyType.value) {
+    //       columns.value = getTableHeaderColumns(historyType.value + '_history');
+    //     }
+    //   }
+    // } else {
+    //   columns.value = column;
+    // }
+    await getDeviceList();
+    nextTick(() => {
+      getDataSource();
+    });
+
+    if (historyTable.value) reload();
+  },
+  {
+    immediate: true,
+  }
+);
+
+watch(historyType, (type) => {
+  if (!type) return;
+  // if (historyTable.value) getForm().resetFields()
+
+  const column = getTableHeaderColumns(type.includes('_history') ? type : type + '_history');
+  if (column && column.length < 1) {
+    const arr = type.split('_');
+    columns.value = getTableHeaderColumns(arr[0] + '_history');
+  } else {
+    columns.value = column;
+  }
+  setColumns(columns.value);
+});
+
+// 是否显示历史曲线,在devices_shows_history_curve字典里可以配置哪些设备类型需要显示曲线
+// 字典内的字段可以是前缀,例如fanlocal之于fanlocal_normal
+// 安全监控设备需要更多的配置,除去配置safetymonitor,还需要配置哪些安全监控设备需要曲线
+// 因此可以配置例如A1001的dataTypeName代码(可以查看真实数据参考)
+function calcShowCurveValue() {
+  const historyCurveDicts = getDictItemsByCode('devices_shows_history_curve') || [];
+  const findDict = (str) => historyCurveDicts.some(({ value }) => str.startsWith(value));
+
+  if (!props.showHistoryCurve) return false;
+  const dt = props.deviceType; // 依赖项
+
+  if (!findDict(dt)) return false;
+  if (!dt.startsWith('safetymonitor')) return true;
+
+  // 和字典的设备类型匹配后,如果是安全监控设备,需要额外的匹配安全监控类型
+  const dtns = get(selectedOption.value, 'readData.dataTypeName', ''); // 依赖项
+  return findDict(dtns);
+}
+
+const tableScroll = computed(() => {
+  if (props.scroll.y) return { y: props.scroll.y - 100 };
+  return {};
+});
+
+// watch(stationType, (type) => {
+//   if (type) {
+//     nextTick(() => {
+//       getDataSource();
+//     });
+//   }
+// });
+
+watch(
+  () => props.deviceId,
+  async () => {
+    await getForm().setFieldsValue({});
+    await getDeviceList();
+  }
+);
+
+/** 获取可供查询历史数据的设备列表 */
+async function getDeviceList() {
+  // if (props.deviceType.split('_')[1] && props.deviceType.split('_')[1] === 'history') return;
+  let result;
+  let response;
+  if (props.onlyBounedDevices) {
+    response = await getDeviceListApi({
+      systemID: props.sysId,
+      devicetype: 'sys',
+    }).then(({ msgTxt }) => {
+      return { msgTxt: msgTxt.filter((e) => e.type === props.deviceType) };
+    });
+  } else if (props.sysId) {
+    response = await getDeviceListApi({
+      sysId: props.sysId,
+      devicetype: props.deviceType.startsWith('vehicle') ? 'location_normal' : props.deviceType,
+      pageSize: 10000,
+    });
+  } else if (props.deviceListApi) {
+    response = await props.deviceListApi();
+  } else {
+    response = await getDeviceListApi({ devicetype: props.deviceType, pageSize: 10000 });
+  }
+
+  // 处理不同格式的数据
+  if (response['records'] && response['records'].length > 0) {
+    result = response['records'];
+  } else if (response['msgTxt'] && response['msgTxt'][0] && response['msgTxt'][0]['datalist']) {
+    result = response['msgTxt'][0]['datalist'];
+  }
+  if (response['msgTxt'] && response['msgTxt'][0]) {
+    deviceTypeName.value = response['msgTxt'][0]['typeName'];
+    deviceType.value = response['msgTxt'][0]['type'];
+  }
+
+  if (result) {
+    deviceOptions.value = [];
+    deviceOptions.value = result.map((item, index) => {
+      return {
+        label: item['strinstallpos'],
+        value: item['id'] || item['deviceID'],
+        strtype: item['strtype'] || item['deviceType'],
+        strinstallpos: item['strinstallpos'],
+        devicekind: item['devicekind'],
+        stationtype: item['stationtype'],
+        readData: item['readData'],
+      };
+    });
+
+    stationType.value = deviceOptions.value[0]['stationtype'];
+    if (props.deviceType.startsWith('vehicle')) {
+      historyType.value = 'vehicle';
+    } else {
+      historyType.value = deviceOptions.value[0]['strtype'] || deviceOptions.value[0]['devicekind'];
+    }
+  }
+  if (VENT_PARAM.historyIsMultiple) {
+    await getForm().setFieldsValue({
+      gdeviceids: [props.deviceId ? props.deviceId : deviceOptions.value[0] ? deviceOptions.value[0]['value'] : ''],
+    });
+    await getForm().updateSchema({
+      field: 'gdeviceids',
+      componentProps: {
+        mode: 'multiple',
+        maxTagCount: 'responsive',
+      },
+    });
+  } else {
+    await getForm().setFieldsValue({
+      gdeviceids: props.deviceId ? props.deviceId : deviceOptions.value[0] ? deviceOptions.value[0]['value'] : '',
+    });
+    await getForm().updateSchema({
+      field: 'gdeviceids',
+    });
+  }
+}
+
+function resetFormParam() {
+  const formData = getForm().getFieldsValue();
+  const pagination = getPaginationRef();
+  formData['pageNo'] = pagination['current'];
+  formData['pageSize'] = pagination['pageSize'];
+  formData['column'] = 'createTime';
+  const params = {
+    pageNo: pagination['current'],
+    pageSize: pagination['pageSize'],
+    column: pagination['createTime'],
+    ttime_begin: formData['ttime_begin'],
+    ttime_end: formData['ttime_end'],
+    deviceId: formData['gdeviceids'],
+  };
+  return params;
+}
+
+async function getDataSource() {
+  dataSource.value = [];
+  setLoading(true);
+  const params = await resetFormParam();
+  const result = await getHistoryList(params);
+  setPagination({ total: Math.abs(result['datalist']['total']) || 0 });
+  if (result['datalist']['records'].length > 0) {
+    dataSource.value = result['datalist']['records'].map((item: any) => {
+      return Object.assign(item, item['readData']);
+    });
+  } else {
+    dataSource.value = [];
+  }
+  setLoading(false);
+}
+
+// 列表页面公共参数、方法
+const { tableContext, onExportXls, onExportXlsPost } = useListPage({
+  tableProps: {
+    // api: list,
+    columns: props.columnsType ? columns : (props.columns as any[]),
+    canResize: true,
+    showTableSetting: false,
+    showActionColumn: false,
+    bordered: false,
+    size: 'small',
+    showIndexColumn: true,
+    tableLayout: 'auto',
+    formConfig: {
+      labelAlign: 'left',
+      labelWidth: 80,
+      showAdvancedButton: false,
+      showSubmitButton: false,
+      showResetButton: false,
+      baseColProps: {
+        xs: 24,
+        sm: 24,
+        md: 24,
+        lg: 9,
+        xl: 7,
+        xxl: 4,
+      },
+      schemas:
+        props.formSchemas.length > 0
+          ? props.formSchemas
+          : [
+              {
+                field: 'ttime_begin',
+                label: '开始时间',
+                component: 'DatePicker',
+                defaultValue: dayjs().startOf('date'),
+                required: true,
+                componentProps: {
+                  showTime: true,
+                  valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                  getPopupContainer: getAutoScrollContainer,
+                },
+                colProps: {
+                  span: 5,
+                },
+              },
+              {
+                field: 'ttime_end',
+                label: '结束时间',
+                component: 'DatePicker',
+                defaultValue: dayjs(),
+                required: true,
+                componentProps: {
+                  showTime: true,
+                  valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                  getPopupContainer: getAutoScrollContainer,
+                },
+                colProps: {
+                  span: 5,
+                },
+              },
+              {
+                label: '查询设备',
+                field: 'gdeviceids',
+                component: 'Select',
+                required: true,
+                componentProps: {
+                  showSearch: true,
+                  filterOption: (input: string, option: any) => {
+                    return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
+                  },
+                  options: deviceOptions,
+                  onChange: (e, option) => {
+                    if (option && (option['strinstallpos'] || option['strtype'] || option['devicekind'])) {
+                      historyType.value = option['strtype'] || option['devicekind'];
+                    }
+                    if (option['strtype']) {
+                      deviceTypeStr.value = option['strtype'];
+                    }
+                    stationType.value = option['stationtype'];
+                    nextTick(() => {
+                      getDataSource();
+                    });
+                  },
+                },
+                colProps: {
+                  span: 6,
+                },
+              },
+            ],
+      // fieldMapToTime: [['tickectDate', ['ttime_begin', 'ttime_end'], '']],
+    },
+    // fetchSetting: {
+    //   listField: 'datalist',
+    //   totalField: 'datalist.total',
+    // },
+    pagination: {
+      current: 1,
+      pageSize: 20,
+      showQuickJumper: false,
+      showSizeChanger: false,
+    },
+    beforeFetch() {
+      const newParams = { ...resetFormParam() };
+      return newParams;
+    },
+  },
+});
+//注册table数据
+const [registerTable, { reload, setLoading, getForm, setColumns, getPaginationRef, setPagination }] = tableContext;
+watchEffect(() => {
+  if (historyTable.value && dataSource) {
+    const data = dataSource.value || [];
+    emit('change', data);
+  }
+});
+
+onMounted(async () => {
+  await getDeviceList();
+  if (deviceOptions.value[0]) {
+    nextTick(async () => {
+      await getDataSource();
+    });
+  }
+
+  watch([() => getPaginationRef()['current'], () => getPaginationRef()['pageSize']], async () => {
+    if (deviceOptions.value[0]) {
+      if (deviceOptions.value[0]) {
+        await getDataSource();
+      }
+    }
+  });
+});
+defineExpose({ setLoading });
+</script>
+
+<style scoped lang="less">
+@import '/@/design/theme.less';
+
+:deep(.@{ventSpace}-table-body) {
+  height: auto !important;
+}
+:deep(.zxm-picker) {
+  height: 30px !important;
+}
+.history-table {
+  width: 100%;
+  :deep(.jeecg-basic-table-form-container) {
+    .@{ventSpace}-form {
+      padding: 0 !important;
+      border: none !important;
+      margin-bottom: 0 !important;
+      .@{ventSpace}-picker,
+      .@{ventSpace}-select-selector {
+        width: 100% !important;
+        height: 100%;
+        background: #00000017;
+        border: 1px solid #b7b7b7;
+        input,
+        .@{ventSpace}-select-selection-item,
+        .@{ventSpace}-picker-suffix {
+          color: #fff;
+        }
+        .@{ventSpace}-select-selection-placeholder {
+          color: #ffffffaa;
+        }
+      }
+    }
+    .@{ventSpace}-table-title {
+      min-height: 0 !important;
+    }
+  }
+  .pagination-box {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    .page-num {
+      border: 1px solid #0090d8;
+      padding: 4px 8px;
+      margin-right: 5px;
+      color: #0090d8;
+    }
+    .btn {
+      margin-right: 10px;
+    }
+  }
+}
+
+.history-chart {
+  background-color: #0090d822;
+  margin: 0 10px;
+}
+</style>

+ 289 - 0
src/views/vent/dataCenter/deviceCenter/history/HistoryTableFan.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="history-table">
+    <BasicTable ref="historyTable" @register="register" :data-source="data" :scroll="scroll" @change="search">
+      <template #bodyCell="{ column, record }">
+        <a-tag v-if="column.dataIndex === 'warnFlag'" :color="record.warnFlag == '0' ? 'green' : 'red'">
+          {{ record.warnFlag == '0' ? '正常' : '报警' }}
+        </a-tag>
+        <a-tag v-if="column.dataIndex === 'netStatus'" :color="record.netStatus == '0' ? '#f00' : 'green'">
+          {{ record.netStatus == '0' ? '断开' : '连接' }}
+        </a-tag>
+      </template>
+      <template #form-submitBefore>
+        <a-button type="primary" preIcon="ant-design:search-outlined" @click="search">查询</a-button>
+      </template>
+    </BasicTable>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref, shallowRef } from 'vue';
+import { BasicColumn, PaginationProps, BasicTable } from '/@/components/Table';
+import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+import { getDefaultSchemas } from './history.data';
+import { getDeviceList, getHistoryList } from './history.api';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { initDictOptions } from '/@/utils/dict';
+
+const props = withDefaults(
+  defineProps<{
+    /** 表格项配置,默认由deviceCode获取且联动dictCode,可以覆写,覆写后将不支持联动,参考BaiscTable */
+    // columns?: BasicColumn[];
+    /** 表格操作项配置,默认为空,可以覆写 */
+    // actionColumns?: BasicColumn;
+    /** 查询表单项配置,默认联动dictCode,可以覆写,覆写后将不支持联动,提供formProps时此项无效,参考BaiscTable */
+    // schemas?: FormSchema[];
+    /** 表格分页配置,可以覆写,参考BaiscTable */
+    pagination?: PaginationProps;
+    /** 设备编码,该编码用于请求设备信息,示例:forcFan */
+    deviceCode: string;
+    /** 字典编码,该编码用于从字典配置中读出设备项,示例:forcFan_dict */
+    dictCode: string;
+    /** 字段编码,该编码用于从设备字段配置中读取默认表头信息,示例:forcFan_history */
+    columnsCode: string;
+
+    scroll: { x: number | true; y: number };
+    /** 表格配置,参考BaiscTable,该值会与默认的配置进行浅合并,这里提供的任何配置都是优先的 */
+    // tableProps?: BasicTableProps;
+    /** 查询表单配置,参考BaiscTable */
+    // formProps?: FormProps;
+  }>(),
+  {
+    deviceCode: '',
+    dictCode: '',
+    pagination: (): PaginationProps => ({
+      current: 1,
+      pageSize: 20,
+      showQuickJumper: false,
+      showSizeChanger: false,
+    }),
+  }
+);
+
+// 未经过处理的原始表头,即项目配置的表头
+let originColumns: BasicColumn[] = [];
+// 表格数据
+const data = shallowRef([]);
+
+const { tableContext } = useListPage({
+  tableProps: {
+    columns: [
+      {
+        align: 'center',
+        dataIndex: 'strinstallpos',
+        defaultHidden: false,
+        title: '安装位置',
+        width: 80,
+      },
+    ],
+    formConfig: {
+      labelAlign: 'left',
+      labelWidth: 80,
+      showAdvancedButton: false,
+      showSubmitButton: false,
+      showResetButton: false,
+      actionColOptions: {
+        xxl: 4,
+      },
+    },
+    canResize: true,
+    showTableSetting: false,
+    showActionColumn: false,
+    bordered: false,
+    size: 'small',
+    showIndexColumn: true,
+    tableLayout: 'auto',
+    scroll: computed(() => {
+      return { ...props.scroll, y: props.scroll.y - 100 };
+    }),
+    pagination: props.pagination,
+  },
+});
+const [register, { getForm, setLoading, getPaginationRef, setPagination, setColumns }] = tableContext;
+
+// 已选中的设备信息
+const deviceInfo = ref<Record<string, unknown>>({});
+// 设备下拉框选项
+const deviceOptions = ref<Record<string, unknown>[]>([]);
+// 子设备下拉框选项
+const dictOptions = ref<Record<string, unknown>[]>([]);
+
+/**
+ * 获取设备信息列表、子设备字典,初始化设备可选项、子设备可选项并选中首个设备
+ */
+async function fetchDevice() {
+  const results = await getDeviceList({ devicetype: props.deviceCode, pageSize: 10000 });
+  const dicts = await initDictOptions(props.dictCode);
+
+  const options = results.map((item) => {
+    return {
+      label: item.strinstallpos,
+      value: item.id || item.deviceID,
+      deviceType: item.strtype || item.deviceType,
+      devicekind: item.devicekind,
+      stationtype: item.stationtype,
+    };
+  });
+  deviceOptions.value = options;
+  dictOptions.value = dicts;
+  onDeviceChangeCallback(null, options[0]);
+}
+
+/**
+ * 选择任意设备后的回调,根据设备信息刷新表格、表头及其数据
+ */
+function onDeviceChangeCallback(__, option) {
+  // 生成所有需要查询的表头编码,例如 forcFan_auto 对应 forcFan_auto_history、forcFan_history 这两个
+  const codes: string[] = [];
+  deviceInfo.value = option;
+  if (deviceInfo.value && deviceInfo.value.deviceType) {
+    const arr = (deviceInfo.value.deviceType as string).split('_');
+    while (arr.length) {
+      codes.push(arr.join('_').concat('_history'));
+      arr.pop();
+    }
+  }
+  // 如此,例如deviceType为forcFan_auto, 则查询表头按 forcFan_auto_history、forcFan_history、columnsCode 为顺序
+  initTable(codes.concat(props.columnsCode));
+  search();
+}
+
+/**
+ * 初始化表格,该方法将根据参数设定新的表头、表单。
+ *
+ * 需要有设备信息之后再初始化表格。
+ *
+ * @param deviceCodes 获取表头所用的编码,从左到右依次尝试,直到找到第一个有表头信息的为止
+ */
+function initTable(deviceCodes: string[]) {
+  const defaultSchemas = getDefaultSchemas(dictOptions.value, deviceOptions.value, onDeviceChangeCallback);
+  for (const code of deviceCodes) {
+    const cols = getTableHeaderColumns(code);
+    if (cols.length) {
+      originColumns = cols;
+      break;
+    }
+  }
+  getForm().setProps({
+    schemas: defaultSchemas,
+  });
+}
+
+/**
+ * 搜索,核心方法
+ *
+ * 该方法获取表单、处理表单数据后尝试获取数据并设置表头
+ */
+async function search() {
+  if (!deviceInfo.value) return;
+
+  const form = getForm();
+  await form.validate();
+  const formData = form.getFieldsValue();
+  const pagination = getPaginationRef() as PaginationProps;
+  const params = {
+    ...formData,
+    pageNo: pagination.current,
+    pageSize: pagination.pageSize,
+  };
+  setLoading(true);
+  const result = await getHistoryList(params);
+  if (result) {
+    setLoading(false);
+  }
+  data.value = result.datalist['records'].map((item: any) => {
+    setPagination({ total: Math.abs(result['datalist']['total']) || 0 });
+    return Object.assign(item, item['readData']);
+  });
+
+  updateColumns(formData.deviceNum);
+}
+
+/**
+ * 更新表头,表头默认情况下需要和子设备联动
+ * @param prefix 子设备的值,即为表头取数据时字段的前缀
+ */
+function updateColumns(prefix?: string) {
+  if (!prefix) return setColumns(originColumns);
+  // 如果有子设备信息,筛选符合规范的表头
+  const cols = originColumns.map((col) => {
+    const dataIndex = col.dataIndex as string;
+    // 获取到子设备编码的前缀及编码,正则例子:forcFan1 => [forcFan1, forcFan, 1]
+    const [_, pfx] = prefix.match(/([A-Za-z]+)([0-9]+)/) || [];
+    // 同时,如若已经在前缀后配置了编号则不要更改
+    const reg = new RegExp(`${pfx}[0-9]`);
+    if (dataIndex.search(reg) !== -1) return col;
+    if (dataIndex.includes(pfx)) {
+      return {
+        ...col,
+        dataIndex: dataIndex.replace(pfx, prefix),
+      };
+    }
+    // 默认直接放行
+    return col;
+  });
+  setColumns(cols);
+}
+
+onMounted(async () => {
+  await fetchDevice();
+  onDeviceChangeCallback(null, deviceInfo.value);
+});
+</script>
+
+<style scoped lang="less">
+@import '/@/design/theme.less';
+
+:deep(.@{ventSpace}-table-body) {
+  height: auto !important;
+}
+:deep(.zxm-picker) {
+  height: 30px !important;
+}
+:deep(.zxm-table) {
+  .zxm-table-title {
+    display: none !important;
+  }
+}
+.history-table {
+  width: 100%;
+  :deep(.jeecg-basic-table-form-container) {
+    .@{ventSpace}-form {
+      padding: 0 !important;
+      border: none !important;
+      margin-bottom: 0 !important;
+      .@{ventSpace}-picker,
+      .@{ventSpace}-select-selector {
+        width: 100% !important;
+        background: #00000017;
+        border: 1px solid #b7b7b7;
+        input,
+        .@{ventSpace}-select-selection-item,
+        .@{ventSpace}-picker-suffix {
+          color: #fff;
+        }
+        .@{ventSpace}-select-selection-placeholder {
+          color: #ffffffaa;
+        }
+      }
+    }
+    .@{ventSpace}-table-title {
+      min-height: 0 !important;
+    }
+  }
+  .pagination-box {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    .page-num {
+      border: 1px solid #0090d8;
+      padding: 4px 8px;
+      margin-right: 5px;
+      color: #0090d8;
+    }
+    .btn {
+      margin-right: 10px;
+    }
+  }
+}
+</style>

+ 78 - 0
src/views/vent/dataCenter/deviceCenter/history/history.api.ts

@@ -0,0 +1,78 @@
+import { PaginationProps } from '/@/components/Table';
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  getHistory = '/dataCenter/safety/ventanalyMonitorData/getRealHistory',
+  getDeviceList = '/monitor/device',
+}
+/**
+ * 获取列表的接口
+ * @param deviceCode 设备编码,作为 strtype 传参
+ * @param deviceInfo 设备信息,根据分站判别所用的 api
+ * @param formData 表单数据
+ * @param pagination 分页数据
+ * @returns
+ */
+
+const intervalMap = new Map([
+  ['1', '1s'],
+  ['2', '5s'],
+  ['3', '10s'],
+  ['4', '30s'],
+  ['5', '1m'],
+  ['6', '10m'],
+  ['7', '30m'],
+  ['8', '1h'],
+]);
+
+/**
+ * 根据所给设备的分站信息、设备编码等信息生成历史数据/数据导出api所需的请求参数
+ * @param deviceCode
+ * @param deviceInfo
+ * @param formData
+ * @param pagination
+ * @returns
+ */
+export const adaptFormData = (deviceCode: string, deviceInfo: any, formData: any, pagination: PaginationProps) => {
+  if (deviceInfo.stationtype === 'redis') {
+    return {
+      pageNum: pagination.current,
+      pageSize: pagination.pageSize,
+      column: 'createTime',
+      startTime: formData.ttime_begin,
+      endTime: formData.ttime_end,
+      deviceId: formData.gdeviceids,
+      strtype: deviceCode,
+      isEmployee: deviceCode.startsWith('vehicle') ? false : true,
+    };
+  } else {
+    return {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      column: 'createTime',
+      strtype: deviceCode,
+      ...formData,
+    };
+  }
+};
+
+// 获取历史数据
+export const getHistoryList = (params) =>
+  defHttp.get({
+    url: Api.getHistory,
+    params,
+  });
+/**
+ * 根据设备编码获取设备列表
+ * @param params
+ */
+export const getDeviceList = (params) =>
+  defHttp.post({ url: Api.getDeviceList, params }).then((r) => {
+    if (r.records && r.records.length) {
+      return r.records;
+    }
+    if (r.msgTxt && r.msgTxt.length) {
+      return r.msgTxt[0].datalist;
+    }
+    return [];
+  });

+ 86 - 0
src/views/vent/dataCenter/deviceCenter/history/history.data.ts

@@ -0,0 +1,86 @@
+import dayjs from 'dayjs';
+import { FormSchema } from '/@/components/Table';
+import { getAutoScrollContainer } from '/@/utils/common/compUtils';
+import { get } from 'lodash-es';
+
+/**
+ * 默认的查询表单项props
+ *
+ * @param dictOptions 用于初始化子设备下拉框
+ * @param deviceOptions 用于初始化设备下拉框
+ * @param onDeviceChange 设备下拉框选择内容后的回调函数
+ * @returns
+ */
+export const getDefaultSchemas: (dictOptions: any[], deviceOptions: any[], onDeviceChange?: Function) => FormSchema[] = (
+  dictOptions: any[],
+  deviceOptions: any[],
+  onDeviceChange?: Function
+) => {
+  const device = get(deviceOptions, '[0].value', '');
+  const isRedis = get(deviceOptions, '[0].stationtype', 'redis') === 'redis';
+  const dictcode = get(dictOptions, '[0].value', '');
+  return [
+    {
+      field: 'ttime_begin',
+      label: '开始时间',
+      component: 'DatePicker',
+      defaultValue: dayjs().startOf('date'),
+      required: true,
+      componentProps: {
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        getPopupContainer: getAutoScrollContainer,
+      },
+      colProps: {
+        span: 5,
+      },
+    },
+    {
+      field: 'ttime_end',
+      label: '结束时间',
+      component: 'DatePicker',
+      defaultValue: dayjs(),
+      required: true,
+      componentProps: {
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        getPopupContainer: getAutoScrollContainer,
+      },
+      colProps: {
+        span: 5,
+      },
+    },
+    {
+      label: '查询设备',
+      field: 'deviceId',
+      component: 'Select',
+      required: true,
+      defaultValue: VENT_PARAM.historyIsMultiple ? [device] : device,
+      componentProps: {
+        options: deviceOptions,
+        mode: VENT_PARAM.historyIsMultiple ? 'multiple' : undefined,
+        maxTagCount: 'responsive',
+        onChange: onDeviceChange,
+      },
+      colProps: {
+        span: 5,
+      },
+    },
+    {
+      label: '子设备',
+      field: 'deviceNum',
+      component: 'Select',
+      required: isRedis ? false : Boolean(dictOptions.length),
+      show: isRedis ? false : Boolean(dictOptions.length),
+      defaultValue: isRedis ? '' : dictcode,
+      componentProps: {
+        options: dictOptions,
+        // onChange: (e, option) => {
+        // },
+      },
+      colProps: {
+        span: 4,
+      },
+    },
+  ];
+};

+ 95 - 94
src/views/vent/dataCenter/deviceCenter/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="safetyList">
-    <customHeader>数据中心-设备监测</customHeader>
+    <customHeader>设备监测</customHeader>
     <div class="content">
       <a-tabs class="tab-box" v-model:activeKey="activeKey" @change="onChangeTab">
         <a-tab-pane tab="设备监测" key="device" />
@@ -54,7 +54,6 @@
                   <RightCircleTwoTone v-else />
                 </a-button>
               </template>
-
               <!-- 嵌套表格 -->
               <template #expandedRowRender="{ record }">
                 <a-table
@@ -75,11 +74,40 @@
                   </template>
                 </a-table>
               </template>
+              <template #bodyCell="{ column, record }">
+                <template v-if="column.key === 'netStatus'">
+                  <span
+                    :style="{
+                      color: record.netStatus ? '#52c41a' : '#ddd',
+                      fontWeight: '500',
+                    }"
+                  >
+                    {{ record.netStatus ? '在线' : '断开' }}
+                  </span>
+                </template>
+                <template v-else>
+                  {{ record[column.dataIndex] }}
+                </template>
+              </template>
             </a-table>
           </div>
           <div class="right-box" v-else-if="activeKey == 'history'">
-            <div class="right-title">历史数据:</div>
-            <a-table></a-table>
+            <template v-if="deviceType.startsWith('fanmain')">
+              <HistoryTableFan class="w-100% h-100%" :device-code="`${deviceType}`" :scroll="scroll" dict-code="fan_dict" />
+            </template>
+            <template v-else-if="deviceType.startsWith('fanlocal')">
+              <HistoryTableFan class="w-100% h-100%" :device-code="`${deviceType}`" :scroll="scroll" dict-code="fanlocal_dict" />s
+            </template>
+            <template v-else>
+              <HistoryTable
+                ref="historyTable"
+                :sysId="systemID"
+                :columns-type="`${deviceType}`"
+                :device-type="deviceType"
+                designScope="device-history"
+                :scroll="scroll"
+              />
+            </template>
           </div>
         </div>
       </div>
@@ -91,15 +119,17 @@
 import { ref, nextTick, reactive, onMounted, onUnmounted, watch, shallowRef, computed } from 'vue';
 import { usePermission } from '/@/hooks/web/usePermission';
 import customHeader from '/@/components/vent/customHeader.vue';
-import { AesEncryption } from '/@/utils/cipher';
-import { loginCipher } from '/@/settings/encryptionSetting';
 import { message, TreeProps } from 'ant-design-vue';
 import { getDeviceTypeList, getDeviceListByType, getDevMonitorListById } from './device.api';
+import HistoryTableFan from './history/HistoryTableFan.vue';
+import HistoryTable from './history/HistoryTable.vue';
 import { RightCircleTwoTone, DownCircleTwoTone } from '@ant-design/icons-vue';
-import { alignElement } from 'dom-align';
-import { active } from 'sortablejs';
-import { stubTrue } from 'lodash';
+import { useRoute } from 'vue-router';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { BasicTable } from '/@/components/Table';
+import { FormSchema } from '/@/components/Form/index';
 
+let route = useRoute();
 const { hasPermission } = usePermission();
 let activeKey = ref('device');
 const treeData = ref<TreeProps['treeData']>([]);
@@ -118,73 +148,11 @@ const deviceList = ref<any[]>([]); // 设备列表
 const monitorList = ref<any[]>([]); // 监测数据列表
 // 当前展开的行key数组
 const expandedRowKeys = ref([]);
-
+const scroll = reactive({
+  y: 680,
+});
 // 加载状态映射
 const loadingMap = reactive({});
-// 分页参数
-const paginationState = ref({
-  current: 1,
-  pageSize: 10,
-  total: 0,
-});
-const paginationState2 = ref({
-  current: 1,
-  pageSize: 10,
-  total: 0,
-});
-// 计算分页后的数据
-const paginatedData = computed(() => {
-  const start = (paginationState.value.current - 1) * paginationState.value.pageSize;
-  const end = start + paginationState.value.pageSize;
-  return monitorList.value.slice(start, end);
-});
-// 计算分页后的数据
-const paginatedData2 = computed(() => {
-  const start = (paginationState2.value.current - 1) * paginationState2.value.pageSize;
-  const end = start + paginationState2.value.pageSize;
-  return deviceList.value.slice(start, end);
-});
-// 分页器配置 - 修复响应式问题
-// const paginationConfig = computed(() => {
-//   return {
-//     current: paginationState.value.current,
-//     pageSize: paginationState.value.pageSize,
-//     total: monitorList.value.length,
-//     showSizeChanger: true,
-//     showQuickJumper: true,
-//     showTotal: (total) => `共 ${total} 条`,
-//     pageSizeOptions: ['10', '20', '50', '100'],
-//     size: 'small',
-//     onChange: (page, pageSize) => {
-//       paginationState.value.current = page;
-//       paginationState.value.pageSize = pageSize;
-//     },
-//     onShowSizeChange: (current, size) => {
-//       paginationState.value.current = 1;
-//       paginationState.value.pageSize = size;
-//     },
-//   };
-// });
-// const paginationConfig2 = computed(() => {
-//   return {
-//     current: paginationState2.value.current,
-//     pageSize: paginationState2.value.pageSize,
-//     total: deviceList.value.length,
-//     showSizeChanger: true,
-//     showQuickJumper: true,
-//     showTotal: (total) => `共 ${total} 条`,
-//     pageSizeOptions: ['10', '20', '50', '100'],
-//     size: 'small',
-//     onChange: (page, pageSize) => {
-//       paginationState2.value.current = page;
-//       paginationState2.value.pageSize = pageSize;
-//     },
-//     onShowSizeChange: (current, size) => {
-//       paginationState2.value.current = 1;
-//       paginationState2.value.pageSize = size;
-//     },
-//   };
-// });
 // 切换tab页面
 async function onChangeTab(tab) {
   activeKey.value = tab;
@@ -211,10 +179,13 @@ const onSelect: TreeProps['onSelect'] = (keys, e) => {
   getDeviceList(deviceType.value);
 };
 // 获取所有节点key的函数
-const getAllNodeKeys = (nodes) => {
+const getAllNodeKeys = (nodes, type) => {
   const keys = [];
   const traverse = (nodeList) => {
     nodeList.forEach((node) => {
+      if (node.type === type) {
+        selectedKeys.value.push(node.key);
+      }
       keys.push(node.key);
       if (node.children && node.children.length > 0) {
         traverse(node.children);
@@ -226,8 +197,9 @@ const getAllNodeKeys = (nodes) => {
 };
 
 // 获取树形菜单数据
-async function getDeviceType() {
+async function getDeviceType(type?) {
   const result = await getDeviceTypeList({});
+
   if (result.length > 0) {
     const dataSource = [];
     let key = '0';
@@ -247,21 +219,37 @@ async function getDeviceType() {
 
     treeData.value = getData(result, dataSource, key);
     // 数据就绪后设置展开key数组
-    expandedKeys.value = getAllNodeKeys(treeData.value);
+    expandedKeys.value = getAllNodeKeys(treeData.value, type);
   }
 }
+// 获取当前选择节点
 // 根据选择设备获取设备列表
-async function getDeviceList(deviceTypeVal?) {
-  if (!deviceTypeVal) return;
+async function getDeviceList(deviceTypeVal?: any) {
+  // 1. 如果没有设备类型值,停止定时器并返回(不再重复请求)
+  if (!deviceTypeVal) {
+    if (timer) {
+      clearInterval(timer);
+      timer = undefined;
+    }
+    return;
+  }
   if (timer) {
     clearInterval(timer);
     timer = undefined;
   }
-  const params: any = {
-    devKind: deviceTypeVal,
+  const fetchDeviceData = async () => {
+    const params: any = {
+      devKind: deviceTypeVal,
+    };
+    try {
+      const result = await getDeviceListByType(params);
+      deviceList.value = result.records; // 更新设备列表
+    } catch (error) {
+      console.error('定时请求设备列表失败:', error);
+    }
   };
-  const result = await getDeviceListByType(params);
-  deviceList.value = result.records;
+  await fetchDeviceData();
+  timer = setInterval(fetchDeviceData, 2000); // 2000ms = 2秒
 }
 
 // 外层表格列配置
@@ -289,10 +277,6 @@ const outerColumns = [
     dataIndex: 'netStatus',
     key: 'netStatus',
     align: 'center',
-    customRender: ({ text }) => {
-      text = '在线';
-      return `${text}`;
-    },
   },
 ];
 
@@ -370,7 +354,13 @@ async function refreshData(deviceId: string) {
 }
 
 onMounted(() => {
-  getDeviceType();
+  const path = route.query.deviceType;
+  if (path) {
+    getDeviceType(path);
+    getDeviceList(path);
+  } else {
+    getDeviceType();
+  }
 });
 onUnmounted(() => {
   if (timer) {
@@ -379,10 +369,6 @@ onUnmounted(() => {
   }
 });
 // 监听分页变化
-watch(
-  () => [paginationState.value.current, paginationState.value.pageSize],
-  () => {}
-);
 </script>
 
 <style lang="less" scoped>
@@ -458,6 +444,8 @@ watch(
           box-sizing: border-box;
           background: url('/@/assets/images/fire/bj1.png') no-repeat center;
           background-size: 100% 100%;
+          border: 3px, solid, #0b69b6;
+          border-radius: 5px;
         }
 
         .right-box {
@@ -467,6 +455,8 @@ watch(
           box-sizing: border-box;
           background: url('/@/assets/images/fire/bj1.png') no-repeat center;
           background-size: 100% 100%;
+          border: 3px, solid, #0b69b6;
+          border-radius: 5px;
 
           .right-title {
             display: flex;
@@ -557,8 +547,8 @@ watch(
 }
 
 .device-select-box {
-  margin-top: 60px;
-  width: 220px;
+  margin-top: 30px;
+  width: 300px;
   height: calc(100% - 70px);
   overflow-y: auto;
   color: #fff;
@@ -606,6 +596,17 @@ watch(
   pointer-events: auto;
   padding: 20px 10px 30px 10px;
 }
+/* 在线状态 - 绿色 */
+.status-online {
+  color: #36d399; /* 可替换为其他绿色,如 #00C48C */
+  font-weight: 500;
+}
+
+/* 断开状态 - 灰色(可选调整为红色) */
+.status-offline {
+  color: #999;
+  /* 若想断开显示红色:color: #F5222D; */
+}
 </style>
 <style>
 div[aria-hidden='true'] {

+ 20 - 2
src/views/vent/dataCenter/infoCenter/index.vue

@@ -51,7 +51,7 @@
           <template #title> 每日采集数据量 </template>
           <template #container>
             <div class="content-wrapper">
-              <CustomChart :chart-config="dailyNumOption" :chart-data="deviceData" height="245px" />
+              <CustomChart :chart-config="dailyNumOption" :chart-data="deviceData" height="260px" width="100%" />
             </div>
           </template>
         </infoBox>
@@ -91,7 +91,9 @@
   import { sysDataColumn, accessStatusColumn, dailyNumOption } from './infoCenter.data';
   import CustomChart from '@/views/vent/home/configurable/components/detail/CustomChart.vue';
   import { getDeviceAll, getHomepageSummaryIndexes } from './infoCenter.api';
+  import { useRouter } from 'vue-router';
   let mainTitle = ref('智能通风数据中心');
+  let router = useRouter();
   //分页参数配置
   const pagination = reactive({
     current: 1, // 当前页码
@@ -135,7 +137,8 @@
   });
   // 查看数据
   function viewData(record) {
-    console.log(record);
+    console.log(record.devicekind, '设备类型');
+    router.push(`/sjzx/deviceCenter/home?deviceType=${record.devicekind}`);
   }
   //分页切换
   const pageChange = async (val) => {
@@ -217,6 +220,12 @@
         width: 100%;
         overflow: hidden;
         padding-bottom: 20px;
+        .content-wrapper {
+          width: 100% !important; // 强制撑满父容器
+          height: 100% !important;
+          padding: 0 !important; // 抵消默认内边距(如果 infoBox 有内边距,这里需对应调整)
+          box-sizing: border-box; // 避免 padding 挤压宽度
+        }
       }
       .item-1 {
         height: 20%;
@@ -248,6 +257,11 @@
             background-size: 100% 100% !important;
           }
         }
+        .infoBox2 {
+          :deep(.box-container) {
+            padding: 5px 0px 5px 20px !important;
+          }
+        }
       }
       .item-3 {
         height: 40%;
@@ -320,6 +334,10 @@
           font-family: 'douyuFont';
           color: #66ffff;
           margin-top: 20px;
+          max-width: 200px; // 限制最大宽度
+          white-space: normal;
+          word-break: break-all; // 强制长内容换行(包括数字/字母)
+          text-align: center; // 保持文字居中对齐
         }
         .item-icon {
           width: 80px;

+ 35 - 5
src/views/vent/dataCenter/infoCenter/infoCenter.data.ts

@@ -3,6 +3,11 @@ import { ModuleDataChart } from '/@/views/vent/deviceManager/configurationTable/
 
 import { h } from 'vue'; // 引入vue的h函数用于创建VNode
 
+// 导入图片资源(关键步骤)
+import rank1 from '@/assets/images/dataCenter/infoCenter/rank-1.png';
+import rank2 from '@/assets/images/dataCenter/infoCenter/rank-2.png';
+import rank3 from '@/assets/images/dataCenter/infoCenter/rank-3.png';
+import rank4 from '@/assets/images/dataCenter/infoCenter/rank-4.png';
 // 系统数据排名
 export const sysDataColumn: BasicColumn[] = [
   {
@@ -15,13 +20,13 @@ export const sysDataColumn: BasicColumn[] = [
       const numIndex = Number(index);
       let rankImg = '';
       if (numIndex === 0) {
-        rankImg = '/src/assets/images/dataCenter/infoCenter/rank-1.png';
+        rankImg = rank1;
       } else if (numIndex === 1) {
-        rankImg = '/src/assets/images/dataCenter/infoCenter/rank-2.png';
+        rankImg = rank2;
       } else if (numIndex === 2) {
-        rankImg = '/src/assets/images/dataCenter/infoCenter/rank-3.png';
+        rankImg = rank3;
       } else {
-        rankImg = '/src/assets/images/dataCenter/infoCenter/rank-4.png';
+        rankImg = rank4;
       }
       return h(
         'div',
@@ -125,7 +130,32 @@ export const dailyNumOption: ModuleDataChart = {
   type: 'bar',
   readFrom: '',
   legend: { show: false },
-  xAxis: [{ show: true }],
+  xAxis: [
+    {
+      show: true,
+      axisLabel: {
+        rotate: 45, // 在这里配置旋转角度
+        interval: 0,
+      },
+    },
+  ],
   yAxis: [{ show: true, name: '', position: 'left' }],
   series: [{ readFrom: 'collectDataByDayList', xprop: 'day', yprop: 'total_count', label: '' }],
+  dataZoom: [
+    {
+      type: 'slider' as any,
+      show: true,
+      xAxisIndex: 0,
+      height: 12,
+      bottom: 10,
+      start: 0,
+      handleStyle: { color: '#66ffff' },
+      backgroundColor: 'rgba(102, 255, 255, 0.1)', // 滚动条背景色,适配UI
+    },
+    {
+      type: 'inside' as any, // 支持鼠标滚轮缩放柱子
+      xAxisIndex: 0,
+      zoomOnMouseWheel: true,
+    },
+  ] as any[],
 };

+ 32 - 46
src/views/vent/dataCenter/stationCenter/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="safetyList">
-    <customHeader>数据中心-分站监测</customHeader>
+    <customHeader>分站监测</customHeader>
     <div class="content">
       <a-tabs class="tab-box" v-model:activeKey="activeKey" @change="onChangeTab">
         <a-tab-pane tab="分站监测" key="device" />
@@ -11,7 +11,7 @@
           <div class="left-box">
             <div class="left-content">
               <div class="card-box" v-for="(item, index) in cardList" :key="index">
-                <div :class="activeIndex1 === index ? 'card-itemD' : 'card-itemL'" @click="cardClick(item, index)">
+                <div class="card-itemL" :class="{ active: selectedIndex === index }" @click="cardClick(item, index)">
                   <div class="card-item-label">{{ item.strname }}</div>
                 </div>
               </div>
@@ -46,7 +46,21 @@
                   <RightCircleTwoTone v-else />
                 </a-button>
               </template>
-
+              <template #bodyCell="{ column, record }">
+                <template v-if="column.key === 'netStatus'">
+                  <span
+                    :style="{
+                      color: record.netStatus ? '#52c41a' : '#ddd',
+                      fontWeight: '500',
+                    }"
+                  >
+                    {{ record.netStatus ? '在线' : '断开' }}
+                  </span>
+                </template>
+                <template v-else>
+                  {{ record[column.dataIndex] }}
+                </template>
+              </template>
               <!-- 嵌套表格 -->
               <template #expandedRowRender="{ record }">
                 <a-table
@@ -94,6 +108,7 @@ const openNum = ref(0);
 const clsoeNum = ref(0);
 const monitorList = ref<any[]>([]); // 监测数据列表
 const expandedRowKeys = ref([]);
+const selectedIndex = ref(0);
 // // 分页参数
 // const paginationState = ref({
 //   current: 1,
@@ -172,6 +187,7 @@ async function getSubStationList() {
 }
 //菜单选项切换
 function cardClick(item, ind) {
+  selectedIndex.value = selectedIndex.value === ind ? -1 : ind;
   if (timer) {
     clearInterval(timer);
     timer = undefined;
@@ -215,10 +231,6 @@ const outerColumns = [
     dataIndex: 'netStatus',
     key: 'netStatus',
     align: 'center',
-    customRender: ({ text }) => {
-      text = '在线';
-      return `${text}`;
-    },
   },
 ];
 
@@ -384,6 +396,8 @@ onUnmounted(() => {
           box-sizing: border-box;
           background: url('/@/assets/images/fire/bj1.png') no-repeat center;
           background-size: 100% 100%;
+          border: 3px, solid, #0b69b6;
+          border-radius: 5px;
           .left-content {
             display: flex;
             justify-content: flex-start;
@@ -393,30 +407,13 @@ onUnmounted(() => {
 
             .card-box {
               position: relative;
-              width: 182px;
+              width: 178px;
               height: 120px;
+              margin-top: 15px;
               margin-bottom: 30px;
               display: flex;
               justify-content: center;
 
-              .card-itemN {
-                position: relative;
-                width: 85px;
-                height: 120px;
-                background: url('/@/assets/images/zd-2.png') no-repeat center;
-                background-size: 100% 100%;
-                cursor: pointer;
-
-                .card-item-label {
-                  width: 100%;
-                  position: absolute;
-                  bottom: 5px;
-                  font-size: 12px;
-                  color: #fff;
-                  text-align: center;
-                }
-              }
-
               .card-itemL {
                 position: relative;
                 width: 85px;
@@ -424,7 +421,8 @@ onUnmounted(() => {
                 background: url('/@/assets/images/zd-3.png') no-repeat center;
                 background-size: 100% 100%;
                 cursor: pointer;
-
+                border: 1px solid transparent;
+                transition: border-color 0.2s ease;
                 .card-item-label {
                   width: 100%;
                   position: absolute;
@@ -434,25 +432,12 @@ onUnmounted(() => {
                   text-align: center;
                 }
               }
-
-              .card-itemD {
-                position: relative;
-                width: 85px;
-                height: 120px;
-                background: url('/@/assets/images/zd-1.png') no-repeat center;
-                background-size: 100% 100%;
-                cursor: pointer;
-
-                .card-item-label {
-                  width: 100%;
-                  position: absolute;
-                  bottom: 5px;
-                  font-size: 12px;
-                  color: #fff;
-                  text-align: center;
-                }
+              /* 选中状态的高亮边框 */
+              .card-itemL.active {
+                border-color: #0b69b6;
+                border-radius: 5px;
+                box-shadow: 0 0 3px 3px rgb(0, 128, 255);
               }
-
               .card-modal {
                 width: 86px;
                 position: absolute;
@@ -483,7 +468,8 @@ onUnmounted(() => {
           box-sizing: border-box;
           background: url('/@/assets/images/fire/bj1.png') no-repeat center;
           background-size: 100% 100%;
-
+          border: 3px, solid, #0b69b6;
+          border-radius: 5px;
           .right-title {
             display: flex;
             height: 30px;

+ 257 - 0
src/views/vent/dataCenter/statsCenter/index.vue

@@ -0,0 +1,257 @@
+<template>
+  <div class="safetyList">
+    <customHeader>统计监测</customHeader>
+    <div class="content">
+      <div class="box-content">
+        <a-table size="small" :columns="outerColumns" :data-source="deviceList" :pagination="true">
+          <template #bodyCell="{ column, record }">
+            <template v-if="column.dataIndex === 'action'">
+              <a class="action-link" @click="openNestedTable(record)">详情</a>
+            </template>
+          </template>
+        </a-table>
+      </div>
+    </div>
+    <a-modal width="80%" :footer="null" title="统计详情" centered v-model:visible="showDetail">
+      <a-table size="small" :columns="innerColumns" :data-source="monitorList" :pagination="true"></a-table>
+    </a-modal>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, nextTick, reactive, onMounted, onUnmounted, watch, shallowRef, computed } from 'vue';
+import { getInvokeList, getInvokeDetailList } from './stats.api.ts';
+import customHeader from '/@/components/vent/customHeader.vue';
+const deviceList = ref<any[]>([]); // 列表数据
+const monitorList = ref<any[]>([]); // 详情数据
+const showDetail = ref(false);
+// 分页参数
+const paginationState = ref({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+});
+const paginationState2 = ref({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+});
+// 计算分页后的数据
+const paginatedData = computed(() => {
+  const start = (paginationState.value.current - 1) * paginationState.value.pageSize;
+  const end = start + paginationState.value.pageSize;
+  return monitorList.value.slice(start, end);
+});
+// 计算分页后的数据
+const paginatedData2 = computed(() => {
+  const start = (paginationState2.value.current - 1) * paginationState2.value.pageSize;
+  const end = start + paginationState2.value.pageSize;
+  return deviceList.value.slice(start, end);
+});
+// 分页器配置 - 修复响应式问题
+const paginationConfig = computed(() => {
+  return {
+    current: paginationState.value.current,
+    pageSize: paginationState.value.pageSize,
+    total: monitorList.value.length,
+    showSizeChanger: true,
+    showQuickJumper: true,
+    showTotal: (total) => `共 ${total} 条`,
+    pageSizeOptions: ['10', '20', '50', '100'],
+    size: 'small',
+    onChange: (page, pageSize) => {
+      paginationState.value.current = page;
+      paginationState.value.pageSize = pageSize;
+    },
+    onShowSizeChange: (current, size) => {
+      paginationState.value.current = 1;
+      paginationState.value.pageSize = size;
+    },
+  };
+});
+const paginationConfig2 = computed(() => {
+  return {
+    current: paginationState2.value.current,
+    pageSize: paginationState2.value.pageSize,
+    total: deviceList.value.length,
+    showSizeChanger: true,
+    showQuickJumper: true,
+    showTotal: (total) => `共 ${total} 条`,
+    pageSizeOptions: ['10', '20', '50', '100'],
+    size: 'small',
+    onChange: (page, pageSize) => {
+      paginationState2.value.current = page;
+      paginationState2.value.pageSize = pageSize;
+    },
+    onShowSizeChange: (current, size) => {
+      paginationState2.value.current = 1;
+      paginationState2.value.pageSize = size;
+    },
+  };
+});
+// 外层表格列配置
+const outerColumns = [
+  {
+    title: '账户',
+    dataIndex: 'userid',
+    key: 'userid',
+    align: 'center',
+  },
+  {
+    title: '账号名称',
+    dataIndex: 'username',
+    key: 'username',
+    align: 'center',
+  },
+  {
+    title: '访问次数',
+    dataIndex: 'invoke_count',
+    key: 'invoke_count',
+    align: 'center',
+  },
+  {
+    title: '最新访问时间',
+    dataIndex: 'lastTime',
+    key: 'lastTime',
+    align: 'center',
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    width: 80,
+    align: 'center',
+  },
+];
+
+// 内层表格列配置
+const innerColumns = [
+  {
+    title: '账号',
+    dataIndex: 'userid',
+    key: 'userid',
+    align: 'center',
+  },
+  {
+    title: '账号名称',
+    dataIndex: 'username',
+    key: 'username',
+    align: 'center',
+  },
+  {
+    title: '请求路径',
+    dataIndex: 'requestUrl',
+    key: 'requestUrl',
+    align: 'center',
+  },
+  {
+    title: '访问ip',
+    dataIndex: 'ip',
+    key: 'ip',
+    align: 'center',
+  },
+  {
+    title: '请求时间',
+    dataIndex: 'createTime',
+    key: 'createTime',
+    align: 'center',
+  },
+];
+const openNestedTable = (record) => {
+  showDetail.value = !showDetail.value;
+  refreshData(record.userid);
+};
+
+async function getTableData(params?) {
+  if (!params) {
+    const params = {
+      pageNo: 1,
+      pageSize: 20,
+      logType: 9,
+    };
+    const result = await getInvokeList(params);
+    deviceList.value = result.records;
+  } else {
+    const result = await getInvokeList(params);
+    deviceList.value = result.records;
+  }
+}
+async function refreshData(userid: string) {
+  // 这里实现具体的请求逻辑
+  const params = {
+    pageNo: 1,
+    pageSize: 20,
+    userid: userid,
+  };
+  const result = await getInvokeDetailList(params);
+  monitorList.value = Object.values(result.records);
+}
+onMounted(() => {
+  getTableData();
+});
+onUnmounted(() => {});
+// 监听分页变化
+watch(
+  () => [paginationState.value.current, paginationState.value.pageSize],
+  () => {}
+);
+</script>
+
+<style lang="less" scoped>
+.safetyList {
+  width: calc(100% - 20px);
+  height: calc(100% - 80px);
+  position: relative;
+  margin: 50px 10px 10px 10px;
+
+  .content {
+    position: relative;
+    width: 100%;
+    height: 100%;
+
+    .box-content {
+      height: calc(100% - 50px);
+      padding-top: 10px;
+      box-sizing: border-box;
+    }
+  }
+}
+::v-deep(.zxm-radio-wrapper) {
+  font-size: 12px;
+}
+
+::v-deep(.zxm-input) {
+  font-size: 12px;
+}
+
+::v-deep(.zxm-select:not(.zxm-select-customize-input) .zxm-select-selector) {
+  border: 1px solid #3ad8ff77 !important;
+}
+
+// ::v-deep(.zxm-select-selection-item) {
+//   color: #fff ;
+// }
+
+// ::v-deep(.zxm-form-item-label > label) {
+//   color: #fff !important;
+// }
+
+/* 嵌套表格样式 */
+:deep(.ant-table-expanded-row) > td {
+  background-color: #f9f9f9 !important;
+  padding: 0 !important;
+}
+
+:deep(.ant-table-expanded-row .ant-table) {
+  margin: -10px -8px;
+  background: #f9f9f9;
+}
+/* 自定义展开按钮 */
+:deep(.ant-table-row-expand-icon) {
+  margin-right: 8px;
+}
+</style>
+<style>
+div[aria-hidden='true'] {
+  display: none !important;
+}
+</style>

+ 18 - 0
src/views/vent/dataCenter/statsCenter/stats.api.ts

@@ -0,0 +1,18 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  invokeList = '/dataCenter/sys/log/invokeList',
+  invokeDetailList = '/dataCenter/sys/log/invokeDetailList',
+}
+//统计列表接口
+export const getInvokeList = (params) =>
+  defHttp.get({
+    url: Api.invokeList,
+    params,
+  });
+//统计详情接口
+export const getInvokeDetailList = (params) =>
+  defHttp.get({
+    url: Api.invokeDetailList,
+    params,
+  });

+ 11 - 2
src/views/vent/deviceManager/configurationTable/types.ts

@@ -179,7 +179,11 @@ export interface ModuleDataBoard extends ReadFrom {
     | 'R'
     | 'S'
     | 'T'
-    | 'U';
+    | 'U'
+    | 'CKQMB'
+    | 'CKQMB0'
+    | 'CKQMB1'
+    | 'CKQMB2';
   /** 展示牌布局,决定是 label 部分在上方或是 value 在上方 */
   layout: 'val-top' | 'label-top' | 'new-top' | 'new1-top' | 'new2-top' | 'new3-top';
   /** 核心配置,每个展示牌对应一项 */
@@ -291,6 +295,11 @@ export interface ModuleDataChart extends ReadFrom {
   xAxis: {
     show: boolean;
     name?: string;
+    axisLabel?: {
+      rotate?: number;
+      interval?: number;
+      [key: string]: any; // 允许其他 ECharts 支持的属性
+    };
   }[];
   /** 图表y轴配置(若有),支持多个,注意至少一个 */
   yAxis: {
@@ -334,7 +343,7 @@ export interface ModuleDataChart extends ReadFrom {
 
 export interface ModuleDataTable extends ReadFrom {
   /** 表格的预设样式 */
-  type: 'A' | 'B' | 'C' | 'D';
+  type: 'A' | 'B' | 'C' | 'D' | 'CKQMB';
   /** 是否自动滚动 */
   autoScroll?: boolean;
   /** 核心配置,每个表格列对应一项 */

+ 4 - 0
src/views/vent/home/configurable/components/detail/CustomChart.vue

@@ -504,6 +504,8 @@ const genChartOption = () => {
       grid: {
         top: 50,
         bottom: dataZoom.length ? 70 : 30,
+        left: 30, // 缩小左侧边距,增加图表宽度
+        right: 30, // 缩小右侧边距
       },
       legend: {
         textStyle,
@@ -516,11 +518,13 @@ const genChartOption = () => {
       xAxis: xAxis.map((e) => {
         return {
           ...e,
+          dataZoom: baseDataZoom,
           type: 'category',
           axisLabel: {
             interval: 0,
             width: baseDataZoom.length ? 100 : 800 / get(baseSeries, '[0].data.length', 1),
             overflow: 'break',
+            rotate: get(e, 'axisLabel.rotate', 0), // 读取配置表中的rotate值,默认0
           },
         };
       }),

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

@@ -85,6 +85,7 @@
     --image-list-head: url('/@/assets/images/home-container/configurable/firehome/list-head.png');
     --image-content-label-d: url(/@/assets/images/home-container/configurable/minehome/content-label.png);
     --image-content-body-d: url('/@/assets/images/home-container/configurable/minehome/content-body.png');
+    --image-content-label-ckqmb: url('/@/assets/images/sealedGoaf/configurable/table/table1-label.png');
     height: 100%;
     box-sizing: border-box;
     display: flex;
@@ -196,5 +197,41 @@
         background: #092b3a;
       }
     }
+    .table__content_label_CKQMB {
+      background-image: var(--image-content-label-ckqmb);
+      background-size: 100% 100%;
+      background-repeat: no-repeat;
+      color: #000000;
+    }
+    .table__content_list_CKQMB {
+      font-weight: bold;
+      font-size: 18px;
+    }
+    /* 第一个子元素:黑色 */
+    .table__content_list_row .table__content__list_item_CKQMB:nth-child(1) {
+      font-size: 12px;
+      text-align: left;
+      color: #000000;
+    }
+
+    /* 第二个子元素:蓝色 */
+    .table__content_list_row .table__content__list_item_CKQMB:nth-child(2) {
+      color: #1890ff;
+    }
+
+    /* 第三个子元素:黄色 */
+    .table__content_list_row .table__content__list_item_CKQMB:nth-child(3) {
+      color: #ffc107;
+    }
+
+    /* 第四个子元素:橙色 */
+    .table__content_list_row .table__content__list_item_CKQMB:nth-child(4) {
+      color: #ff7a45;
+    }
+
+    /* 第五个子元素:红色 */
+    .table__content_list_row .table__content__list_item_CKQMB:nth-child(5) {
+      color: #ff4d4f;
+    }
   }
 </style>

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

@@ -216,6 +216,13 @@
     --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-board-bg-ckqmb-1: url('/@/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-1.png');
+    --image-board-bg-ckqmb-2: url('/@/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-2.png');
+    --image-board-bg-ckqmb-3: url('/@/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-3.png');
+    --image-board-bg-ckqmb-4: url('/@/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-4.png');
+    --image-board-bg-ckqmb-5: url('/@/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb-5.png');
+    --image-board-bg-ckqmb1: url('/@/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb1.png');
+    --image-board-bg-ckqmb2: url('/@/assets/images/sealedGoaf/configurable/miniBoard/board-bg-ckqmb2.png');
 
     --image-hycd: url(/@/assets/images/home-container/configurable/dusthome/hycd.png);
     --image-dyfl: url(/@/assets/images/home-container/configurable/dusthome/dyfl.png);
@@ -477,6 +484,148 @@
     background-image: var(--image-board-bg-o);
     background-size: 100% 100%;
   }
+  .mini-board_CKQMB {
+    width: 220px;
+    height: 50px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 0 20px 0 20px;
+    background-image: var(--image-board-bg-ckqmb-1);
+    background-size: 100% 100%;
+  }
+  .mini-board_CKQMB:nth-child(2) {
+    background-image: var(--image-board-bg-ckqmb-2);
+  }
+  .mini-board_CKQMB:nth-child(3) {
+    background-image: var(--image-board-bg-ckqmb-3);
+  }
+  .mini-board_CKQMB:nth-child(4) {
+    background-image: var(--image-board-bg-ckqmb-4);
+  }
+  .mini-board_CKQMB:nth-child(5) {
+    background-image: var(--image-board-bg-ckqmb-5);
+  }
+  .mini-board_CKQMB:nth-child(6) {
+    background-image: var(--image-board-bg-ckqmb-1);
+  }
+  .mini-board__label_CKQMB {
+    color: #000000;
+    font-size: 13px;
+    font-family: 'Microsoft YaHei';
+    font-weight: bold;
+    padding-bottom: 6px;
+  }
+  .mini-board__value_CKQMB {
+    width: 55px;
+    color: #000000;
+    font-size: 25px;
+    padding-bottom: 13px;
+    font-family: 'Microsoft YaHei';
+  }
+  .mini-board_CKQMB0 {
+    width: 100px;
+    height: 90px;
+    background-image: var(--image-board-bg-ckqmb1);
+    background-size: 100% 100%;
+    padding-top: 28px;
+  }
+
+  .mini-board__label_CKQMB0 {
+    color: #000000;
+    font-size: 14px;
+    font-family: 'Microsoft YaHei';
+  }
+  .mini-board__value_CKQMB0 {
+    color: #000000;
+    font-size: 22px;
+    font-family: 'Microsoft YaHei';
+    padding-bottom: 10px;
+  }
+  /* 1. 第一个容器:label和value为蓝色 */
+  .mini-board_CKQMB0:nth-child(1) .mini-board__label_CKQMB0,
+  .mini-board_CKQMB0:nth-child(1) .mini-board__value_CKQMB0 {
+    color: #1890ff;
+  }
+
+  /* 2. 第二个容器:label和value为黄色 */
+  .mini-board_CKQMB0:nth-child(2) .mini-board__label_CKQMB0,
+  .mini-board_CKQMB0:nth-child(2) .mini-board__value_CKQMB0 {
+    color: #36a64d;
+  }
+
+  /* 3. 第三个容器:label和value为橙色 */
+  .mini-board_CKQMB0:nth-child(3) .mini-board__label_CKQMB0,
+  .mini-board_CKQMB0:nth-child(3) .mini-board__value_CKQMB0 {
+    color: #a97523;
+  }
+
+  /* 4. 第四个容器:label和value为红色 */
+  .mini-board_CKQMB0:nth-child(4) .mini-board__label_CKQMB0,
+  .mini-board_CKQMB0:nth-child(4) .mini-board__value_CKQMB0 {
+    color: #d6666c;
+  }
+  .mini-board_CKQMB1 {
+    width: 100px;
+    height: 90px;
+    background-image: var(--image-board-bg-ckqmb1);
+    background-size: 100% 100%;
+    padding-top: 28px;
+  }
+
+  .mini-board__label_CKQMB1 {
+    color: #000000;
+    font-size: 14px;
+    font-family: 'Microsoft YaHei';
+  }
+  .mini-board__value_CKQMB1 {
+    color: #000000;
+    font-size: 22px;
+    font-family: 'Microsoft YaHei';
+    padding-bottom: 10px;
+  }
+  /* 1. 第一个容器:label和value为蓝色 */
+  .mini-board_CKQMB1:nth-child(1) .mini-board__label_CKQMB1,
+  .mini-board_CKQMB1:nth-child(1) .mini-board__value_CKQMB1 {
+    color: #1890ff;
+  }
+
+  /* 2. 第二个容器:label和value为黄色 */
+  .mini-board_CKQMB1:nth-child(2) .mini-board__label_CKQMB1,
+  .mini-board_CKQMB1:nth-child(2) .mini-board__value_CKQMB1 {
+    color: #ffc107;
+  }
+
+  /* 3. 第三个容器:label和value为橙色 */
+  .mini-board_CKQMB1:nth-child(3) .mini-board__label_CKQMB1,
+  .mini-board_CKQMB1:nth-child(3) .mini-board__value_CKQMB1 {
+    color: #ff7a45;
+  }
+
+  /* 4. 第四个容器:label和value为红色 */
+  .mini-board_CKQMB1:nth-child(4) .mini-board__label_CKQMB1,
+  .mini-board_CKQMB1:nth-child(4) .mini-board__value_CKQMB1 {
+    color: #ff4d4f;
+  }
+  .mini-board_CKQMB2 {
+    width: 96px;
+    height: 71px;
+    background-image: var(--image-board-bg-ckqmb2);
+    background-size: 100% 100%;
+    padding-top: 8px;
+  }
+
+  .mini-board__label_CKQMB2 {
+    color: #000000;
+    font-size: 14px;
+    font-family: 'Microsoft YaHei';
+  }
+  .mini-board__value_CKQMB2 {
+    color: #2b6ff0;
+    font-size: 22px;
+    font-family: 'Microsoft YaHei';
+    padding-bottom: 6px;
+  }
 
   .mini-board_P {
     width: 97px;

+ 12 - 6
src/views/vent/monitorManager/airDoor/airdoor.api.ts

@@ -7,11 +7,15 @@ enum Api {
   getDevice = '/monitor/device',
   getCameraUrl = '/monitor/camera/queryByCameraCode',
   devicecontrol = '/safety/ventanalyMonitorData/devicecontrol_ssl',
-  insertSyncRule = '/ventanaly-device/synccontrol/upcoming/saveOrUpdateRule',
+  //insertSyncRule = '/ventanaly-device/synccontrol/upcoming/saveOrUpdateRule',
+  insertSyncRule = '/ventanaly-device/monitor/timeSync/insertSyncRule',
   upcoming = '/ventanaly-device/synccontrol/upcoming',
   GetSyncRule='/ventanaly-device/synccontrol/upcoming/GetSyncRule',
   GetSyncRuleOperationLog='/ventanaly-device/synccontrol/upcoming/GetSyncRuleOperationLog',
-  GetSyncRuleExecLog='/ventanaly-device/synccontrol/upcoming/GetSyncRuleExecLog'
+  GetSyncRuleExecLog='/ventanaly-device/synccontrol/upcoming/GetSyncRuleExecLog',
+  list='/safety/ventanalyDeviceInfo/list',
+  edit='/ventanaly-device/safety/ventanalyDeviceInfo/batchEdit',
+  manualTimeSync='/ventanaly-device/monitor/timeSync/manualTimeSync'
 }
 
 
@@ -45,9 +49,6 @@ export const GetSyncRule = (params) => defHttp.post({ url: Api.GetSyncRule, para
 * @param params
 */
 export const insertSyncRule = (params) => {
-  // 加密password
-  //const encryption = new AesEncryption({ key: loginCipher.key, iv: loginCipher.iv });
-  //params.password = encryption.encryptByAES(params.password);
   return defHttp.post({ url: Api.insertSyncRule, params });
 };
 
@@ -59,4 +60,9 @@ export const GetSyncRuleOperationLog = (params) => defHttp.get({ url: Api.GetSyn
 
 //读取集控风门操作日志
 export const GetSyncRuleExecLog = (params) => defHttp.get({ url: Api.GetSyncRuleExecLog, params });
-
+//集控风门列表
+export const controlList = (params) => defHttp.get({ url: Api.list, params });
+//集控风门-确认选中
+export const confirmChoice = (params) => defHttp.put({ url: Api.edit, params });
+//同步PLC时钟
+export const manualTimeSync = () => defHttp.post({ url: Api.manualTimeSync });

+ 6 - 0
src/views/vent/monitorManager/airDoor/airdoor.data.ts

@@ -229,6 +229,12 @@ export let controlOptionList = [
   { label: '自动', value: '自动' },
 ]
 
+//门的同步开关状态
+export let doorStatus={
+  openOrclose:false,
+  statusCode:''
+}
+
 
 
 

+ 86 - 0
src/views/vent/monitorManager/airDoor/components/deviceControl.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="device-control">
+    <div class="control-container">
+      <a-checkbox-group v-model:value="deviceChoiceList" class="check-box">
+        <a-row v-for="(item, index) in deviceControlData" :key="index" class="row-box">
+          <a-col :span="24">
+            <a-checkbox :value="item.id">{{ item.strinstallpos }}</a-checkbox>
+          </a-col>
+        </a-row>
+      </a-checkbox-group>
+      <a-button class="device-control-btn" type="primary" @click="confirmChoice">确定</a-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+
+let props = defineProps({
+  deviceControlData: {
+    type: Array,
+    default: () => {
+      return []
+    }
+  }
+})
+const deviceChoiceList = ref([]);
+const $emit = defineEmits(['confirmChoice'])
+
+function confirmChoice() {
+  $emit('confirmChoice', deviceChoiceList.value)
+}
+
+</script>
+
+<style lang="less" scoped>
+@import '/@/design/theme.less';
+
+@{theme-deepblue} {
+  .door-content-r {
+    --image-left-bd: url('@/assets/images/themify/deepblue/home-container/configurable/wind-door/left-bd.png');
+  }
+}
+
+.device-control {
+  --image-left-bd: url('@/assets/images/home-container/configurable/wind-door/left-bd.png');
+  width: 200px;
+  height: 280px;
+  background-image: var(--image-left-bd);
+  background-size: 100% 100%;
+  position: absolute;
+  right: 35px;
+  top: 70px;
+
+  .control-container {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: calc(100% - 10px);
+    height: calc(100% - 10px);
+    margin: 4px;
+    padding: 5px 8px;
+    box-sizing: border-box;
+    background-color: rgba(54, 198, 254, .5);
+  }
+
+  .check-box {
+    width: 100%;
+    height: 230px;
+    overflow: hidden;
+    overflow-y: auto;
+  }
+
+  .row-box {
+    margin: 10px 0px;
+  }
+
+  .zxm-checkbox-wrapper {
+    color: #fff;
+  }
+  .device-control-btn{
+    position: absolute;
+    bottom: 5px;
+  }
+}
+</style>

+ 133 - 26
src/views/vent/monitorManager/airDoor/components/door-content-r.vue

@@ -14,6 +14,8 @@
         定时设置
       </a-button>
       <a-button style="margin: 0px 10px" type="primary" @click="handlerOperation">操作日志</a-button>
+      <a-button type="primary" @click="handlerClock">同步PLC时钟</a-button>
+      <a-button class="device-control-btn" type="primary" @click="handlerDeviceChoice">集控设备选择</a-button>
     </div>
     <div class="content-r-container">
       <div class="content-r-box" v-for="(item, index) in infoDatas" :key="index">
@@ -22,16 +24,42 @@
           <div class="title-detail" @click="handlerDetail(item.deviceID)">详情</div>
         </div>
         <div class="box-content">
+          <!-- plc时钟显示 -->
+          <!-- <div class="timePlcNow">2025-12-4 13:23:24</div> -->
+          <div class="timePlcNow">{{ item.readData.PLCyear ?
+            `20${item.readData.PLCyear}-${item.readData.PLCmonth}-${item.readData.PLCday}
+            ${item.readData.PLChour}:${item.readData.PLCminute}:${item.readData.PLCsecond}` : '' }}</div>
           <!-- 二三维信息 -->
-          <gateSVG v-if="item.gateStyle == 'fm_fc_ssl'" :ref="(el) => setChildRef(el, index)" :identify="index"></gateSVG>
-           <gateDualSVG v-else :ref="(el) => setChildRef(el, index)" :identify="index">
+          <gateSVG v-if="item.gateStyle == 'fm_fc_ssl'" :ref="(el) => setChildRef(el, index)" :identify="index">
+          </gateSVG>
+          <gateDualSVG v-else :ref="(el) => setChildRef(el, index)" :identify="index">
           </gateDualSVG>
         </div>
         <img src="@/assets/images/camera.png" alt="" @click="handlerCamera(item, index)" />
+        <div class="setting-time">
+          <div>
+            <span>定时开启时间:</span>
+            <!-- <span class="set-time">13:23:24</span> -->
+            <span class="set-time">{{ item.readData.time_on_hour ?
+              `${item.readData.time_on_hour}:${item.readData.time_on_min}` : '--' }}</span>
+          </div>
+          <div>
+            <span>启用状态:</span>
+            <span class="set-time">{{ item.readData.timerSwitch == '0' ? '启用' : '未启用' }}</span>
+          </div>
+          <div>
+            <span>定时关闭时间:</span>
+            <!-- <span class="set-time">13:23:24</span> -->
+            <span class="set-time">{{ item.readData.time_off_hour ?
+              `${item.readData.time_off_hour}:${item.readData.time_off_min}` : '--' }}</span>
+          </div>
+        </div>
       </div>
+
     </div>
     <!-- 同步开启/关闭弹窗 -->
-    <syncModal :visible="visible" @handleCancel="handleCancel" @handleOk="handleOk"></syncModal>
+    <syncModal :visible="visible" :tooltipText="tooltipText" @handleCancel="handleCancel" @handleOk="handleOk">
+    </syncModal>
     <!-- 定时设置弹窗 -->
     <timeSetModal :visibleTime="visibleTime" @handleCancelTime="handleCancelTime" @handleOk="handleOkTime">
     </timeSetModal>
@@ -44,11 +72,14 @@
     <!-- 操作日志 -->
     <operationModal :visible="visibleOperation" @handleCancel="handleCancelOperation">
     </operationModal>
+    <!-- 集控设备选择 -->
+    <DeviceControl v-if="visibleDeviceControl" :deviceControlData="gateControlData" @confirmChoice="handlerChoice">
+    </DeviceControl>
   </div>
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, inject, watch, nextTick } from 'vue';
+import { reactive, ref, inject, watch, nextTick, onMounted } from 'vue';
 import syncModal from './syncModal.vue';
 import timeSetModal from './timeSetModal.vue';
 import CameraModal from './cameraModal.vue';
@@ -57,8 +88,10 @@ import tipModal from './tipModal.vue'
 import gateDualSVG from './gateDualSVG.ssl.vue'
 import gateSVG from './gateSVG.ssl.vue'
 import operationModal from './operationModal.vue'
+import DeviceControl from './deviceControl.vue'
 import { useRouter } from 'vue-router';
-import { devicecontrol, insertSyncRule, GetSyncRule } from '../airdoor.api'
+import { devicecontrol, insertSyncRule, GetSyncRule, controlList, confirmChoice, manualTimeSync } from '../airdoor.api'
+import { doorStatus } from '../airdoor.data'
 // import { getModelComponent } from '../airdoor.data'
 import { useMessage } from '/@/hooks/web/useMessage';
 import { SvgIcon } from '/@/components/Icon';
@@ -75,6 +108,8 @@ let props = defineProps({
 const { createMessage } = useMessage();
 const globalConfig = inject<any>('globalConfig');
 let router = useRouter();
+//判断当前开启的是同步/集控密码弹窗
+let isTbOrJk = ref('')
 let infoDatas = ref<any[]>([])
 //同步开启/关闭弹窗-控制显示与隐藏
 let visible = ref(false);
@@ -88,31 +123,50 @@ let cameraData = reactive({})
 //操作日志弹窗显示/关闭
 let visibleOperation = ref(false)
 const childRefs = ref<any[]>([])
+//集控风门列表
+let gateControlData = ref<any[]>([])
+//集控设备弹窗显示
+let visibleDeviceControl = ref(false)
+let tooltipText = ref('')
+
 const setChildRef = (el, index) => {
   childRefs.value[index] = el
 }
-
-
-
 //同步开启/关闭--弹窗
 function handlerOpenOrClose(data) {
   visible.value = true;
   visibleStatus.value = data
+  isTbOrJk.value = 'Tb'
+  tooltipText.value = '您正在执行 “开启/关闭” 全部风门的操作,请输入密码执行。'
 }
 //确定
 async function handleOk(param) {
-  let deviceStr = infoDatas.value.map(v => v.deviceID).join(',')
-  let paramcode = visibleStatus.value == 'open' ? 'sameTimeOpen' : 'sameTimeClose'
-  let res = await devicecontrol({ deviceids: deviceStr, paramcode: paramcode, password: param.pass || globalConfig?.simulatedPassword })
-  if (res) {
+  if (isTbOrJk.value == 'Tb') {
+    let deviceStr = infoDatas.value.map(v => v.deviceID).join(',')
+    let paramcode = visibleStatus.value == 'open' ? 'sameTimeOpen' : 'sameTimeClose'
+    let res = await devicecontrol({ deviceids: deviceStr, paramcode: paramcode, password: param.pass || globalConfig?.simulatedPassword })
+    if (res) {
+      visible.value = param.visib
+      // visibleStatus.value = ''
+      doorStatus.openOrclose = true
+      doorStatus.statusCode = visibleStatus.value
+      setTimeout(() => {
+        doorStatus.openOrclose = false
+        doorStatus.statusCode = ''
+      }, 60000 * 5)
+    }
+  } else {
     visible.value = param.visib
-    visibleStatus.value = ''
+    visibleDeviceControl.value = true
   }
+
 }
 //取消
 function handleCancel(param) {
   visible.value = param;
   visibleStatus.value = ''
+  isTbOrJk.value = ''
+  tooltipText.value = ''
 }
 
 //定时设置
@@ -124,12 +178,8 @@ async function handlerTimeSet() {
 }
 //确定
 async function handleOkTime(param) {
-  console.log(param, '定时参数---')
-  let start_time = `${param.formState.hourS}:${param.formState.minuteS}:${param.formState.secondS}`
-  let end_time = `${param.formState.hourE}:${param.formState.minuteE}:${param.formState.secondE}`
-  let enabled = param.formState.checked ? '1' : '0'
-  let res = await insertSyncRule({ startTime: start_time, endTime: end_time, enabled: enabled, id: Ids.value, password: param.formState.passWord })
-  console.log(res, '设置定时---')
+  let timerSwitch = param.formState.checked ? '1' : '0'
+  let res = await insertSyncRule({ timerSwitch: timerSwitch, settimeOnHour: param.formState.hourS, settimeOnMin: param.formState.minuteS, settimeOffHour: param.formState.hourE, settimeOffMin: param.formState.minuteE, password: param.formState.passWord })
   if (res) {
     visibleTime.value = param.visib
     Ids.value = ''
@@ -167,14 +217,38 @@ function handleCancelOperation(param) {
 function monitorAnimation(selectData, index) {
   childRefs.value[index]?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
 }
-// function getInitSvg(param) {
-//   // const dictCodes = getDictItemsByCode('gateStyle');
-//   // console.log(dictCodes, 'dictCodes---')
-// }
-
-
+//获取集控风门列表
+async function getControlList() {
+  let res = await controlList({ devicekind: 'gate', pageNo: 1, pageSize: 100 })
+  gateControlData.value = res.records || []
+}
+//集控设备选择点击
+function handlerDeviceChoice() {
+  isTbOrJk.value = 'Jk'
+  visible.value = true;
+  tooltipText.value = '您正在执行 “集控” 全部风门的操作,请输入密码执行。'
+}
+//集控设备确定选择
+async function handlerChoice(param) {
+  let data: any[] = []
+  param.forEach(el => {
+    let list = gateControlData.value.find(v => v.id == el)
+    list.isAutosync = '1'
+    data.push(list)
+  })
+  let res = await confirmChoice({ obj: data })
+  if (res) {
+    visibleDeviceControl.value = false
+  }
+}
+//同步PLC时钟
+async function handlerClock() {
+  let res = await manualTimeSync()
+  if (res) {
+    createMessage.success('PLC时钟同步成功!');
+  }
+}
 watch(() => props.infoData, (newV, oldV) => {
-  console.log(newV, 'new---')
   infoDatas.value = newV
   if (newV.length) {
     nextTick(() => {
@@ -186,6 +260,9 @@ watch(() => props.infoData, (newV, oldV) => {
   }
 })
 
+onMounted(() => {
+  getControlList()
+})
 
 </script>
 
@@ -214,7 +291,13 @@ watch(() => props.infoData, (newV, oldV) => {
     height: 40px;
     padding: 0px 10px;
     display: flex;
+
     // align-items: center;
+    .device-control-btn {
+      position: absolute;
+      right: 35px;
+      top: 30px;
+    }
   }
 
   .content-r-container {
@@ -263,6 +346,30 @@ watch(() => props.infoData, (newV, oldV) => {
         height: calc(100% - 40px);
       }
 
+      .timePlcNow {
+        position: absolute;
+        right: 5px;
+        top: 0px;
+        color: #fff;
+        font-size: 12px;
+
+      }
+
+      .setting-time {
+        width: 100%;
+        position: absolute;
+        bottom: -25px;
+        left: 0;
+        display: flex;
+        justify-content: space-between;
+        color: #fff;
+        font-size: 12px;
+      }
+
+      .set-time {
+        color: rgba(60, 242, 255);
+      }
+
       img {
         position: absolute;
         right: 20px;

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

@@ -1,14 +1,23 @@
 <template>
   <div class="door-status-list">
     <div class="list-content" v-for="(item, index) in menuDatas" :key="index">
-      <div class="item-label">{{ item.strinstallpos }}</div>
-      <div :class="item.statusC == '正常' ? 'item-status' : 'item-status-warn'">{{ item.statusC }}</div>
+      <div class="item-label">{{ item.strinstallpos }} </div>
+      <!-- <div :class="item.netStatus == '1' ? 'item-status' : 'item-status-warn'">{{ doorStatus.openOrclose ?
+        item.ndoorcount == '2' ? item.readData.frontGateOpen == '1' && item.readData.rearGateOpen == '1' ? '同步打开' :
+          item.readData.frontGateOpen == '0' && item.readData.rearGateOpen == '0' ? '同步关闭' : '状态异常' :
+          item.readData.frontGateOpen == '1' ? '同步打开' : item.readData.frontGateOpen == '0' ? '同步关闭' : '状态异常' :
+        item.netStatus == '0' ? '断开' : '连接' }}</div> -->
+       
+      <div :class="item.netStatus == '1' ? 'item-status' : 'item-status-warn'">{{ doorStatus.openOrclose ?
+        doorStatus.statusCode == 'open' ? '打开成功' : doorStatus.statusCode == 'close' ? '关闭正常' : '操作异常' : item.netStatus == '0'
+          ? '断开' : '连接' }}</div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { ref, watch, watchEffect } from 'vue'
+import { doorStatus } from '../airdoor.data'
 
 let props = defineProps({
   menuData: {
@@ -21,16 +30,8 @@ let props = defineProps({
 
 let menuDatas = ref<any[]>([])
 
-watch(() => props.menuData, (newV, oldV) => {
-  console.log(newV, 'menuData')
-  if (newV.length) {
-    menuDatas.value = newV.map((el: any) => {
-      return {
-        ...el,
-        statusC:el.ndoorcount=='2' ? el.readData.frontGateOpen == '1' && el.readData.rearGateOpen == '1' ? '正常' : '异常' : el.readData.frontGateOpen == '1' ? '正常' : '异常'
-      }
-    })
-  }
+watchEffect(() => {
+  menuDatas.value = props.menuData
 })
 
 </script>
@@ -66,20 +67,18 @@ watch(() => props.menuData, (newV, oldV) => {
     box-sizing: border-box;
 
     .item-label {
-      width: calc(100% - 50px);
+      width: calc(100% - 70px);
     }
 
     .item-status {
-      width: 40px;
+      width: 70px;
       text-align: right;
-      margin-left: 10px;
       color: #16e65c;
     }
 
     .item-status-warn {
-      width: 40px;
+      width: 70px;
       text-align: right;
-      margin-left: 10px;
       color: #f00808;
     }
   }

+ 5 - 1
src/views/vent/monitorManager/airDoor/components/syncModal.vue

@@ -2,7 +2,7 @@
   <div class="sync-modal">
     <a-modal v-model:visible="Visible" width="450px" :title="Title" centered destroyOnClose @ok="handleOk"
       @cancel="handleCancel">
-      <p>您正在执行 “开启/关闭” 全部风门的操作,请输入密码执行。</p>
+      <p>{{ tooltipText }}</p>
       <a-form :model="formState" name="basic" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" autocomplete="off">
         <a-form-item label="输入密码" name="passWord" :rules="[{ required: true, message: '请输入密码!' }]">
           <a-input-password style="width:220px" v-model:value="formState.passWord" />
@@ -24,6 +24,10 @@ let props = defineProps({
   Title: {
     type: String,
     default: '操作确认'
+  },
+  tooltipText:{
+    type:String,
+    default:''
   }
 })
 

+ 9 - 9
src/views/vent/monitorManager/airDoor/components/timeSetModal.vue

@@ -3,7 +3,7 @@
     <a-modal v-model:visible="Visible" width="550px" :title="Title" centered destroyOnClose @ok="handleOk"
       @cancel="handleCancelTime">
 
-      <a-form :model="formState" name="basic" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" autocomplete="off">
+      <a-form :model="formState" name="basic" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }" autocomplete="off">
         <a-form-item label="开启时间">
           <a-select ref="select" v-model:value="formState.hourS" style="width:130px" placeholder="请选择...">
             <a-select-option v-for="(item, index) in hourOption" :key="index" :value="item.value">{{ item.label
@@ -15,11 +15,11 @@
             }}</a-select-option>
           </a-select>
           <div class="unit">分</div>
-          <a-select ref="select" v-model:value="formState.secondS" style="width:130px" placeholder="请选择...">
+          <!-- <a-select ref="select" v-model:value="formState.secondS" style="width:130px" placeholder="请选择...">
             <a-select-option v-for="(item, index) in minuteOption" :key="index" :value="item.value">{{ item.label
             }}</a-select-option>
           </a-select>
-          <div class="unit">秒</div>
+          <div class="unit">秒</div> -->
         </a-form-item>
         <a-form-item label="关闭时间">
           <a-select ref="select" v-model:value="formState.hourE" style="width:130px" placeholder="请选择...">
@@ -32,11 +32,11 @@
             }}</a-select-option>
           </a-select>
           <div class="unit">分</div>
-          <a-select ref="select" v-model:value="formState.secondE" style="width:130px" placeholder="请选择...">
+          <!-- <a-select ref="select" v-model:value="formState.secondE" style="width:130px" placeholder="请选择...">
             <a-select-option v-for="(item, index) in minuteOption" :key="index" :value="item.value">{{ item.label
             }}</a-select-option>
           </a-select>
-          <div class="unit">秒</div>
+          <div class="unit">秒</div> -->
         </a-form-item>
         <a-form-item label="启动定时">
           <a-switch v-model:checked="formState.checked" />
@@ -68,10 +68,10 @@ let Visible = ref(false)
 let formState = reactive({
   hourS: '',
   minuteS: '',
-  secondS: '',
+  // secondS: '',
   hourE: '',
   minuteE: '',
-  secondE: '',
+  // secondE: '',
   checked: true,
   passWord: ''
 })
@@ -89,8 +89,8 @@ function handleCancelTime(){
   formState.minuteE=''
   formState.minuteS=''
   formState.passWord=''
-  formState.secondE=''
-  formState.secondS=''
+  // formState.secondE=''
+  // formState.secondS=''
   $emit('handleCancelTime',false)
 }
 

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

@@ -19,9 +19,9 @@ import doorMenuL from './components/door-menu-l.vue'
 import doorContentR from './components/door-content-r.vue'
 import { getDevice, upcoming } from './airdoor.api'
 
-  let menuData = ref<any[]>([]);
-  //控制定时设置提示弹窗显示/隐藏
-  let visibleTs30 = ref(false);
+let menuData = ref<any[]>([]);
+//控制定时设置提示弹窗显示/隐藏
+let visibleTs30 = ref(false);
 
 // https获取监测数据
 let timer: null | NodeJS.Timeout = null;
@@ -44,7 +44,11 @@ function getMonitor(flag?) {
 async function getMenuList() {
   let res = await getDevice({ devicetype: "gate", pagetype: "normal" })
   console.log(res, 'menuList')
-  menuData.value = res.msgTxt[0].datalist || []
+  if (res.msgTxt[0].datalist.length) {
+    menuData.value = res.msgTxt[0].datalist.filter(v => v.isAutosync == '1')
+  } else {
+    menuData.value = []
+  }
 }
 async function upcomingList() {
   let res = await upcoming({})

+ 159 - 172
src/views/vent/monitorManager/footageMonitor/components/moduleCommon.vue

@@ -7,10 +7,10 @@
         </template>
         <template #container>
           <div class="container-t">
-            <ModuleHead :timeDate="timeDate" @change-time="changeTime" />
+            <ModuleHead  :timeDate="timeDate" @changeTime="changeTime"></ModuleHead>
           </div>
           <div class="container-b">
-            <SingLineArea :option="option" :chartData="chartData" height="280px" />
+            <SingLineArea :option="option" :chartData="chartData" height="280px"></SingLineArea>
           </div>
         </template>
       </ventBox1>
@@ -20,27 +20,17 @@
         </template>
         <template #container>
           <div class="container-t">
-            <ModuleHead
-              :isShowSelect="true"
-              :menuData="gasMenuList"
-              :timeDate="timeDate"
-              :devLabel="gasDevLabel"
-              @change-menu="changeGasMenu"
-              @change-time="changeTime"
-            />
+            <ModuleHead :isShowSelect="true" :menuData="gasMenuList" :timeDate="timeDate" :devLabel="gasDevLabel" @changeMenu="changeGasMenu"
+              @changeTime="changeTime"></ModuleHead>
           </div>
           <div class="container-b">
-            <BarAndLine
-              class="echarts-line"
-              :xAxisPropType="xAxisPropType"
-              :dataSource="gasList"
-              height="280px"
-              :chartsColumns="chartsColumns"
-              :option="echatsOption"
-            />
+            <BarAndLine class="echarts-line" :xAxisPropType="xAxisPropType" :dataSource="gasList" height="280px"
+              :chartsColumns="chartsColumns" :option="echatsOption" />
           </div>
+
         </template>
       </ventBox1>
+
     </div>
     <div class="lr right-box">
       <div class="item-box sensor-container">
@@ -49,183 +39,180 @@
             <div>工作面进尺及瓦斯涌出量</div>
           </template>
           <template #container>
-            <SingLineArea :option="optionGas" :chartData="chartGasData" height="280px" />
+            <SingLineArea :option="optionGas" :chartData="chartGasData" height="280px"></SingLineArea>
           </template>
         </ventBox1>
+
       </div>
     </div>
   </div>
+
 </template>
 
 <script setup lang="ts">
-  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';
-  import ModuleHead from './moduleHead.vue';
-  import { option, gasMenuList, chartsColumns, echatsOption, optionGas } from '../footage.data';
-  import { list, getCurveGraphData } from '../footage.api';
-  import dayjs from 'dayjs';
-
-  let props = defineProps({
-    optionValue: {
-      type: String,
-      default: '',
-    },
-  });
-  let paramData = ref<any[]>([]);
-  let timeDate = reactive({
-    startTime: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
-    endTime: dayjs().format('YYYY-MM-DD'),
-  });
-  let gasDevLabel = ref('T0(上隅角)');
-  let xAxisPropType = ref('ttime');
-  let chartData = ref<any[]>([]);
-  let gasList = ref<any[]>([]);
-  let chartGasData = ref<any[]>([]);
-
-  // https获取监测数据
-  let timer: null | NodeJS.Timeout = null;
-  function getMonitor(flag?) {
-    if (Object.prototype.toString.call(timer) === '[object Null]') {
-      timer = setTimeout(
-        async () => {
-          getCurveGraphDataList();
-          if (timer) {
-            timer = null;
-          }
-          await getMonitor();
-        },
-        flag ? 0 : 5000
-      );
-    }
-  }
-  async function getCurveGraphDataList() {
-    let res = await getCurveGraphData({ devId: props.optionValue, startTime: timeDate.startTime, endTime: timeDate.endTime });
-    if (res.length) {
-      paramData.value = res;
-      chartData.value =
-        res.map((el) => {
-          return {
-            name: el.fteTime,
-            val: el.planFte,
-            val1: el.ateFte,
-            val2: el.sumFte,
-          };
-        }) || [];
-      gasList.value =
-        res.map((d) => {
-          let data = JSON.parse(d.gasInfo);
-          return {
-            ttime: d.fteTime,
-            wsnd: data.gasVal0Ave,
-            xdnd: data.gasVal0Rel,
-            jdnd: data.gasVal0Abs,
-            m3Ave: data.m3Ave,
-          };
-        }) || [];
-      chartGasData.value =
-        res.map((c) => {
-          let data = JSON.parse(c.gasInfo);
-          return {
-            name: c.fteTime,
-            val: c.ateFte,
-            val1: data.gasVal0Ave,
-          };
-        }) || [];
-    }
+
+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';
+import ModuleHead from './moduleHead.vue'
+import { option, gasMenuList, chartsColumns, echatsOption, optionGas } from '../footage.data'
+import { list, getCurveGraphData } from '../footage.api';
+import dayjs from 'dayjs';
+
+let props=defineProps({
+  optionValue:{
+    type:String,
+    default:''
   }
+})
+let paramData = ref<any[]>([])
+let timeDate = reactive({
+  startTime: dayjs().subtract(7, 'day').format('YYYY-MM-DD'),
+  endTime: dayjs().format('YYYY-MM-DD'),
+})
+let gasDevLabel = ref('T0(上隅角)')
+let xAxisPropType = ref('ttime');
+let chartData = ref<any[]>([]);
+let gasList = ref<any[]>([]);
+let chartGasData = ref<any[]>([])
 
-  function changeGasMenu(param) {
-    gasDevLabel.value = param.label;
-    switch (param.label) {
-      case 'T0(上隅角)':
-        gasList.value = paramData.value.map((d) => {
-          let data = JSON.parse(d.gasInfo);
-          return {
-            ttime: d.fteTime,
-            wsnd: data.gasVal0Ave,
-            xdnd: data.gasVal0Rel,
-            jdnd: data.gasVal0Abs,
-            m3Ave: data.m3Ave,
-          };
-        });
-        break;
-      case 'T1(回风流距工作面割煤线10m范围内)':
-        gasList.value = paramData.value.map((d) => {
-          let data = JSON.parse(d.gasInfo);
-          return {
-            ttime: d.fteTime,
-            wsnd: data.gasVal1Ave,
-            xdnd: data.gasVal1Rel,
-            jdnd: data.gasVal1Abs,
-            m3Ave: data.m3Ave,
-          };
-        });
-        break;
-      case 'T2(回风绕道口10~15m处)':
-        gasList.value = paramData.value.map((d) => {
-          let data = JSON.parse(d.gasInfo);
-          return {
-            ttime: d.fteTime,
-            wsnd: data.gasVal2Ave,
-            xdnd: data.gasVal2Rel,
-            jdnd: data.gasVal2Abs,
-            m3Ave: data.m3Ave,
-          };
-        });
-        break;
-    }
+// https获取监测数据
+let timer: null | NodeJS.Timeout = null;
+function getMonitor(flag?) {
+  if (Object.prototype.toString.call(timer) === '[object Null]') {
+    timer = setTimeout(async () => {
+      getCurveGraphDataList()
+      if (timer) {
+        timer = null;
+      }
+      await getMonitor();
+    }, flag ? 0 : 5000);
   }
-  function changeTime(param) {
-    console.log(param, 'param===');
-    timeDate.startTime = param.startTime;
-    timeDate.endTime = param.endTime;
-    getCurveGraphDataList();
+};
+async function getCurveGraphDataList() {
+  let res = await getCurveGraphData({ devId: props.optionValue, startTime: timeDate.startTime, endTime: timeDate.endTime })
+  if (res.length) {
+    paramData.value = res
+    chartData.value = res.map(el => {
+      return {
+        name: el.fteTime,
+        val: el.planFte,
+        val1: el.ateFte,
+        val2: el.sumFte
+      }
+    }) || []
+    gasList.value = res.map(d => {
+      let data = JSON.parse(d.gasInfo)
+      return {
+        ttime: d.fteTime,
+        wsnd: data.gasVal0Ave,
+        xdnd: data.gasVal0Rel,
+        jdnd: data.gasVal0Abs,
+        m3Ave: data.m3Ave
+      }
+    }) || []
+    chartGasData.value = res.map(c => {
+      let data = JSON.parse(c.gasInfo)
+      return {
+        name: c.fteTime,
+        val: c.ateFte,
+        val1: data.gasVal0Ave,
+      }
+    }) || []
   }
+}
 
-  watchEffect(() => {
-    props.optionValue && getCurveGraphDataList();
-  });
-  onMounted(async () => {
-    await getMonitor(true);
-  });
-  onUnmounted(() => {
-    if (timer) {
-      clearTimeout(timer);
-      timer = undefined;
-    }
-  });
+function changeGasMenu(param) {
+  gasDevLabel.value = param.label
+  switch (param.label) {
+    case 'T0(上隅角)':
+      gasList.value = paramData.value.map(d => {
+        let data = JSON.parse(d.gasInfo)
+        return {
+          ttime: d.fteTime,
+          wsnd: data.gasVal0Ave,
+          xdnd: data.gasVal0Rel,
+          jdnd: data.gasVal0Abs,
+          m3Ave: data.m3Ave
+        }
+      })
+      break;
+    case 'T1(回风流距工作面割煤线10m范围内)':
+      gasList.value = paramData.value.map(d => {
+        let data = JSON.parse(d.gasInfo)
+        return {
+          ttime: d.fteTime,
+          wsnd: data.gasVal1Ave,
+          xdnd: data.gasVal1Rel,
+          jdnd: data.gasVal1Abs,
+          m3Ave: data.m3Ave
+        }
+      })
+      break;
+    case 'T2(回风绕道口10~15m处)':
+      gasList.value = paramData.value.map(d => {
+        let data = JSON.parse(d.gasInfo)
+        return {
+          ttime: d.fteTime,
+          wsnd: data.gasVal2Ave,
+          xdnd: data.gasVal2Rel,
+          jdnd: data.gasVal2Abs,
+          m3Ave: data.m3Ave
+        }
+      })
+      break;
+  }
+}
+function changeTime(param) {
+  console.log(param, 'param===')
+  timeDate.startTime = param.startTime
+  timeDate.endTime = param.endTime
+  getCurveGraphDataList()
+}
+
+watchEffect(()=>{
+  props.optionValue &&  getCurveGraphDataList()
+})
+onMounted(async () => {
+  await getMonitor()
+});
+onUnmounted(() => {
+  if (timer) {
+    clearTimeout(timer);
+    timer = undefined;
+  }
+});
 </script>
 
 <style lang="less">
-  @import '/@/design/vent/modal.less';
+@import '/@/design/vent/modal.less';
 
-  .@{ventSpace}-select-dropdown {
-    background: #ffffff !important;
-    border-bottom: 1px solid rgba(236, 236, 236, 0.4);
-    backdrop-filter: blur(10px);
+.@{ventSpace}-select-dropdown {
+  background: #ffffff !important;
+  border-bottom: 1px solid rgba(236, 236, 236, 0.4);
+  backdrop-filter: blur(10px);
 
-    .@{ventSpace}-select-item-option-selected,
-    .@{ventSpace}-select-item-option-active {
-      background-color: #ffffff33 !important;
-    }
+  .@{ventSpace}-select-item-option-selected,
+  .@{ventSpace}-select-item-option-active {
+    background-color: #ffffff33 !important;
+  }
 
-    .@{ventSpace}-select-item:hover {
-      background-color: #ffffff33 !important;
-    }
+  .@{ventSpace}-select-item:hover {
+    background-color: #ffffff33 !important;
   }
+}
 </style>
 
 <style lang="less" scoped>
-  @import '../../comment/less/workFace.less';
-  @ventSpace: zxm;
+@import '../../comment/less/workFace.less';
+@ventSpace: zxm;
 
-  .left-box {
-    margin-top: 0 !important;
-  }
+.left-box {
+  margin-top: 0 !important;
+}
 
-  .right-box {
-    margin-top: 0 !important;
-  }
+.right-box {
+  margin-top: 0 !important;
+}
 </style>

+ 100 - 0
src/views/vent/monitorManager/safetyMonitor/WarnHistoryTable.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="warn-history">
+    <a-form :model="formState" layout="inline" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off">
+      <a-form-item label="安装地点">
+        <a-input v-model:value="formState.installLocation" placeholder="请输入" style="width: 220px;" />
+      </a-form-item>
+      <a-form-item label="报警类型">
+        <a-select v-model:value="formState.exceptionType" style="width: 220px">
+          <a-select-option v-for="(item, index) in exceptionTypeList" :key="index" :value="item.value">{{
+            item.label
+          }}</a-select-option>
+        </a-select>
+      </a-form-item>
+
+      <a-form-item label="是否完成">
+        <a-select v-model:value="formState.isCompleted" style="width: 220px">
+          <a-select-option v-for="(item, index) in isCompletedList" :key="index" :value="item.value">{{
+            item.label
+          }}</a-select-option>
+        </a-select>
+      </a-form-item>
+      <a-button type="primary" preIcon="ant-design:search-outlined" style="margin-left:30px;"
+        @click="getSearch">查询</a-button>
+      <a-button type="primary" preIcon="ant-design:sync-outlined" style="margin-left: 20px;"
+        @click="getReset">重置</a-button>
+    </a-form>
+    <a-table size="small" :dataSource="dataSource" :columns="warnHistoryColumns" :scroll="{ y: 620 }"
+      :pagination="pagination" @change="pageChange">
+      <template #actionSp="{ record }">
+
+      </template>
+    </a-table>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, onMounted } from 'vue'
+import { warnHistoryColumns, exceptionTypeList, isCompletedList,pagination} from './safety.data'
+import { warnList } from './safety.api'
+
+let dataSource = ref<any[]>([])
+let formState = reactive({
+  installLocation: '',
+  exceptionType: '',
+  isCompleted: ""
+})
+
+async function getTableList() {
+  let res = await warnList({ pageNo: pagination.current, pageSize: pagination.pageSize, ...formState })
+  dataSource.value = res.records.map(el => {
+    return {
+      ...el,
+      isCompletedC: el.isCompleted == 1 ? '是' : el.isCompleted == 0 ? '否' : '',
+      exceptionTypeC: el.exceptionType == '001' ? '超限报警' : el.exceptionType == '002' ? '断电报警' : el.exceptionType == '003' ? '馈电异常' : el.exceptionType == '004' ? '传感器断线' : el.exceptionType == '005' ? '基站断电' : el.exceptionType == '006' ? '基站不通' : el.exceptionType == '007' ? '标校' : el.exceptionType == '008' ? '超量程' : ''
+    }
+  }) || []
+  pagination.total = res.total
+}
+//分页
+function pageChange(val) {
+  pagination.current = val.current;
+  pagination.pageSize = val.pageSize;
+  getTableList()
+}
+//查询
+function getSearch() {
+  pagination.current = 1
+  getTableList()
+}
+//重置
+function getReset() {
+  formState.exceptionType = ''
+  formState.installLocation = ''
+  formState.isCompleted = ''
+  pagination.current = 1
+  getTableList()
+}
+
+onMounted(() => {
+  getTableList()
+})
+</script>
+
+<style lang="less" scoped>
+.zxm-form {
+  height: 70px;
+  display: flex;
+  align-items: center;
+
+}
+
+::v-deep .zxm-form-item-label>label {
+  color: #fff !important;
+}
+
+::v-deep .zxm-input {
+  background: #00000017 !important;
+  border: 1px solid var(--vent-form-item-border) !important;
+}
+</style>

+ 437 - 465
src/views/vent/monitorManager/safetyMonitor/index.vue

@@ -1,26 +1,18 @@
 <template>
   <div class="device-box" id="monitorBox">
-    <a-tabs class="tabs-box" type="card" v-model:activeKey="activeKey" @change="tabChange" id="tabsBox" v-if="isRefresh">
+    <a-tabs class="tabs-box" type="card" v-model:activeKey="activeKey" @change="tabChange" id="tabsBox"
+      v-if="isRefresh">
       <a-tab-pane key="1" tab="实时监测">
         <template v-if="deviceType == 'fan' && activeKey == '1'">
           <GroupMonitorTable :dataSource="dataSource" :columnsType="`${deviceType}_monitor`" />
         </template>
         <template v-else-if="activeKey == '1' && deviceType">
           <template v-if="hasPermission('btn:noGb') && deviceType.startsWith('safetymonitor')">
-            <MonitorTable
-              ref="monitorTable"
-              :columnsType="`${deviceType}_monitor`"
-              :dataSource="dataSource"
-              design-scope="device_monitor"
-              :isShowPagination="false"
-              :isShowActionColumn="false"
+            <MonitorTable ref="monitorTable" :columnsType="`${deviceType}_monitor`" :dataSource="dataSource"
+              design-scope="device_monitor" :isShowPagination="false" :isShowActionColumn="false"
               :is-show-select="false"
               :form-config="deviceType == 'safetymonitor' && sysOrgCode != 'zjtzqctmk' ? formConfig : undefined"
-              title="设备监测"
-              :scroll="{ y: 650 }"
-              :defSort="defSort"
-              sortDataIndex="strinstallpos"
-            >
+              title="设备监测" :scroll="{ y: 650 }" :defSort="defSort" sortDataIndex="strinstallpos">
               <template #filterCell="{ column, record }">
                 <template v-if="deviceType.startsWith('safetymonitor')">
                   <div v-if="!record.devicename && column.dataIndex === 'devicename'">-</div>
@@ -30,9 +22,9 @@
                   <div v-if="!record.lowRange && column.dataIndex === 'lowRange'">-</div>
                   <div v-if="!record.dataTypeName && column.dataIndex === 'dataTypeName'">-</div>
                 </template>
-                <a-tag v-if="column.dataIndex === 'exceptionType_str'" :color="record.exceptionType_str == '正常' ? 'green' : '#f00'">
-                  {{ record.exceptionType_str == '正常' ? '正常' : '异常' }}</a-tag
-                >
+                <a-tag v-if="column.dataIndex === 'exceptionType_str'"
+                  :color="record.exceptionType_str == '正常' ? 'green' : '#f00'">
+                  {{ record.exceptionType_str == '正常' ? '正常' : '异常' }}</a-tag>
                 <a-tag v-if="column.dataIndex === 'netStatus'" :color="record.netStatus == '1' ? 'green' : '#f00'">{{
                   record.netStatus == '1' ? '正常' : '异常'
                 }}</a-tag>
@@ -43,20 +35,11 @@
             </MonitorTable>
           </template>
           <template v-else>
-            <MonitorTable
-              ref="monitorTable"
-              :columnsType="`${deviceType}_monitor`"
-              :dataSource="dataSource"
-              design-scope="device_monitor"
-              :isShowPagination="false"
-              :isShowActionColumn="false"
+            <MonitorTable ref="monitorTable" :columnsType="`${deviceType}_monitor`" :dataSource="dataSource"
+              design-scope="device_monitor" :isShowPagination="false" :isShowActionColumn="false"
               :is-show-select="false"
               :form-config="deviceType == 'safetymonitor' && sysOrgCode != 'zjtzqctmk' ? formConfig : undefined"
-              title="设备监测"
-              :scroll="{ y: 650 }"
-              :defSort="defSort"
-              sortDataIndex="strinstallpos"
-            >
+              title="设备监测" :scroll="{ y: 650 }" :defSort="defSort" sortDataIndex="strinstallpos">
               <template #filterCell="{ column, record }">
                 <template v-if="deviceType.startsWith('safetymonitor')">
                   <div v-if="!record.devicename && column.dataIndex === 'devicename'">-</div>
@@ -66,9 +49,9 @@
                   <div v-if="!record.lowRange && column.dataIndex === 'lowRange'">-</div>
                   <div v-if="!record.dataTypeName && column.dataIndex === 'dataTypeName'">-</div>
                 </template>
-                <a-tag v-if="column.dataIndex === 'exceptionType_str'" :color="record.exceptionType_str == '正常' ? 'green' : '#f00'">
-                  {{ record.exceptionType_str == '正常' ? '正常' : '异常' }}</a-tag
-                >
+                <a-tag v-if="column.dataIndex === 'exceptionType_str'"
+                  :color="record.exceptionType_str == '正常' ? 'green' : '#f00'">
+                  {{ record.exceptionType_str == '正常' ? '正常' : '异常' }}</a-tag>
                 <a-tag v-if="column.dataIndex === 'netStatus'" :color="record.netStatus == '1' ? 'green' : '#f00'">{{
                   record.netStatus == '1' ? '正常' : '异常'
                 }}</a-tag>
@@ -111,13 +94,8 @@
       </a-tab-pane>
       <a-tab-pane key="2" tab="历史数据">
         <div class="tab-item">
-          <HistoryTable
-            ref="historyTable"
-            v-if="activeKey == '2'"
-            :columns-type="`${deviceType}`"
-            :device-type="deviceType"
-            designScope="device-history"
-          />
+          <HistoryTable ref="historyTable" v-if="activeKey == '2'" :columns-type="`${deviceType}`"
+            :device-type="deviceType" designScope="device-history" />
         </div>
         <!-- 图表 -->
         <!-- <div v-if="alive" style="width:100%;height:280px;margin: 20px 0px;">
@@ -141,41 +119,28 @@
       <template v-if="!hasPermission('safety:hideWarning')">
         <a-tab-pane key="3" tab="报警历史">
           <div class="tab-item">
-            <AlarmHistoryTable
-              ref="alarmHistoryTable"
-              v-if="activeKey == '3' && deviceType == 'safetymonitor'"
-              columns-type="alarm"
-              :list="safetyList"
-              :device-type="deviceType"
+            <AlarmHistoryTable ref="alarmHistoryTable" v-if="activeKey == '3' && deviceType == 'safetymonitor'"
+              columns-type="alarm" :list="safetyList" :device-type="deviceType"
               :device-list-api="getDeviceList.bind(null, { devicekind: deviceType, pageSize: 10000 })"
-              designScope="alarm-history"
-            />
+              designScope="alarm-history" />
           </div>
         </a-tab-pane>
       </template>
       <template v-else-if="deviceType != 'safetymonitor'">
         <a-tab-pane key="3" tab="报警历史">
           <div class="tab-item">
-            <AlarmHistoryCommentTable
-              v-if="activeKey == '3'"
-              columns-type="alarm"
-              :device-type="deviceType"
+            <AlarmHistoryCommentTable v-if="activeKey == '3'" columns-type="alarm" :device-type="deviceType"
               :device-list-api="getDeviceList.bind(null, { devicekind: deviceType, pageSize: 10000 })"
-              designScope="alarm-history"
-            />
+              designScope="alarm-history" />
           </div>
         </a-tab-pane>
       </template>
       <a-tab-pane key="4" tab="操作历史" v-if="deviceType !== 'safetymonitor' && deviceType !== 'wasichoufang'">
         <div class="tab-item">
-          <HandlerHistoryTable
-            ref="handlerHistoryTable"
-            v-if="activeKey == '4'"
-            columns-type="operator_history"
+          <HandlerHistoryTable ref="handlerHistoryTable" v-if="activeKey == '4'" columns-type="operator_history"
             :device-type="deviceType"
             :device-list-api="getDeviceList.bind(null, { devicekind: deviceType, pageSize: 10000 })"
-            designScope="operator-history"
-          />
+            designScope="operator-history" />
         </div>
       </a-tab-pane>
       <a-tab-pane key="5" tab="报警分析" v-if="!hasPermission('safety:hideWarning') && deviceType == 'safetymonitor'">
@@ -183,506 +148,513 @@
           <safetyWarnAnalysis :device-type="deviceType" />
         </div>
       </a-tab-pane>
+       <a-tab-pane key="6" tab="预警历史" v-if="sysOrgCode == 'sdmtjtbdmk'">
+          <div class="tab-item">
+            <WarnHistory></WarnHistory>
+          </div>
+        </a-tab-pane>
     </a-tabs>
     <div class="right-btn-group" v-if="deviceType.startsWith('safetymonitor')">
       <div class="update-btn">
         <span>同步分站:</span>
-        <a-select
-          v-model:value="subStation"
-          :options="subStationOptions"
-          placeholder="同步分站"
-          size="large"
-          :fieldNames="{ label: 'strinstallpos', value: 'id' }"
-          style="width: 150px"
-        />
+        <a-select v-model:value="subStation" :options="subStationOptions" placeholder="同步分站" size="large"
+          :fieldNames="{ label: 'strinstallpos', value: 'id' }" style="width: 150px" />
         <div class="btn btn1" @click="updateSubstation">确定</div>
       </div>
-      <div class="export-btn"><div class="btn btn1" @click="exportData" v-if="!hasPermission('safety:hideWarning')">一键导出</div></div>
-      <div class="export-btn m-l-20px" v-if="!hasPermission('safety:hideWarning')"
-        ><div class="btn btn2" @click="goSystem"><send-outlined />安全监控系统</div></div
-      >
+      <div class="export-btn">
+        <div class="btn btn1" @click="exportData" v-if="!hasPermission('safety:hideWarning')">一键导出</div>
+      </div>
+      <div class="export-btn m-l-20px" v-if="!hasPermission('safety:hideWarning')">
+        <div class="btn btn2" @click="goSystem"><send-outlined />安全监控系统</div>
+      </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted, onUnmounted, shallowRef, defineProps, watch, inject, unref } from 'vue';
-  import { list, getDeviceList, safetyList, getExportUrl, subStationList, initSubStation } from './safety.api';
-  import AlarmHistoryCommentTable from '../comment/AlarmHistoryTable.vue';
-  import safetyWarnAnalysis from '../comment/safetyWarnAnalysis.vue';
-  import AlarmHistoryTable from './AlarmHistoryTable.vue';
-  import HistoryTable from './HistoryTable.vue';
-  import HandlerHistoryTable from '../comment/HandlerHistoryTable.vue';
-  import MonitorTable from '../comment/MonitorTable.vue';
-  import GroupMonitorTable from '../comment/GroupMonitorTable.vue';
-  import { useRouter } from 'vue-router';
-  import { formConfig, isHaveNoAction } from './safety.data';
-  import { getDictItemsByCode } from '/@/utils/dict';
-  import { usePermission } from '/@/hooks/web/usePermission';
-  import { useGlobSetting } from '/@/hooks/setting';
-  import { useMethods } from '/@/hooks/system/useMethods';
-  import { message } from 'ant-design-vue';
-  import { SendOutlined } from '@ant-design/icons-vue';
-  import { template } from 'lodash-es';
-
-  const { sysOrgCode } = useGlobSetting();
-  const { hasPermission } = usePermission();
-  const globalConfig = inject('globalConfig');
-
-  const { handleExportXls } = useMethods();
-
-  // import { BorderBox8 as DvBorderBox8 } from '@kjgl77/datav-vue3';
-
-  // const echartsOption = {
-  //   grid: {
-  //     top: '60px',
-  //     left: '10px',
-  //     right: '25px',
-  //     bottom: '5%',
-  //     containLabel: true,
-  //   },
-  //   toolbox: {
-  //     feature: {},
-  //   },
-  // };
-  // let alive = ref(true)
-
-  type DeviceType = { deviceType: string; deviceName: string; datalist: any[] };
-
-  const props = defineProps({
-    pageData: {
-      type: Object,
-      default: () => {},
-    },
-  });
-
-  const scroll = {
-    y: 360,
-  };
-  const defSort = {
-    column: 'strinstallpos',
-    order: 'desc',
-  };
-  const monitorTable = ref();
-  const historyTable = ref();
-  const alarmHistoryTable = ref();
-  const handlerHistoryTable = ref();
-
-  const isRefresh = ref(true);
-
-  const activeKey = ref('1'); // tab key
-  const dataSource = shallowRef([]); // 实时监测数据
-  const deviceType = ref(''); // 监测设备类型
-  const subStation = ref('');
-  const subStationOptions = ref([]);
-  // let dataSourceHis = shallowRef([])//历史数据
-
-  //历史数据
-  async function changeHis(data) {
-    // alive.value = false
-    // nextTick(() => {
-    //   dataSourceHis = data
-    //   alive.value = true
-    // })
-  }
-
-  async function tabChange(activeKeyVal) {
-    activeKey.value = activeKeyVal;
-    if (activeKey.value != '1') {
-      if (timer != undefined) {
-        clearTimeout(timer);
-        timer = undefined;
-      }
-    } else {
-      timer = null;
-      await getMonitor(true);
+import { ref, onMounted, onUnmounted, shallowRef, defineProps, watch, inject, unref } from 'vue';
+import { list, getDeviceList, safetyList, getExportUrl, subStationList, initSubStation } from './safety.api';
+import AlarmHistoryCommentTable from '../comment/AlarmHistoryTable.vue';
+import safetyWarnAnalysis from '../comment/safetyWarnAnalysis.vue';
+import AlarmHistoryTable from './AlarmHistoryTable.vue';
+import HistoryTable from './HistoryTable.vue';
+import WarnHistory from './WarnHistoryTable.vue'
+import HandlerHistoryTable from '../comment/HandlerHistoryTable.vue';
+import MonitorTable from '../comment/MonitorTable.vue';
+import GroupMonitorTable from '../comment/GroupMonitorTable.vue';
+import { useRouter } from 'vue-router';
+import { formConfig, isHaveNoAction } from './safety.data';
+import { getDictItemsByCode } from '/@/utils/dict';
+import { usePermission } from '/@/hooks/web/usePermission';
+import { useGlobSetting } from '/@/hooks/setting';
+import { useMethods } from '/@/hooks/system/useMethods';
+import { message } from 'ant-design-vue';
+import { SendOutlined } from '@ant-design/icons-vue';
+import { template } from 'lodash-es';
+
+const { sysOrgCode } = useGlobSetting();
+const { hasPermission } = usePermission();
+const globalConfig = inject('globalConfig');
+
+const { handleExportXls } = useMethods();
+
+// import { BorderBox8 as DvBorderBox8 } from '@kjgl77/datav-vue3';
+
+// const echartsOption = {
+//   grid: {
+//     top: '60px',
+//     left: '10px',
+//     right: '25px',
+//     bottom: '5%',
+//     containLabel: true,
+//   },
+//   toolbox: {
+//     feature: {},
+//   },
+// };
+// let alive = ref(true)
+
+type DeviceType = { deviceType: string; deviceName: string; datalist: any[] };
+
+const props = defineProps({
+  pageData: {
+    type: Object,
+    default: () => { },
+  },
+});
+
+const scroll = {
+  y: 360,
+};
+const defSort = {
+  column: 'strinstallpos',
+  order: 'desc',
+};
+const monitorTable = ref();
+const historyTable = ref();
+const alarmHistoryTable = ref();
+const handlerHistoryTable = ref();
+
+const isRefresh = ref(true);
+
+const activeKey = ref('1'); // tab key
+const dataSource = shallowRef([]); // 实时监测数据
+const deviceType = ref(''); // 监测设备类型
+const subStation = ref('');
+const subStationOptions = ref([]);
+// let dataSourceHis = shallowRef([])//历史数据
+
+//历史数据
+async function changeHis(data) {
+  // alive.value = false
+  // nextTick(() => {
+  //   dataSourceHis = data
+  //   alive.value = true
+  // })
+}
+
+async function tabChange(activeKeyVal) {
+  activeKey.value = activeKeyVal;
+  if (activeKey.value != '1') {
+    if (timer != undefined) {
+      clearTimeout(timer);
+      timer = undefined;
     }
+  } else {
+    timer = null;
+    await getMonitor(true);
   }
-
-  // https获取监测数据
-  let timer: null | NodeJS.Timeout = null;
-  function getMonitor(flag?) {
-    if (deviceType.value) {
-      if (timer) timer = null;
-      if (Object.prototype.toString.call(timer) === '[object Null]') {
-        timer = setTimeout(
-          async () => {
-            await getDataSource();
-            if (timer) {
-              getMonitor();
-            }
-          },
-          flag ? 0 : 1000
-        );
-      }
+}
+
+// https获取监测数据
+let timer: null | NodeJS.Timeout = null;
+function getMonitor(flag?) {
+  if (deviceType.value) {
+    if (timer) timer = null;
+    if (Object.prototype.toString.call(timer) === '[object Null]') {
+      timer = setTimeout(
+        async () => {
+          await getDataSource();
+          if (timer) {
+            getMonitor();
+          }
+        },
+        flag ? 0 : 1000
+      );
     }
   }
-
-  async function getDataSource() {
-    const formData = monitorTable.value.getForm();
-    const res = await list({ devicetype: deviceType.value, filterParams: { ...formData.getFieldsValue() } });
-    if (res.msgTxt.length > 0) {
-      dataSource.value = [];
-      let dataArr = res.msgTxt[0].datalist || [];
-      dataArr.filter((data: any) => {
-        const readData = data.readData;
-        return Object.assign(data, readData);
-      });
-      if (deviceType.value == 'safetymonitor') {
-        // 如果是安全监控的数据时需要过滤常见设备数据,根据设定的常用安全监控字典去匹配
-        let dictCodes = getDictItemsByCode('safetynormal');
-        const searchForm = formData.getFieldsValue();
-
-        if (searchForm && searchForm['dataTypeName'] && dictCodes && dictCodes.length > 0) {
-          const tempData = [];
-          const tempData1 = [];
-          for (let i = 0; i < dataArr.length; i++) {
-            const item = dataArr[i];
-            let flag = false;
-            for (let i = 0; i < dictCodes.length; i++) {
-              const dict = dictCodes[i];
-              if (dict['value'] == item['dataTypeName']) {
-                flag = true;
-              }
-            }
-            if (flag) {
-              tempData.push(item);
-            } else {
-              tempData1.push(item);
+}
+
+async function getDataSource() {
+  const formData = monitorTable.value.getForm();
+  const res = await list({ devicetype: deviceType.value, filterParams: { ...formData.getFieldsValue() } });
+  if (res.msgTxt.length > 0) {
+    dataSource.value = [];
+    let dataArr = res.msgTxt[0].datalist || [];
+    dataArr.filter((data: any) => {
+      const readData = data.readData;
+      return Object.assign(data, readData);
+    });
+    if (deviceType.value == 'safetymonitor') {
+      // 如果是安全监控的数据时需要过滤常见设备数据,根据设定的常用安全监控字典去匹配
+      let dictCodes = getDictItemsByCode('safetynormal');
+      const searchForm = formData.getFieldsValue();
+
+      if (searchForm && searchForm['dataTypeName'] && dictCodes && dictCodes.length > 0) {
+        const tempData = [];
+        const tempData1 = [];
+        for (let i = 0; i < dataArr.length; i++) {
+          const item = dataArr[i];
+          let flag = false;
+          for (let i = 0; i < dictCodes.length; i++) {
+            const dict = dictCodes[i];
+            if (dict['value'] == item['dataTypeName']) {
+              flag = true;
             }
           }
-          if (sysOrgCode == 'zjtzqctmk' || hasPermission('btn:noGb')) {
-            dataSource.value = [...tempData, ...tempData1];
+          if (flag) {
+            tempData.push(item);
           } else {
-            dataSource.value = [...tempData];
+            tempData1.push(item);
           }
+        }
+        if (sysOrgCode == 'zjtzqctmk' || hasPermission('btn:noGb')) {
+          dataSource.value = [...tempData, ...tempData1];
         } else {
-          dataSource.value = dataArr;
+          dataSource.value = [...tempData];
         }
       } else {
         dataSource.value = dataArr;
       }
     } else {
-      dataSource.value = [];
+      dataSource.value = dataArr;
     }
+  } else {
+    dataSource.value = [];
   }
+}
 
-  async function getSubstation() {
-    const list: [] = await subStationList({ monitorparam: 'safetymonitor*' });
-    subStationOptions.value = list;
-    if (list.length > 0) {
-      subStation.value = list[0]['id'];
-    }
+async function getSubstation() {
+  const list: [] = await subStationList({ monitorparam: 'safetymonitor*' });
+  subStationOptions.value = list;
+  if (list.length > 0) {
+    subStation.value = list[0]['id'];
   }
-
-  function exportData() {
-    handleExportXls('安全监控导出', getExportUrl);
+}
+
+function exportData() {
+  handleExportXls('安全监控导出', getExportUrl);
+}
+function goSystem() {
+  if (VENT_PARAM['safetyCrlPlatformUrl']) {
+    window.open(VENT_PARAM['safetyCrlPlatformUrl'], '_blank');
+    return;
   }
-  function goSystem() {
-    if (VENT_PARAM['safetyCrlPlatformUrl']) {
-      window.open(VENT_PARAM['safetyCrlPlatformUrl'], '_blank');
-      return;
-    }
+}
+
+function updateSubstation() {
+  if (subStation.value) {
+    initSubStation({ substationID: subStation.value }).then(() => {
+      message.success('分站同步完成!');
+    });
+  } else {
+    message.warning('请选择分站!');
   }
-
-  function updateSubstation() {
-    if (subStation.value) {
-      initSubStation({ substationID: subStation.value }).then(() => {
-        message.success('分站同步完成!');
-      });
-    } else {
-      message.warning('请选择分站!');
+}
+
+onMounted(async () => {
+  const { currentRoute } = useRouter();
+  if (unref(currentRoute)) {
+    const path = unref(currentRoute).path;
+    if (path) {
+      deviceType.value = path.substring(path.lastIndexOf('/') + 1);
     }
+    await getMonitor(true);
+    await getSubstation();
   }
+});
 
-  onMounted(async () => {
-    const { currentRoute } = useRouter();
-    if (unref(currentRoute)) {
-      const path = unref(currentRoute).path;
-      if (path) {
-        deviceType.value = path.substring(path.lastIndexOf('/') + 1);
-      }
-      await getMonitor(true);
-      await getSubstation();
-    }
-  });
-
-  onUnmounted(() => {
-    if (timer) {
-      clearTimeout(timer);
-    }
-    timer = undefined;
-  });
+onUnmounted(() => {
+  if (timer) {
+    clearTimeout(timer);
+  }
+  timer = undefined;
+});
 </script>
 
 <style lang="less" scoped>
-  @import '/@/design/theme.less';
-  @import '/@/design/vent/modal.less';
-  @ventSpace: zxm;
+@import '/@/design/theme.less';
+@import '/@/design/vent/modal.less';
+@ventSpace: zxm;
+
+.device-box {
+  width: 100%;
+  height: calc(100% - 100px);
+  padding-bottom: 10px;
+  margin-top: 20px;
+  display: flex;
+  justify-content: center;
+
+  .tabs-box {
+    width: calc(100% - 12px) !important;
+    height: 100% !important;
+    bottom: 3px !important;
+  }
 
-  .device-box {
-    width: 100%;
-    height: calc(100% - 100px);
-    padding-bottom: 10px;
-    margin-top: 20px;
+  .device-button-group {
+    position: absolute;
+    top: -30px;
     display: flex;
-    justify-content: center;
-
-    .tabs-box {
-      width: calc(100% - 12px) !important;
-      height: 100% !important;
-      bottom: 3px !important;
-    }
+    width: 100%;
 
-    .device-button-group {
-      position: absolute;
-      top: -30px;
+    .device-button {
+      height: 26px;
+      padding: 0 20px;
+      background: linear-gradient(45deg, #04e6fb55, #0c5cab55);
+      clip-path: polygon(10px 0, 0 50%, 10px 100%, 100% 100%, calc(100% - 10px) 50%, 100% 0);
       display: flex;
-      width: 100%;
-
-      .device-button {
-        height: 26px;
-        padding: 0 20px;
-        background: linear-gradient(45deg, #04e6fb55, #0c5cab55);
-        clip-path: polygon(10px 0, 0 50%, 10px 100%, 100% 100%, calc(100% - 10px) 50%, 100% 0);
-        display: flex;
-        justify-content: center;
-        align-items: center;
-        color: #fff;
-        position: relative;
-        cursor: pointer;
-
-        &:nth-child(1) {
-          left: calc(-6px * 1);
-        }
-
-        &:nth-child(2) {
-          left: calc(-6px * 2);
-        }
+      justify-content: center;
+      align-items: center;
+      color: #fff;
+      position: relative;
+      cursor: pointer;
 
-        &:nth-child(3) {
-          left: calc(-6px * 3);
-        }
+      &:nth-child(1) {
+        left: calc(-6px * 1);
+      }
 
-        &:nth-child(4) {
-          left: calc(-6px * 4);
-        }
+      &:nth-child(2) {
+        left: calc(-6px * 2);
+      }
 
-        &:nth-child(5) {
-          left: calc(-6px * 5);
-        }
+      &:nth-child(3) {
+        left: calc(-6px * 3);
+      }
 
-        &:nth-child(6) {
-          left: calc(-6px * 6);
-        }
+      &:nth-child(4) {
+        left: calc(-6px * 4);
+      }
 
-        &:nth-child(7) {
-          left: calc(-6px * 7);
-        }
+      &:nth-child(5) {
+        left: calc(-6px * 5);
+      }
 
-        &:nth-child(8) {
-          left: calc(-6px * 8);
-        }
+      &:nth-child(6) {
+        left: calc(-6px * 6);
+      }
 
-        &:nth-child(9) {
-          left: calc(-6px * 9);
-        }
+      &:nth-child(7) {
+        left: calc(-6px * 7);
+      }
 
-        &:nth-child(10) {
-          left: calc(-6px * 10);
-        }
+      &:nth-child(8) {
+        left: calc(-6px * 8);
+      }
 
-        &:nth-child(11) {
-          left: calc(-6px * 11);
-        }
+      &:nth-child(9) {
+        left: calc(-6px * 9);
+      }
 
-        &:nth-child(12) {
-          left: calc(-6px * 12);
-        }
+      &:nth-child(10) {
+        left: calc(-6px * 10);
+      }
 
-        &:nth-child(13) {
-          left: calc(-6px * 13);
-        }
+      &:nth-child(11) {
+        left: calc(-6px * 11);
+      }
 
-        &:nth-child(14) {
-          left: calc(-6px * 14);
-        }
+      &:nth-child(12) {
+        left: calc(-6px * 12);
+      }
 
-        &:nth-child(15) {
-          left: calc(-6px * 15);
-        }
+      &:nth-child(13) {
+        left: calc(-6px * 13);
+      }
 
-        &:first-child {
-          clip-path: polygon(0 0, 10px 50%, 0 100%, 100% 100%, calc(100% - 10px) 50%, 100% 0);
-        }
+      &:nth-child(14) {
+        left: calc(-6px * 14);
       }
 
-      .device-active {
-        background: linear-gradient(45deg, #04e6fb, #0c5cab);
+      &:nth-child(15) {
+        left: calc(-6px * 15);
+      }
 
-        &::before {
-          border-color: #0efcff;
-          box-shadow: 1px 1px 3px 1px #0efcff inset;
-        }
+      &:first-child {
+        clip-path: polygon(0 0, 10px 50%, 0 100%, 100% 100%, calc(100% - 10px) 50%, 100% 0);
       }
     }
 
-    .enter-detail {
-      color: #fff;
-      cursor: pointer;
-      position: absolute;
-      right: 120px;
-      top: -6px;
-      padding: 5px;
-      border-radius: 5px;
-      margin-left: 8px;
-      margin-right: 8px;
-      width: auto;
-      height: 33px !important;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      color: #fff;
-      padding: 5px 15px 5px 15px;
-      cursor: pointer;
-
-      &:hover {
-        background: var(--vent-modal-bg2);
-      }
+    .device-active {
+      background: linear-gradient(45deg, #04e6fb, #0c5cab);
 
       &::before {
-        width: calc(100% - 6px);
-        height: 27px;
-        content: '';
-        position: absolute;
-        top: 3px;
-        right: 0;
-        left: 3px;
-        bottom: 0;
-        z-index: -1;
-        border-radius: inherit;
-        /*important*/
-        background: linear-gradient(#1fa6cb, #127cb5);
+        border-color: #0efcff;
+        box-shadow: 1px 1px 3px 1px #0efcff inset;
       }
     }
   }
-  .right-btn-group {
-    position: absolute;
+
+  .enter-detail {
     color: #fff;
-    right: 20px;
+    cursor: pointer;
+    position: absolute;
+    right: 120px;
+    top: -6px;
+    padding: 5px;
+    border-radius: 5px;
+    margin-left: 8px;
+    margin-right: 8px;
+    width: auto;
+    height: 33px !important;
     display: flex;
-    .export-btn {
-    }
-    .update-btn {
-      margin-right: 10px;
-      display: flex;
-      align-items: center;
-      .btn {
-        background: var(--vent-modal-bg2);
-      }
-    }
-    .btn {
-      padding: 8px 20px;
-      position: relative;
-      border-radius: 2px;
-      color: #fff;
-      width: fit-content;
-      cursor: pointer;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    padding: 5px 15px 5px 15px;
+    cursor: pointer;
 
-      &::before {
-        position: absolute;
-        display: block;
-        content: '';
-        width: calc(100% - 4px);
-        height: calc(100% - 4px);
-        top: 2px;
-        left: 2px;
-        border-radius: 2px;
-        z-index: -1;
-      }
+    &:hover {
+      background: var(--vent-modal-bg2);
     }
 
-    .btn1 {
-      border: 1px solid var(--vent-btn-primary-border-color);
+    &::before {
+      width: calc(100% - 6px);
+      height: 27px;
+      content: '';
+      position: absolute;
+      top: 3px;
+      right: 0;
+      left: 3px;
+      bottom: 0;
+      z-index: -1;
+      border-radius: inherit;
+      /*important*/
+      background: linear-gradient(#1fa6cb, #127cb5);
+    }
+  }
+}
 
-      &::before {
-        background-image: linear-gradient(#2effee92, #0cb1d592);
-      }
+.right-btn-group {
+  position: absolute;
+  color: #fff;
+  right: 20px;
+  display: flex;
 
-      &:hover {
-        border: 1px solid #5cfaffaa;
+  .export-btn {}
 
-        &::before {
-          background-image: linear-gradient(#2effee72, #0cb1d572);
-        }
-      }
-    }
-    .btn2 {
-      border: 1px solid var(--vent-btn-primary-border-color);
-      background-image: linear-gradient(#2effee92, #0cb1d592);
+  .update-btn {
+    margin-right: 10px;
+    display: flex;
+    align-items: center;
 
-      &:hover {
-        border: 1px solid var(--vent-btn-primary-focus-color);
-        background-image: linear-gradient(#0cb0d554, rgba(46, 255, 238, 0.342));
-      }
+    .btn {
+      background: var(--vent-modal-bg2);
     }
   }
 
-  :deep(.@{ventSpace}-tabs-tabpane-active) {
-    height: 100%;
-    border: 1px solid var(--vent-device-manager-box-border);
+  .btn {
+    padding: 8px 20px;
+    position: relative;
     border-radius: 2px;
-    -webkit-backdrop-filter: blur(8px);
-    box-shadow: 0 0 20px #44b4ff33 inset;
-    background-color: var(--vent-device-manager-box-bg);
-    overflow-y: auto;
+    color: #fff;
+    width: fit-content;
+    cursor: pointer;
+
+    &::before {
+      position: absolute;
+      display: block;
+      content: '';
+      width: calc(100% - 4px);
+      height: calc(100% - 4px);
+      top: 2px;
+      left: 2px;
+      border-radius: 2px;
+      z-index: -1;
+    }
   }
 
-  :deep(.@{ventSpace}-tabs-card) {
-    .@{ventSpace}-tabs-tab {
-      background: var(--vent-modal-bg2);
-      border-color: var(--vent-btn-primary-border-color);
-      border-radius: 0%;
+  .btn1 {
+    border: 1px solid var(--vent-btn-primary-border-color);
 
-      &:hover {
-        color: #64d5ff;
-      }
+    &::before {
+      background-image: linear-gradient(#2effee92, #0cb1d592);
     }
 
-    .@{ventSpace}-tabs-content {
-      height: 100% !important;
-    }
+    &:hover {
+      border: 1px solid #5cfaffaa;
 
-    .@{ventSpace}-tabs-tab.@{ventSpace}-tabs-tab-active .@{ventSpace}-tabs-tab-btn {
-      color: var(--vent-font-action-link);
+      &::before {
+        background-image: linear-gradient(#2effee72, #0cb1d572);
+      }
     }
+  }
+
+  .btn2 {
+    border: 1px solid var(--vent-btn-primary-border-color);
+    background-image: linear-gradient(#2effee92, #0cb1d592);
 
-    .@{ventSpace}-tabs-nav::before {
-      border-color: var(--vent-btn-primary-border-color);
+    &:hover {
+      border: 1px solid var(--vent-btn-primary-focus-color);
+      background-image: linear-gradient(#0cb0d554, rgba(46, 255, 238, 0.342));
     }
+  }
+}
+
+:deep(.@{ventSpace}-tabs-tabpane-active) {
+  height: 100%;
+  border: 1px solid var(--vent-device-manager-box-border);
+  border-radius: 2px;
+  -webkit-backdrop-filter: blur(8px);
+  box-shadow: 0 0 20px #44b4ff33 inset;
+  background-color: var(--vent-device-manager-box-bg);
+  overflow-y: auto;
+}
+
+:deep(.@{ventSpace}-tabs-card) {
+  .@{ventSpace}-tabs-tab {
+    background: var(--vent-modal-bg2);
+    border-color: var(--vent-btn-primary-border-color);
+    border-radius: 0%;
+
+    &:hover {
+      color: #64d5ff;
+    }
+  }
 
-    .@{ventSpace}-picker,
-    .@{ventSpace}-select-selector {
-      width: 100%;
-      background: #00000017 !important;
-      border: 1px solid @vent-form-item-border !important;
+  .@{ventSpace}-tabs-content {
+    height: 100% !important;
+  }
 
-      input,
-      .@{ventSpace}-select-selection-item,
-      .@{ventSpace}-picker-suffix {
-        color: #fff !important;
-      }
+  .@{ventSpace}-tabs-tab.@{ventSpace}-tabs-tab-active .@{ventSpace}-tabs-tab-btn {
+    color: var(--vent-font-action-link);
+  }
 
-      .@{ventSpace}-select-selection-placeholder {
-        color: #b7b7b7 !important;
-      }
-    }
+  .@{ventSpace}-tabs-nav::before {
+    border-color: var(--vent-btn-primary-border-color);
+  }
+
+  .@{ventSpace}-picker,
+  .@{ventSpace}-select-selector {
+    width: 100%;
+    background: #00000017 !important;
+    border: 1px solid @vent-form-item-border !important;
 
-    .@{ventSpace}-pagination-next,
-    .action,
-    .@{ventSpace}-select-arrow,
-    .@{ventSpace}-picker-separator {
+    input,
+    .@{ventSpace}-select-selection-item,
+    .@{ventSpace}-picker-suffix {
       color: #fff !important;
     }
+
+    .@{ventSpace}-select-selection-placeholder {
+      color: #b7b7b7 !important;
+    }
+  }
+
+  .@{ventSpace}-pagination-next,
+  .action,
+  .@{ventSpace}-select-arrow,
+  .@{ventSpace}-picker-separator {
+    color: #fff !important;
   }
+}
 </style>

+ 7 - 0
src/views/vent/monitorManager/safetyMonitor/safety.api.ts

@@ -10,6 +10,7 @@ enum Api {
   export = '/safety/reportInfo/expComReport?tempName=aqjk',
   subStationList = '/safety/ventanalySubStation/alllist',
   initSubStation = '/monitor/initKafkaDeviceInfo',
+  warnList='/ventanaly-device/monitor/securityWarning/list'
 }
 /**
  * 列表接口
@@ -37,3 +38,9 @@ export const getDeviceTypeList = (params) => defHttp.get({ url: Api.deviceTypeLi
 export const itemList = (params) => defHttp.get({ url: Api.itemList, params });
 
 export const getExportUrl = Api.export;
+/**
+ * 预警历史
+ * @param params
+ */
+export const warnList = (params) => defHttp.get({ url: Api.warnList, params });
+

+ 80 - 0
src/views/vent/monitorManager/safetyMonitor/safety.data.ts

@@ -1,4 +1,84 @@
 import { safetyDeviceList } from './safety.api';
+import { BasicColumn } from '/@/components/Table';
+import {reactive} from 'vue'
+
+export const warnHistoryColumns: BasicColumn[] = [
+  {
+    title: '序号',
+    width: 60,
+    align: 'center',
+    customRender: ({ index }: { index: number }) => `${index + 1}`
+  },
+  {
+    title: '安装位置',
+    dataIndex: 'installLocation',
+    key: 'installLocation',
+    align: 'center',
+    ellipsis: true,
+  },
+  {
+    title: '报警类型',
+    dataIndex: 'exceptionTypeC',
+    key: 'exceptionTypeC',
+    align: 'center',
+    ellipsis: true,
+  },
+  {
+    title: '报警原因',
+    dataIndex: 'exceptionReason',
+    key: 'exceptionReason',
+    align: 'center',
+    ellipsis: true,
+  },
+  {
+    title: '开始报警时间',
+    dataIndex: 'exceptionStartTime',
+    key: 'exceptionStartTime',
+    align: 'center',
+    ellipsis: true,
+  },
+  {
+    title: '结束报警时间',
+    dataIndex: 'exceptionEndTime',
+    key: 'exceptionEndTime',
+    align: 'center',
+    ellipsis: true,
+  },
+  {
+    title: '是否完成',
+    dataIndex: 'isCompletedC',
+    key: 'isCompletedC',
+    align: 'center',
+    ellipsis: true,
+  },
+
+]
+
+export let exceptionTypeList = [
+  { label: '超限报警', value: '001' },
+  { label: '断电报警', value: '002' },
+  { label: '馈电异常', value: '003' },
+  { label: '传感器断线', value: '004' },
+  { label: '基站断电', value: '005' },
+  { label: '基站不通', value: '006' },
+  { label: '标校', value: '007' },
+  { label: '超量程', value: '008' },
+]
+export let isCompletedList = [
+  { label: '是', value: 1 },
+  { label: '否', value: 0 },
+]
+
+//分页参数配置
+export let pagination = reactive({
+  current: 1, // 当前页码
+  pageSize: 10, // 每页显示条数
+  total: 0, // 总条目数,后端返回
+  // showTotal: (total, range) => `${range[0]}-${range[1]} 条,总共 ${total} 条`, // 分页右下角显示信息
+  showSizeChanger: true, // 是否可改变每页显示条数
+  pageSizeOptions: ['10', '20', '50'], // 可选的每页显示条数
+})
+
 
 export const chartsColumns = (deviceType) => {
   if (deviceType === '') {

+ 1 - 1
src/views/vent/performance/approvalPend/index.vue

@@ -19,7 +19,7 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, reactive, watch, nextTick, defineProps, onMounted, provide, toRaw } from 'vue';
+  import { ref, reactive, watch, nextTick, onMounted, provide, toRaw } from 'vue';
   import HistorySp from '../comment/HistorySp.vue';
   import DeviceModal from '../comment/DeviceModal.vue';
   import CADModal from '../comment/CADModal.vue';