Explorar el Código

完成scoket连接,作业日志和剩余内容接口对接更新

wyn hace 3 meses
padre
commit
0bafefa5f3
Se han modificado 3 ficheros con 428 adiciones y 155 borrados
  1. 5 0
      src/api/job/index.ts
  2. 145 0
      src/utils/webSocket.ts
  3. 278 155
      src/views/jobTicket/job/JobMonitor.vue

+ 5 - 0
src/api/job/index.ts

@@ -64,3 +64,8 @@ export const deleteJobTicketList = async (ids: number) => {
     url: '/iscs/job-ticket/deleteJobTicketList?ids='+ids,
   })
 }
+
+//获得作业日志
+export const getTicketOperLogPage = async (data)=>{
+  return await request.get({url:'/iscs/ticket-oper-log/getTicketOperLogPage', params: data })
+}

+ 145 - 0
src/utils/webSocket.ts

@@ -0,0 +1,145 @@
+// src/utils/websocket.ts
+let wsObj: WebSocket | null = null;
+let wsUrl: string = '';
+let lockReconnect = false;
+let wsCreateHandler: ReturnType<typeof setTimeout> | null = null;
+let messageCallback: ((msg: string) => void) | null = null;
+let errorCallback: ((err: Event) => void) | null = null;
+let sendDatas: Record<string, any> = {};
+
+// WebSocket连接初始化
+export const connectWebsocket = (
+  url: string,
+  agentData: Record<string, any>,
+  successCallback: (msg: string) => void,
+  errCallback: (err: Event) => void
+) => {
+  wsUrl = url;
+  messageCallback = successCallback;
+  errorCallback = errCallback;
+  sendDatas = agentData;
+  createWebSocket();
+};
+
+// 手动关闭WebSocket
+export const closeWebsocket = () => {
+  if (wsObj) {
+    log('手动关闭WebSocket连接');
+    wsObj.close();
+    lockReconnect = true;
+    wsCreateHandler && clearTimeout(wsCreateHandler);
+    heartCheck.stop();
+  }
+};
+
+// 发送消息
+export const sendMsg = (value: any) => {
+  if (wsObj && wsObj.readyState === WebSocket.OPEN) {
+    wsObj.send(JSON.stringify(value));
+  } else {
+    log('WebSocket 未连接,消息发送失败');
+  }
+};
+
+// 创建 WebSocket 实例
+const createWebSocket = () => {
+  if (typeof WebSocket === 'undefined') {
+    log('当前浏览器不支持 WebSocket');
+    return;
+  }
+
+  try {
+    wsObj = new WebSocket(wsUrl);
+    initWsEvents();
+  } catch (e) {
+    log('WebSocket 创建异常,尝试重连');
+    reconnect();
+  }
+};
+
+// 绑定事件
+const initWsEvents = () => {
+  if (!wsObj) return;
+
+  wsObj.onopen = (event) => {
+    log('WebSocket连接成功');
+    if (wsObj?.readyState === WebSocket.OPEN) {
+      wsObj.send(JSON.stringify(sendDatas));
+    }
+    heartCheck.start();
+  };
+
+  wsObj.onmessage = (event) => {
+    heartCheck.reset();
+    log('接收到服务器消息:', event.data);
+    messageCallback?.(event.data);
+  };
+
+  wsObj.onclose = (event) => {
+    log('WebSocket连接关闭:', event);
+    if (event.code !== 1000) {
+      errorCallback?.(event);
+      reconnect();
+    }
+  };
+
+  wsObj.onerror = (event) => {
+    log('WebSocket连接错误:', event);
+    errorCallback?.(event);
+    reconnect();
+  };
+};
+
+// 封装 console.log
+const log = (...args: any[]) => {
+  console.log('[WebSocket]', ...args);
+};
+
+// 心跳机制
+const heartCheck = {
+  timeout: 60 * 1000,
+  timeoutObj: null as ReturnType<typeof setTimeout> | null,
+  serverTimeoutObj: null as ReturnType<typeof setTimeout> | null,
+
+  reset() {
+    this.timeoutObj && clearTimeout(this.timeoutObj);
+    this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
+    this.start();
+  },
+
+  stop() {
+    this.timeoutObj && clearTimeout(this.timeoutObj);
+    this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
+  },
+
+  start() {
+    this.timeoutObj = setTimeout(() => {
+      try {
+        log('发送心跳包');
+        const ping = { content: '心跳检测' };
+        wsObj?.send(JSON.stringify(ping));
+      } catch (err) {
+        log('发送心跳失败,尝试重连');
+        reconnect();
+      }
+
+      this.serverTimeoutObj = setTimeout(() => {
+        log('心跳无响应,尝试重连');
+        reconnect();
+      }, this.timeout);
+    }, this.timeout);
+  },
+};
+
+// 重连逻辑
+const reconnect = () => {
+  if (lockReconnect) return;
+  lockReconnect = true;
+
+  wsCreateHandler && clearTimeout(wsCreateHandler);
+  wsCreateHandler = setTimeout(() => {
+    log('执行重连...');
+    createWebSocket();
+    lockReconnect = false;
+  }, 60000); // 1分钟重连
+};

+ 278 - 155
src/views/jobTicket/job/JobMonitor.vue

@@ -10,15 +10,34 @@
         >
       </div>
       <div class="basicContent">
-        <p>作业名称: <span>{{JobForm.ticketName}}</span> </p>
-        <p>作业区域: <span>{{JobForm.workstationName}}</span> </p>
-        <p>工艺设备: <span>{{JobForm.machineryName}}</span> </p>
-        <p>作业类型: <span>{{ ticketTypeLabel }}</span></p>
-        <p>作业状态: <span>{{ ticketStatusLabel }}</span></p>
-        <p>开始时间: <span>{{JobForm.ticketStartTime}}</span> </p>
-        <p>结束时间: <span>{{JobForm.ticketEndTime || '-'}}  </span> </p>
-        <p>当前步骤: <span class="specialText">{{ currentStepTitle }}</span> </p>
-        <p>最新日志: <span>待更新</span> </p>
+        <p
+          >作业名称: <span>{{ JobForm.ticketName }}</span></p
+        >
+        <p
+          >作业区域: <span>{{ JobForm.workstationName }}</span></p
+        >
+        <p
+          >工艺设备: <span>{{ JobForm.machineryName }}</span></p
+        >
+        <p
+          >作业类型: <span>{{ ticketTypeLabel }}</span></p
+        >
+        <p
+          >作业状态: <span>{{ ticketStatusLabel }}</span></p
+        >
+        <p
+          >开始时间: <span>{{ JobForm.ticketStartTime }}</span></p
+        >
+        <p
+          >结束时间: <span>{{ JobForm.ticketEndTime || '-' }} </span></p
+        >
+        <p
+          >当前步骤: <span class="specialText">{{ currentStepTitle }}</span></p
+        >
+        <p>最新日志:
+          <span v-if="latestLog" class="hh" v-html="renderLogContent(latestLog.operationContent)"></span>
+        </p>
+
       </div>
     </div>
   </ContentWrap>
@@ -32,9 +51,16 @@
       </div>
       <div class="processDetail" ref="scrollContainer">
         <!-- VueFlow 主画布 -->
-        <VueFlow style="width: 100%; height: 300px" :min-zoom="1" :max-zoom="1" :default-zoom="1" :nodes="nodes" :edges="edges">
-          <template #node-default="{ id, data }" >
-            <div class="custom-node" :id="id" >
+        <VueFlow
+          style="width: 100%; height: 300px"
+          :min-zoom="1"
+          :max-zoom="1"
+          :default-zoom="1"
+          :nodes="nodes"
+          :edges="edges"
+        >
+          <template #node-default="{ id, data }">
+            <div class="custom-node" :id="id">
               <div class="node-content">
                 <!-- 图标显示 -->
                 <div style="font-size: 30px">
@@ -129,14 +155,14 @@
             </div>
           </template>
           <div class="jobMemberBox">
-            <div
-              class="group-box"
-              v-for="(group, index) in jobGroupList"
-              :key="group.id"
-            >
+            <div class="group-box" v-for="(group, index) in jobGroupList" :key="group.id">
               <div class="tab-header">
-                <p class="tab-title">{{ group.groupName }}(待锁定:{{group.
-                  waitLock}} 已锁定:{{group.locked}} 已解锁:{{group.unlocked}})</p>
+                <p class="tab-title"
+                  >{{ group.groupName }}(待锁定:{{ group.waitLock }} 已锁定:{{
+                    group.locked
+                  }}
+                  已解锁:{{ group.unlocked }})</p
+                >
               </div>
               <!-- 表格数据 -->
               <div class="tableCon">
@@ -144,22 +170,15 @@
                   <el-radio-button value="fixed">常规</el-radio-button>
                   <el-radio-button value="auto">扩展</el-radio-button>
                 </el-radio-group>
-                <el-table
-                  :data="group.ticketPointsRespVOList"
-                  :table-layout="tableLayouts[index]"
-                >
+                <el-table :data="group.ticketPointsRespVOList" :table-layout="tableLayouts[index]">
                   <el-table-column prop="pointName" label="隔离点" />
                   <el-table-column prop="ability" label="作用" />
                   <el-table-column prop="lockUserName" label="锁定人" />
-                  <el-table-column prop="pointStatus" label="锁定状态" >
+                  <el-table-column prop="pointStatus" label="锁定状态">
                     <template #default="scope">
-                      <dict-tag
-                        :type="DICT_TYPE.POINT_STATUS"
-                        :value="scope.row.pointStatus"
-                      />
+                      <dict-tag :type="DICT_TYPE.POINT_STATUS" :value="scope.row.pointStatus" />
                     </template>
                   </el-table-column>
-
                 </el-table>
               </div>
             </div>
@@ -180,8 +199,8 @@
                 <span class="tab-title">待共锁({{ waitLockUsers.length }})</span>
               </div>
               <div class="user">
-                <div class="userItem" v-for="user in waitLockUsers" :key="user.userId" >
-                  <img src="@/assets/images/icon_co-lock.png" alt=""/>
+                <div class="userItem" v-for="user in waitLockUsers" :key="user.userId">
+                  <img :src="user.avatar" alt="" />
                   <p>{{ user.userName }}</p>
                 </div>
               </div>
@@ -196,7 +215,7 @@
               </div>
               <div class="user">
                 <div class="userItem" v-for="user in lockedUsers" :key="user.userId">
-                  <img src="@/assets/images/icon_co-lock.png" alt=""/>
+                  <img :src="user.avatar" alt="" />
                   <p>{{ user.userName }}</p>
                 </div>
               </div>
@@ -211,7 +230,7 @@
               </div>
               <div class="user">
                 <div class="userItem" v-for="user in unlockPendingUsers" :key="user.userId">
-                  <img src="@/assets/images/icon_co-lock.png" alt=""/>
+                  <img :src="user.avatar" alt="" />
                   <p>{{ user.userName }} </p>
                 </div>
               </div>
@@ -239,8 +258,7 @@
                 :indeterminate="isIndeterminate"
                 :checked="checkAlljoblog"
                 @change="handleCheckAllChange"
-                style="margin:3px 25px;font-size: 16px"
-
+                style="margin: 0 25px; font-size: 16px"
               >
                 全部
               </el-checkbox>
@@ -266,22 +284,11 @@
       </el-tabs>
     </div>
   </ContentWrap>
-<!--  作业日志弹框-->
-  <el-dialog
-    v-model="joblogDialogVisible"
-    title="Warning"
-    width="500"
-    align-center
-  >
-    <span>Open the dialog from the center from the screen</span>
-    <template #footer>
-      <div class="dialog-footer">
-        <el-button @click="joblogDialogVisible = false">Cancel</el-button>
-        <el-button type="primary" @click="joblogDialogVisible = false">
-          Confirm
-        </el-button>
-      </div>
-    </template>
+  <!--  作业日志弹框-->
+  <el-dialog v-model="JoblogDialogVisible" title="新日志提醒" width="500" align-center>
+    <p>
+      <span v-html="renderLogContent(dialogLog.operationContent)"></span>
+    </p>
   </el-dialog>
 </template>
 
@@ -294,6 +301,9 @@ import * as JobApi from '@/api/job/index'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import type { TableInstance } from 'element-plus'
 import { CaretRight } from '@element-plus/icons-vue'
+import { getTicketOperLogPage } from '@/api/job/index'
+import { connectWebsocket, closeWebsocket } from '@/utils/webSocket'
+import { getIsSystemAttributeByKey } from '@/api/basic/configuration/index'
 
 const JobForm = reactive({
   createTime: null,
@@ -317,7 +327,8 @@ const JobForm = reactive({
   ticketGroupList: null,
   ticketPointsList: null,
   ticketStepList: null,
-  ticketUserList: null
+  ticketUserList: null,
+  ticketOperLogList:null
 })
 const allGroups = ref<any[]>([]) //获取所有分组
 const groupList = ref([]) //获取当前sopId的分组
@@ -329,30 +340,29 @@ const jobStatusOptions = ref([])
 const currentStepTitle = ref('') // 当前步骤名称
 
 // 表格数据
-const jobGroupList=ref([]) //获取作业分组信息
+const jobGroupList = ref([]) //获取作业分组信息
 const ticketPointsList = ref([]) // 隔离点数据
 const tableLayouts = ref([]) // 每组表格的布局
 
 // 人员共锁
 const jobclockerList = ref([])
-const isShowclocker=ref(true)  //是否展示人员共锁
+const isShowclocker = ref(true) //是否展示人员共锁
 const waitLockUsers = ref([])
 const lockedUsers = ref([])
 const unlockPendingUsers = ref([])
 
 // 作业日志
-const joblogList = ref([])//原始作业日志数据列表
+const joblogList = ref([]) //原始作业日志数据列表
 const filteredJoblogList = ref([]) // 过滤后日志
 //   作业日志底部查询
-const isIndeterminate= ref(false)
+const isIndeterminate = ref(false)
 const checkedJoblogs = ref([])
-const jobLogtypes= ref([])
-const joblogsOptions= ref([]) // 只存所有 dictValue,用于全选逻辑,
-const checkAlljoblog =ref(true)
-const JoblogdialogVisible =ref(false)
-const dialogLog= ref('')
-const isJoblogVisible= ref(false)
-const joblogDialogVisible= ref(false)
+const jobLogtypes = ref([])
+const joblogsOptions = ref([]) // 只存所有 dictValue,用于全选逻辑,
+const checkAlljoblog = ref(true)
+const dialogLog = ref('')
+const isJoblogVisible = ref(false)
+const JoblogDialogVisible = ref(false)
 
 
 // 初始化
@@ -362,25 +372,33 @@ onMounted(async () => {
   jobTypeOptions.value = await getIntDictOptions(DICT_TYPE.TICKET_TYPE)
   jobStatusOptions.value = await getIntDictOptions(DICT_TYPE.TICKET_STATUS)
   await fetchAllGroupsAndPoints() //获取所有分组和点位 来渲染SOP的首页
-  await onModeContent()  //作业流程
+  await onModeContent() //作业流程
   window.addEventListener('resize', onResize)
   onResize()
+  getWebSocket()
+
 })
 onBeforeUnmount(() => {
   window.removeEventListener('resize', onResize)
+  closeWebsocket()
 })
 console.log(route.query.id, '是否传递成功')
 
-// 基础信息
+// 基础信息作业类型
 const ticketTypeLabel = computed(() => {
-  const match = jobTypeOptions.value.find(item => item.value == JobForm.ticketType)
+  const match = jobTypeOptions.value.find((item) => item.value == JobForm.ticketType)
   return match ? match.label : JobForm.ticketType
 })
-
+// 基础信息作业状态
 const ticketStatusLabel = computed(() => {
-  const match = jobStatusOptions.value.find(item => item.value == JobForm.ticketStatus)
+  const match = jobStatusOptions.value.find((item) => item.value == JobForm.ticketStatus)
   return match ? match.label : JobForm.ticketStatus
 })
+// 基础信息最新日志
+const latestLog = computed(() => {
+  if (!joblogList.value || joblogList.value.length === 0) return null
+  return joblogList.value.reduce((max, item) => item.id > max.id ? item : max)
+})
 
 // 初始化详情数据
 const getDetail = async () => {
@@ -390,18 +408,28 @@ const getDetail = async () => {
   // 点位锁定
   jobGroupList.value = JobForm.ticketGroupList
   ticketPointsList.value = JobForm.ticketGroupList.ticketPointsRespVOList
-  console.log(JobData,'数据有哪些',ticketPointsList.value,'隔离点数据',jobGroupList.value,'分组信息')
+
+  console.log(
+    JobData,
+    '数据有哪些',
+    ticketPointsList.value,
+    '隔离点数据',
+    jobGroupList.value,
+    '分组信息'
+  )
   // 初始化每个表格的布局设置为 'fixed'
   tableLayouts.value = jobGroupList.value.map(() => 'fixed')
-
-//   人员共锁
+  //   人员共锁
   jobclockerList.value = JobForm.ticketUserList
   // 判断是否存在共锁人
-  isShowclocker.value = jobclockerList.value.some(item => item.userRole === 'jtcolocker')
+  isShowclocker.value = jobclockerList.value.some((item) => item.userRole === 'jtcolocker')
   updateUserGroups() // 分类数据
+//   作业日志
+  joblogList.value = JobForm.ticketOperLogList
+  // ✨强制刷新筛选后的列表
+  filterJoblogs()
 }
 
-
 // 作业流程绘制
 const scrollContainer = ref(null)
 const nodes = ref([]) //储存节点
@@ -414,7 +442,7 @@ const scrollToCurrentNode = () => {
     const container = scrollContainer.value
     if (!container) return
 
-    const currentIndex = nodes.value.findIndex(n => n.data.stepStatus === 0)
+    const currentIndex = nodes.value.findIndex((n) => n.data.stepStatus === 0)
     if (currentIndex === -1) return
 
     const currentNodeId = nodes.value[currentIndex]?.id
@@ -451,10 +479,11 @@ const onResize = () => {
   const isMobile = window.innerWidth <= 600
   const stepsData = [...allSteps.value] // 缓存的完整流程步骤数据
 
-  const currentIndex = stepsData.findIndex(item => item.stepStatus === 0)
-  const filtered = isMobile && currentIndex !== -1
-    ? stepsData.slice(currentIndex) // 小屏只显示当前及后续
-    : stepsData // 大屏恢复全部
+  const currentIndex = stepsData.findIndex((item) => item.stepStatus === 0)
+  const filtered =
+    isMobile && currentIndex !== -1
+      ? stepsData.slice(currentIndex) // 小屏只显示当前及后续
+      : stepsData // 大屏恢复全部
 
   renderNodesFromData(filtered)
   renderEdgesFromData(filtered)
@@ -464,7 +493,6 @@ const onResize = () => {
   })
 }
 
-
 // 模式初始化
 const onModeContent = async (value) => {
   JobForm.modeId = value
@@ -476,14 +504,17 @@ const onModeContent = async (value) => {
   allSteps.value = sortedData // 缓存原始数据
   renderResponsiveSteps()
 }
+
+// 屏幕缩小节点展示
 const renderResponsiveSteps = () => {
   const isMobile = window.innerWidth <= 600
   const stepsData = [...allSteps.value]
 
-  const currentIndex = stepsData.findIndex(item => item.stepStatus === 0)
-  const filtered = isMobile && currentIndex !== -1
-    ? stepsData.slice(currentIndex) // 从当前步骤开始
-    : stepsData // 全部步骤
+  const currentIndex = stepsData.findIndex((item) => item.stepStatus === 0)
+  const filtered =
+    isMobile && currentIndex !== -1
+      ? stepsData.slice(currentIndex) // 从当前步骤开始
+      : stepsData // 全部步骤
 
   renderNodesFromData(filtered)
   renderEdgesFromData(filtered)
@@ -504,7 +535,7 @@ const clearCanvasProperly = async () => {
 const renderNodesFromData = (data) => {
   nodes.value = []
 
-  const firstZeroIndex = data.findIndex(item => item.stepStatus === 0)
+  const firstZeroIndex = data.findIndex((item) => item.stepStatus === 0)
   if (firstZeroIndex !== -1) {
     currentStepTitle.value = data[firstZeroIndex].stepTitleShort || '无标题'
   } else {
@@ -557,7 +588,6 @@ const renderNodesFromData = (data) => {
   nextTick(() => {
     scrollToCurrentNode()
   })
-
 }
 
 // const renderNodesFromData = (data) => {
@@ -657,27 +687,26 @@ const renderEdgesFromData = (data) => {
 
 // 底部tabbar点击事件
 const handleClick = async (tab: TabsPaneContext, event: Event) => {
-  console.log(tab, event)
-  // console.log(activeName.value,'名称')
-  console.log(tab.paneName, '当前点击的 tab 名称')
-
-  if(tab.paneName === 'fourth') {
-    console.log('作业日志')
-    const dictRes= await getIntDictOptions(DICT_TYPE.JOB_LOG_TYPE)
-    console.log(dictRes,'接口调用是否成功')
-    const dicts = dictRes.map(item => ({
+// 当选中作业日志时传递isJoblogVisible给onNewSocketLog判断日志弹框的显示
+  isJoblogVisible.value = (tab.paneName === 'fourth');
+  await getDetail()
+  if (tab.paneName === 'fourth') {
+    // console.log('作业日志')
+    const dictRes = await getIntDictOptions(DICT_TYPE.JOB_LOG_TYPE)
+    console.log(dictRes, '接口调用是否成功')
+    const dicts = dictRes.map((item) => ({
       label: item.label,
-      value: String(item.value), // 确保是字符串类型
-    }));
-
-    jobLogtypes.value = dicts;
-    joblogsOptions.value = dicts.map(item => item.value);
-    checkedJoblogs.value = [...joblogsOptions.value]; // 初始化全选
-    console.log(checkAlljoblog.value,'选中状态');
-    checkAlljoblog.value = true;
-    console.log(checkAlljoblog.value,'是否选中');
-    isIndeterminate.value = false;
-    filterJoblogs();
+      value: String(item.value) // 确保是字符串类型
+    }))
+
+    jobLogtypes.value = dicts
+    joblogsOptions.value = dicts.map((item) => item.value)
+    checkedJoblogs.value = [...joblogsOptions.value] // 初始化全选
+    // console.log(checkAlljoblog.value, '选中状态')
+    checkAlljoblog.value = true
+    // console.log(checkAlljoblog.value, '是否选中')
+    isIndeterminate.value = false
+    filterJoblogs()
   }
 }
 
@@ -766,7 +795,6 @@ const resolvedGroupedPoints = computed(() => {
   return result
 })
 
-
 // 获取某个分组下的隔离点
 // const getPointsByGroupId = (groupId) => {
 //   return ticketPointsList.value.filter((point) => point.groupId === groupId)
@@ -794,23 +822,67 @@ const coLockUsers = computed(() => {
   return JobForm.ticketUserList?.filter((u) => u.userRole === 'jtcolocker') || []
 })
 
-
 // 人员共锁
 const updateUserGroups = () => {
   const allUsers = jobclockerList.value
-  console.log(allUsers,jobclockerList.value,'数据拿到了吗')
-
-  waitLockUsers.value = jobclockerList.value.filter(u => u.userRole == 'jtcolocker' && u.jobStatus == 0)
-  console.log(waitLockUsers.value,'待共锁')
-  lockedUsers.value = allUsers.filter(u => u.userRole === 'jtcolocker' && u.jobStatus == 1)
-  unlockPendingUsers.value = allUsers.filter(u => u.userRole === 'jtcolocker' && u.jobStatus == 2)
+  console.log(allUsers, jobclockerList.value, '数据拿到了吗')
+
+  waitLockUsers.value = jobclockerList.value.filter(
+    (u) => u.userRole == 'jtcolocker' && u.jobStatus == 0
+  )
+  console.log(waitLockUsers.value, '待共锁')
+  lockedUsers.value = allUsers.filter((u) => u.userRole === 'jtcolocker' && u.jobStatus == 1)
+  unlockPendingUsers.value = allUsers.filter((u) => u.userRole === 'jtcolocker' && u.jobStatus == 2)
 }
 
+const getWebSocket = async () => {
+  const code= route.query.id;
+  const address = 'sys.websocket.address'
+  const addressData = await getIsSystemAttributeByKey(address)
+  const url = addressData.sysAttrValue
+  const isLocalDev = window.location.hostname === 'localhost'
+  const baseAddress = isLocalDev
+    ? 'ws://192.168.0.10:48080'
+    : url;
+  connectWebsocket(
+    `${baseAddress}/websocket/jobTicketLog/${code}`,
+    { w: 'S' },
+     async(msg) => {
+      console.log('接收消息:', msg)
+      // const parts = msg.split('operationType=');
+      if (msg !== 'heartbeat') {
+        // 判断是否需要弹窗显示
+         onNewSocketLog(msg);  // 👈传入最新的 WebSocket 消息
+        await getDetail(); // 抽出专用接口函数
+      }
+      // 你的处理逻辑
+    },
+    (err) => {
+      console.error('WebSocket 错误:', err)
+    }
+  )
+}
+//获取作业日志数据
+// const getJobLogs = async () => {
+//   const joblogData = {
+//     titcketId: route.query.id,
+//     current: 1,
+//     size: -1
+//   }
+//   try {
+//     const res = await JobApi.getTicketOperLogPage(joblogData)
+//     joblogList.value = res.list
+//     // ✨强制刷新筛选后的列表
+//     filterJoblogs()
+//     console.log(res.list, '日志数据', joblogList.value, '复制是否成功')
+//   } catch (error) {
+//     console.error('获取日志数据失败', error)
+//   }
+//
+// }
 
-
-// 作用日志
 // 作业日志页面数据展示样式分割
-const renderLogContent=(content)=> {
+const renderLogContent = (content) => {
   if (!content) return ''
   // 第一步:先处理 <url>[label] 为 <img> + label
   content = content.replace(/<([^>]+)>\[([^\]]+)\]/g, (match, url, label) => {
@@ -822,30 +894,72 @@ const renderLogContent=(content)=> {
 }
 
 //   作业日志底部筛选功能
-const handleCheckedJoblogsChange=(val)=> {
-  const checkedCount = val.length;
-  checkAlljoblog.value = checkedCount === joblogsOptions.value.length;
-  isIndeterminate.value = checkedCount > 0 && checkedCount < joblogsOptions.value.length;
+const handleCheckedJoblogsChange = (val) => {
+  const checkedCount = val.length
+  checkAlljoblog.value = checkedCount === joblogsOptions.value.length
+  isIndeterminate.value = checkedCount > 0 && checkedCount < joblogsOptions.value.length
 
- filterJoblogs();
+  filterJoblogs()
 }
-const handleCheckAllChange=(val)=> {
-  checkedJoblogs.value = val ? [...joblogsOptions.value] : [];
-  isIndeterminate.value = false;
-  filterJoblogs();
+const handleCheckAllChange = (val) => {
+  checkedJoblogs.value = val ? [...joblogsOptions.value] : []
+  isIndeterminate.value = false
+  filterJoblogs()
 }
 
 // 实际日志筛选逻辑
 const filterJoblogs = () => {
   if (checkedJoblogs.value.includes('ALL')) {
-    filteredJoblogList.value = joblogList.value;
+    filteredJoblogList.value = joblogList.value
   } else {
-    filteredJoblogList.value = joblogList.value.filter(log =>
+    filteredJoblogList.value = joblogList.value.filter((log) =>
       checkedJoblogs.value.includes(String(log.operationType))
-    );
+    )
   }
 }
+//   作业日志弹框
+const parseLogString=(logStr)=> {
+  const obj = {};
+  const match = logStr.match(/^[^(]+?\((.*)\)$/);
+  if (!match) return obj;
+  const keyValuePairs = match[1].split(/,\s*(?=\w+=)/); // 处理 "key=value" 中的逗号
+  keyValuePairs.forEach(pair => {
+    const [key, value] = pair.split('=');
+    if (value === 'null') {
+      obj[key] = null;
+    } else if (!isNaN(value)) {
+      obj[key] = Number(value);
+    } else {
+      obj[key] = value;
+    }
+  });
+
+  return obj;
+}
 
+// 判断某条日志是否在当前筛选之外(即是否未被选中)
+const onNewSocketLog=async (rawLogStr)=> {
+  // 1. 将字符串解析为对象
+  const newLog = parseLogString(rawLogStr);
+
+  // 2. 获取当前勾选的类型(都转成字符串)
+  const selectedTypes = checkedJoblogs.value.map(String);
+  const newLogType = String(newLog.operationType);
+  // console.log('新日志类型:', newLogType, '勾选类型:', selectedTypes);
+  // 3. 判断是否未被选中 → 弹窗
+  if (!selectedTypes.includes(newLogType)) {
+    dialogLog.value = newLog;
+    console.log(isJoblogVisible.value,'this.isJoblogVisible');
+    if (isJoblogVisible.value){
+      JoblogDialogVisible.value = true;
+      console.log('弹框内容:', newLog);
+    }
+
+  } else {
+    JoblogDialogVisible.value = false;
+    // console.log('该类型已勾选,不弹框');
+  }
+}
 </script>
 
 <style scoped lang="scss">
@@ -888,12 +1002,14 @@ const filterJoblogs = () => {
     height: 25px;
     margin-right: 8px;
   }
+
   .tab-title {
     font-size: 14px;
     font-weight: 500;
     color: #303133;
   }
 }
+
 .processDetail {
   width: 100%;
   height: 300px;
@@ -915,6 +1031,7 @@ const filterJoblogs = () => {
       height: 25px;
       margin-right: 8px;
     }
+
     .tab-text {
       font-size: 14px;
       font-weight: 500;
@@ -929,6 +1046,7 @@ const filterJoblogs = () => {
     flex-wrap: wrap;
     //background: #000;
   }
+
   .left_box {
     width: 500px;
     margin-right: 10px;
@@ -951,6 +1069,7 @@ const filterJoblogs = () => {
       height: 80px;
     }
   }
+
   //作业流程
   .custom-node {
     position: relative;
@@ -1165,33 +1284,35 @@ const filterJoblogs = () => {
       word-break: break-all;
     }
   }
+
   //点位数据
-  .group-box{
+  .group-box {
     margin: 0 20px;
   }
 
   //人员共锁
-  .biglockBox{
+  .biglockBox {
     width: 100%;
     height: 100%;
     display: flex;
     flex-wrap: wrap;
     //background: green;
-    .mumberbox{
+    .mumberbox {
       width: 28%;
       height: 100%;
       margin: 0 15px;
-      .user{
+
+      .user {
         width: 100%;
         max-height: 500px;
         overflow-y: auto;
         overflow-x: hidden;
         display: flex;
         flex-wrap: wrap;
-        padding:2px 5px;
+        padding: 2px 5px;
         box-sizing: border-box;
         //background: pink;
-        .userItem{
+        .userItem {
           width: 20%;
           height: 80px;
           padding: 2px 3px;
@@ -1199,46 +1320,51 @@ const filterJoblogs = () => {
           text-align: center;
           //background: #000;
           box-sizing: border-box;
-        img{
-          width: 50px;
-          height:50px;
-        }
+
+          img {
+            width: 50px;
+            height: 50px;
+          }
+
           p {
             font-size: 16px;
             display: -webkit-box;
-            -webkit-line-clamp: 1;      /* 最多显示1行 */
+            -webkit-line-clamp: 1; /* 最多显示1行 */
             -webkit-box-orient: vertical;
             overflow: hidden;
             text-overflow: ellipsis;
           }
-
         }
       }
     }
-    .arrow{
-     margin-top: 15%;
+
+    .arrow {
+      margin-top: 15%;
     }
   }
+
   //作业日志
-  .joblogCon{
+  .joblogCon {
     width: 98%;
     height: 75%;
     margin: auto;
     //background: greenyellow;
-    .joblogTop{
+    .joblogTop {
       width: 100%;
       height: 490px;
       margin: auto;
       overflow-y: auto;
       //background: cadetblue;
-      p{
+      p {
         font-size: 16px;
         line-height: 20px;
+        margin: 10px 0;
       }
     }
-    .bottomCheck{
-      width:100%;
-      height: 50px;
+
+    .bottomCheck {
+      width: 100%;
+      min-height: 50px;
       line-height: 50px;
       font-size: 23px;
       display: flex;
@@ -1249,11 +1375,9 @@ const filterJoblogs = () => {
       .big-checkbox {
         font-size: 23px; /* 字体大小 */
       }
-
     }
-
-
   }
+
   /* 小屏幕下隐藏文字,只显示图标 */
   @media (max-width: 768px) {
     .tab-label {
@@ -1277,13 +1401,12 @@ const filterJoblogs = () => {
     .group-card-user {
       width: 160px;
       min-height: 130px;
-
     }
     //点位数据
-    .group-box{
+    .group-box {
       margin: 10px 5px;
     }
-  //  人员共锁
+    //  人员共锁
     .biglockBox {
       overflow-y: auto;
       flex-direction: column;
@@ -1293,6 +1416,7 @@ const filterJoblogs = () => {
         width: 95%;
         margin: 15px 0;
       }
+
       .arrow {
         transform: rotate(90deg); // 向右 → 向下
         margin-top: 10px;
@@ -1300,6 +1424,5 @@ const filterJoblogs = () => {
       }
     }
   }
-
 }
 </style>