Răsfoiți Sursa

Merge branch 'master' into monitoring-board

Zimo 3 zile în urmă
părinte
comite
e6fb5aa182

+ 3 - 0
src/api/pms/device/index.ts

@@ -53,6 +53,9 @@ export const IotDeviceApi = {
   getCompany: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/company?id=` + params })
   },
+  getDevice: async (params: any) => {
+    return await request.get({ url: `rq/iot-device/ly/device`, params })
+  },
   getCompanyByDevice: async (params: any) => {
     return await request.get({ url: `/rq/iot-device/company/` + params })
   },

+ 1 - 5
src/components/DeptTreeSelect/index.vue

@@ -75,11 +75,7 @@ const loadTree = async () => {
       // 检查传入的 modelValue 是否存在于当前加载的树数据中
       const isModelValueValid = props.modelValue && res.some((item) => item.id === props.modelValue)
 
-      if (isModelValueValid) {
-        // A. 如果传入了值,且该值在当前树结构中,不做修改,保留原值
-        // 这里不需要 emit,因为值没变
-      } else {
-        // B. 如果没有传入值,或者传入的值不在当前树结构中,强制选中根节点
+      if (!isModelValueValid) {
         emits('update:modelValue', id)
       }
     }

+ 4 - 5
src/components/UserSelectForm/index.vue

@@ -8,7 +8,6 @@
             :data="deptTree"
             :expand-on-click-node="false"
             :props="defaultProps"
-            default-expand-all
             highlight-current
             node-key="id"
             @node-click="handleNodeClick"
@@ -42,8 +41,8 @@
 import { defaultProps, handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import * as UserApi from '@/api/system/user'
-import {companyLevelChildrenDepts} from "@/api/system/dept";
-import {companyDeptsEmployee} from "@/api/system/user";
+import { companyLevelChildrenDepts } from '@/api/system/dept'
+import { companyDeptsEmployee } from '@/api/system/user'
 
 defineOptions({ name: 'UserSelectForm' })
 const emit = defineEmits<{
@@ -86,10 +85,10 @@ const open = async (id: number, selectedList?: any[]) => {
   deptList.value = deptData // 保存扁平结构的部门数据
   deptTree.value = handleTree(deptData) // 转换成树形结构
   // userList.value = await UserApi.getSimpleUserList()
-  const ids = deptData.map(item => item.id)
+  const ids = deptData.map((item) => item.id)
   const params = {
     deptIds: ids
-  };
+  }
   debugger
   userList.value = await UserApi.companyDeptsEmployee(params)
 

+ 1 - 1
src/components/ZmTable/ZmTableColumn.vue

@@ -108,7 +108,7 @@ const getTextWidth = (text: string, fontSize = 14) => {
 
 const calculativeWidth = () => {
   const values = tableContext.data.value
-    .map((item) => props.realValue?.(item[props.prop]) || item[props.prop])
+    .map((item) => props.realValue?.(item) || item[props.prop])
     .filter(Boolean)
 
   let labelWidth = getTextWidth(bindProps.value.label || '') + 34

+ 122 - 0
src/utils/mqtt.ts

@@ -0,0 +1,122 @@
+import { ElMessage } from 'element-plus'
+import mqtt, { MqttClient, IClientOptions, IClientSubscribeOptions } from 'mqtt' // 静态导入 mqtt
+
+// 定义 MQTT 配置类型接口
+interface MqttConfig {
+  host: string
+  options: IClientOptions
+  topic: string
+}
+
+// 定义消息回调函数类型
+type MessageCallback = (data: { topic: string; message: Record<string, any> }) => void
+
+// MQTT 连接配置
+const MQTT_CONFIG: MqttConfig = {
+  // 替换为你的 MQTT 服务器地址(ws/wss 协议)
+  host: 'ws://172.21.10.65:8083/mqtt',
+  // MQTT 连接选项
+  options: {
+    username: 'yanfan', // 替换为你的用户名(无则省略)
+    password: 'eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjY0YmM2NjJlLWZhMjQtNGY1Ny1hOTk1LWZiMGM2YjNhYzI4OCJ9.9nxoDUNGTk1szRlZHHG0AcWZctLrzJ16UA5rsBagHNcD10PC-LIMTgAr2CK1Ppafa6cW5XPdn7RqBF6iZjHtww', // 替换为你的密码(无则省略)
+    clean: true,
+    reconnectPeriod: 5000, // 重连间隔 5 秒
+    clientId: 'web-' + Math.random().toString(16).substr(2),
+    connectTimeout: 10000, // 连接超时 10 秒
+  },
+  // 要监听的主题
+  topic: '/649/YF6660355/property/post'
+}
+
+// 创建 MQTT 客户端实例(添加类型注解)
+let client: MqttClient | null = null
+// 消息回调函数
+let messageCallback: MessageCallback | null = null
+
+/**
+ * 初始化 MQTT 连接
+ * @param callback 接收消息的回调函数
+ */
+export const initMqtt = (callback: MessageCallback): void => {
+  // 保存消息回调
+  messageCallback = callback
+
+  // 避免重复连接
+  if (client && client.connected) {
+    ElMessage.info('MQTT 已连接')
+    return
+  }
+
+  try {
+    // 直接使用静态导入的 mqtt 创建连接(核心修复点)
+    client = mqtt.connect(MQTT_CONFIG.host, MQTT_CONFIG.options)
+
+    // 连接成功
+    client.on('connect', () => {
+      ElMessage.success('MQTT 连接成功')
+      // 订阅指定主题
+      const subscribeOptions: IClientSubscribeOptions = { qos: 0 }
+      client?.subscribe(MQTT_CONFIG.topic, subscribeOptions, (err) => {
+        if (err) {
+          ElMessage.error(`订阅主题失败:${err.message}`)
+        } else {
+          ElMessage.info(`已订阅主题:${MQTT_CONFIG.topic}`)
+        }
+      })
+    })
+
+    // 接收消息
+    client.on('message', (topic: string, payload: Buffer) => {
+      if (topic === MQTT_CONFIG.topic) {
+        try {
+          const messageStr = payload.toString()
+          const messageObj = JSON.parse(messageStr) as Record<string, any>
+          // 调用回调函数,将消息传递给组件
+          messageCallback?.({
+            topic,
+            message: messageObj
+          })
+        } catch (parseErr) {
+          ElMessage.error(`消息解析失败:${(parseErr as Error).message}`)
+          // 解析失败时直接返回字符串
+          messageCallback?.({
+            topic,
+            message: { raw: payload.toString() }
+          })
+        }
+      }
+    })
+
+    // 连接断开
+    client.on('close', () => {
+      ElMessage.warning('MQTT 连接已断开')
+    })
+
+    // 连接错误
+    client.on('error', (err: Error) => {
+      ElMessage.error(`MQTT 连接错误:${err.message}`)
+      client?.end()
+    })
+  } catch (err) {
+    const error = err as Error
+    ElMessage.error(`MQTT 初始化失败:${error.message}`)
+  }
+}
+
+/**
+ * 断开 MQTT 连接
+ */
+export const disconnectMqtt = (): void => {
+  if (client) {
+    client.end()
+    client = null
+    ElMessage.info('MQTT 已断开连接')
+  }
+}
+
+/**
+ * 获取当前 MQTT 连接状态
+ */
+export const getMqttStatus = (): boolean => {
+  return !!client?.connected
+}

+ 55 - 29
src/views/oli-connection/monitoring-query/index.vue

@@ -7,24 +7,21 @@ import { formatIotValue } from '@/utils/useSocketBus'
 import { useDebounceFn } from '@vueuse/core'
 import { dayjs, ElMessage } from 'element-plus'
 import { computed, ref } from 'vue'
-import { useI18n } from 'vue-i18n'
 
-const { t } = useI18n()
+defineOptions({ name: 'MonitoringQuery' })
 
 const id = useUserStore().getUser.deptId ?? 157
 const deptId = id
 
 interface Query {
   deptId?: number
-  deviceName?: string
   deviceCode?: string
   time?: string[]
 }
 
 const query = ref<Query>({
-  deviceName: '',
   deviceCode: '',
-  time: []
+  time: [...rangeShortcuts[0].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
 })
 
 const pageSize = ref(100)
@@ -46,13 +43,46 @@ const loading = ref(false)
 
 const isConditionValid = computed(() => {
   const hasTime = query.value.time && query.value.time.length === 2
-  const hasIdentity = !!query.value.deviceName || !!query.value.deviceCode
+  const hasIdentity = !!query.value.deviceCode
   return hasTime && hasIdentity
 })
 
 const canGoBack = computed(() => historyStack.value.length > 0)
 const canGoNext = computed(() => list.value.length >= pageSize.value)
 
+const deviceOptions = ref<{ label: string; value: string }[]>([])
+
+const optionsLoading = ref(false)
+
+const loadOptions = useDebounceFn(async function () {
+  handleReset()
+  try {
+    optionsLoading.value = true
+    const data = await IotDeviceApi.getDevice({
+      deptId: query.value.deptId
+    })
+    deviceOptions.value = data.map((item: any) => {
+      return {
+        label: item.deviceCode + '-' + item.deviceName,
+        value: item.deviceCode
+      }
+    })
+  } catch (error) {
+    console.error(error)
+  } finally {
+    optionsLoading.value = false
+  }
+}, 300)
+
+const handleNodeClick = (data: any) => {
+  query.value.deptId = data.id
+  loadOptions()
+}
+
+onMounted(() => {
+  loadOptions()
+})
+
 const loadList = useDebounceFn(async function () {
   if (!isConditionValid.value) {
     // list.value = []
@@ -82,8 +112,8 @@ const loadList = useDebounceFn(async function () {
 })
 
 function handleQuery() {
-  if (!query.value.deviceName && !query.value.deviceCode) {
-    ElMessage.warning('请输入设备名称或设备编码')
+  if (!query.value.deviceCode) {
+    ElMessage.warning('请选择设备')
     return
   }
 
@@ -102,8 +132,7 @@ function handleQuery() {
 function handleReset() {
   query.value = {
     deviceCode: '',
-    deviceName: '',
-    time: []
+    time: [...rangeShortcuts[0].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
   }
   list.value = []
   historyStack.value = []
@@ -147,8 +176,8 @@ function handleSizeChange() {
 const exportLoading = ref(false)
 
 const handleExport = useDebounceFn(async function () {
-  if (!query.value.deviceName && !query.value.deviceCode) {
-    ElMessage.warning('请输入设备名称或设备编码')
+  if (!query.value.deviceCode) {
+    ElMessage.warning('请选择设备')
     return
   }
 
@@ -189,7 +218,13 @@ function formatterValue(row: ListItem) {
     class="grid grid-cols-[15%_1fr] grid-rows-[62px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
-      <DeptTreeSelect :top-id="156" :deptId="deptId" :init-select="false" :show-title="false" />
+      <DeptTreeSelect
+        :top-id="156"
+        :deptId="deptId"
+        :init-select="false"
+        :show-title="false"
+        @node-click="handleNodeClick"
+      />
     </div>
 
     <el-form
@@ -197,22 +232,13 @@ function formatterValue(row: ListItem) {
       class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
     >
       <div class="flex items-center gap-8">
-        <el-form-item :label="t('monitor.deviceName')">
-          <el-input
-            v-model="query.deviceName"
-            :placeholder="t('monitor.nameHolder')"
-            clearable
-            @keyup.enter="handleQuery()"
-            class="!w-240px"
-          />
-        </el-form-item>
-        <el-form-item :label="t('monitor.deviceCode')">
-          <el-input
+        <el-form-item label="设备">
+          <el-select
+            :loading="optionsLoading"
             v-model="query.deviceCode"
-            :placeholder="t('monitor.codeHolder')"
-            clearable
-            @keyup.enter="handleQuery()"
-            class="!w-240px"
+            :options="deviceOptions"
+            placeholder="请选择设备"
+            class="w-60!"
           />
         </el-form-item>
         <el-form-item label="时间">
@@ -224,7 +250,7 @@ function formatterValue(row: ListItem) {
             end-placeholder="结束日期"
             :shortcuts="rangeShortcuts"
             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-            class="!w-352px"
+            class="!w-360px"
           />
         </el-form-item>
         <el-form-item>

+ 4 - 1
src/views/oli-connection/monitoring/index.vue

@@ -5,6 +5,8 @@ import { useUserStore } from '@/store/modules/user'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 import { useDebounceFn } from '@vueuse/core'
 
+defineOptions({ name: 'MonitoringList' })
+
 const { t } = useI18n()
 
 const id = useUserStore().getUser.deptId ?? 157
@@ -24,7 +26,8 @@ const viewMode = ref('card')
 const query = ref<Query>({
   pageNo: 1,
   pageSize: 12,
-  deptId: id
+  deptId: id,
+  ifInline: '3'
 })
 
 interface OliDevice {

+ 182 - 39
src/views/pms/iotprojecttask/IotProjectTaskForm.vue

@@ -465,6 +465,58 @@
     </el-form>
   </ContentWrap>
 
+  <div
+    v-if="companyName === 'ry'"
+    class="my-6 bg-white p-4 border-1 border-solid border-[var(--el-border-color-light)]"
+  >
+    <h3 style="margin-bottom: 20px">附件</h3>
+    <el-form-item size="default" label="工程设计">
+      <el-upload
+        v-model:file-list="constructionFiles"
+        :action="uploadUrl"
+        multiple
+        :headers="{ 'tenant-id': 1, 'device-id': 'undefined' }"
+        name="files"
+        class="w-50%"
+      >
+        <el-button type="primary">上传工程设计</el-button>
+        <template #tip>
+          <div class="el-upload__tip">文件大小不能超过50MB </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+    <el-form-item size="default" label="地质设计">
+      <el-upload
+        v-model:file-list="geologicalFiles"
+        :action="uploadUrl"
+        multiple
+        :headers="{ 'tenant-id': 1, 'device-id': 'undefined' }"
+        name="files"
+        class="w-50%"
+      >
+        <el-button type="primary">上传地质设计</el-button>
+        <template #tip>
+          <div class="el-upload__tip">文件大小不能超过50MB </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+    <el-form-item size="default" label="完井报告">
+      <el-upload
+        v-model:file-list="completionFiles"
+        :action="uploadUrl"
+        multiple
+        :headers="{ 'tenant-id': 1, 'device-id': 'undefined' }"
+        name="files"
+        class="w-50%"
+      >
+        <el-button :disabled="tableData[0].status !== 'wg'" type="primary">上传完井报告</el-button>
+        <template #tip>
+          <div class="el-upload__tip">文件大小不能超过50MB </div>
+        </template>
+      </el-upload>
+    </el-form-item>
+  </div>
+
   <ContentWrap v-if="currentTask.platformWell === '1'">
     <h3 style="margin-bottom: 20px">平台井</h3>
     <el-table :data="currentTask.platformWellDetails" style="width: 100%">
@@ -764,19 +816,25 @@ import { IotDeviceApi, IotDeviceVO } from '@/api/pms/device'
 import * as UserApi from '@/api/system/user'
 import { IotProjectTaskAttrsApi } from '@/api/pms/iotprojecttaskattrs'
 import { DICT_TYPE, getStrDictOptions, getDictLabel } from '@/utils/dict'
+import { IotOpeationFillApi } from '@/api/pms/iotopeationfill'
 // 在导入部分添加Plus图标
 // import { Plus } from '@element-plus/icons-vue'
 
 const { query, params, name } = useRoute() // 查询参数
 const id = params.id
 
+const uploadUrl = import.meta.env.VITE_BASE_URL + '/admin-api/rq/file/upload'
+const constructionFiles = ref<any>([])
+const geologicalFiles = ref<any>([])
+const completionFiles = ref<any>([])
+
 // 修改projectId获取逻辑:优先使用params.projectId,其次使用query.projectId
 const projectId = ref<string>('')
 // 获取projectId的逻辑
 if (params.projectId) {
   projectId.value = Array.isArray(params.projectId) ? params.projectId[0] : params.projectId
 } else if (query.projectId) {
-  projectId.value = Array.isArray(query.projectId)
+  projectId.value = Array.isArray(query.projectId as any)
     ? query.projectId[0]
     : (query.projectId as string)
 }
@@ -818,7 +876,7 @@ const submitterList = ref([]) // 所有填报人列表
 const selectedSubmitterIds = ref([]) // 选中的日报填报人ID
 
 // 动态属性相关变量
-const dynamicAttrs = ref([]) // 存储动态属性列表
+const dynamicAttrs = ref<any[]>([]) // 存储动态属性列表
 
 // 跟踪是否已从任务数据中获取动态属性
 const hasDynamicAttrsFromTask = ref(false)
@@ -1805,6 +1863,8 @@ const getAllDeviceNames = (deviceIds: number[]) => {
   return deviceNames.join(', ') || '无有效设备'
 }
 
+const companyName = ref('')
+
 /** 打开弹窗 */
 const open = async () => {
   resetForm()
@@ -1820,6 +1880,9 @@ const open = async () => {
       const data = await IotProjectTaskApi.getIotProjectTaskPage(queryParams)
       tableData.value = data.list
 
+      const company = await IotOpeationFillApi.getOrgName(tableData.value[0].deptId)
+      companyName.value = company
+
       // 收集所有设备ID
       const allDeviceIds = new Set<number>()
       // 收集所有责任人ID
@@ -1968,6 +2031,36 @@ const open = async () => {
             }
           })
         }
+
+        const attachments = tableData.value[0].attachments || []
+
+        constructionFiles.value = attachments
+          .filter((item) => item.type === 'CONSTRUCTION_DESIGN')
+          .map((item) => ({
+            name: item.filename,
+            percentage: 100,
+            response: { data: item },
+            status: 'success',
+            uid: item.createTime
+          }))
+        geologicalFiles.value = attachments
+          .filter((item) => item.type === 'GEOLOGICAL_DESIGN')
+          .map((item) => ({
+            name: item.filename,
+            percentage: 100,
+            response: { data: item },
+            status: 'success',
+            uid: item.createTime
+          }))
+        completionFiles.value = attachments
+          .filter((item) => item.type === 'COMPLETION_DESIGN')
+          .map((item) => ({
+            name: item.filename,
+            percentage: 100,
+            response: { data: item },
+            status: 'success',
+            uid: item.createTime
+          }))
       }
     } finally {
       formLoading.value = false
@@ -2324,6 +2417,46 @@ const submitForm = async () => {
     return
   }
 
+  const getFileType = (filename: string) => {
+    const ext = filename.split('.').pop()?.toLowerCase()
+    if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext || '')) {
+      return 'image'
+    } else if (['pdf'].includes(ext || '')) {
+      return 'pdf'
+    } else if (['doc', 'docx'].includes(ext || '')) {
+      return 'word'
+    } else if (['xls', 'xlsx'].includes(ext || '')) {
+      return 'excel'
+    } else {
+      return 'other'
+    }
+  }
+
+  const formatFileSize = (bytes: number) => {
+    if (bytes === 0) return '0 Bytes'
+    const k = 1024
+    const sizes = ['Bytes', 'KB', 'MB', 'GB']
+    const i = Math.floor(Math.log(bytes) / Math.log(k))
+    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+  }
+
+  function formatterAttachments(attachments: any[], type: string, taskId: number) {
+    return attachments.flatMap((item) => {
+      if (item.response.data.files) {
+        return item.response.data.files.map((file) => ({
+          category: 'DAILY_REPORT',
+          bizId: taskId,
+          filename: file.name || '未知文件',
+          fileType: getFileType(file.name),
+          filePath: file.filePath,
+          fileSize: formatFileSize(file.size || 0),
+          remark: '',
+          type
+        }))
+      } else return item.response.data
+    })
+  }
+
   // 处理动态属性数据
   const processedTableData = tableData.value.map((task) => {
     // 获取当前任务的工作量数据并转换为dropdownList格式
@@ -2352,7 +2485,10 @@ const submitForm = async () => {
     return {
       ...task,
       dictType: task.dictType || currentDictLabel.value, // 确保每个任务都有dictType
-      extProperty: extProperties
+      extProperty: extProperties,
+      attachments: formatterAttachments(constructionFiles.value, 'CONSTRUCTION_DESIGN', task.id)
+        .concat(formatterAttachments(geologicalFiles.value, 'GEOLOGICAL_DESIGN', task.id))
+        .concat(formatterAttachments(completionFiles.value, 'COMPLETION_REPORT', task.id))
     }
   })
 
@@ -2366,6 +2502,7 @@ const submitForm = async () => {
     const data = {
       taskList: processedTableData
     }
+
     if (formType.value === 'create') {
       await IotProjectTaskApi.createIotProjectTask(data)
       message.success(t('common.createSuccess'))
@@ -2963,11 +3100,13 @@ onMounted(async () => {
 .edit-input {
   margin-right: 10px;
 }
+
 .error-message {
-  color: #f56c6c;
-  font-size: 12px;
   margin-top: 5px;
+  font-size: 12px;
+  color: #f56c6c;
 }
+
 .action-cell {
   display: flex;
   gap: 8px;
@@ -2975,8 +3114,8 @@ onMounted(async () => {
 
 /* 1. 穿梭框父容器:居中 + 内边距 */
 .transfer-container {
+  padding: 0; /* 上下内边距,避免紧贴对话框边缘 */
   text-align: center;
-  padding: 0px; /* 上下内边距,避免紧贴对话框边缘 */
 }
 
 /* 2. 穿梭框组件:控制宽度,避免过窄或过宽 */
@@ -2993,22 +3132,26 @@ onMounted(async () => {
 /* 3. 深度选择器:修改el-transfer选项样式(解决内容省略问题) */
 :deep(.el-transfer-panel__item) {
   /* white-space: nowrap;  文本不换行,保证一行显示 */
+
   /* overflow: hidden;  超出部分隐藏 */
+
   /* text-overflow: ellipsis;  超出显示省略号 */
+
   /* max-width: 100%;  确保文本不超出选项容器宽度 */
+
   /* padding: 6px 6px;  适当增加内边距,优化点击体验 */
 }
 
 :deep(.el-transfer-panel__item) {
   display: flex !important;
-  align-items: center !important;
   height: 32px !important;
-  line-height: 32px !important;
   padding: 0 8px !important;
   margin: 0 !important;
-  white-space: nowrap;
   overflow: hidden;
+  line-height: 32px !important;
   text-overflow: ellipsis;
+  white-space: nowrap;
+  align-items: center !important;
 }
 
 /* 4. 选项文本:确保继承父容器宽度,省略号正常生效 */
@@ -3019,10 +3162,10 @@ onMounted(async () => {
 
 /* 设备名称显示区域样式 */
 .device-names {
-  white-space: nowrap;
+  max-width: 200px; /* 根据实际列宽调整 */
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: 200px; /* 根据实际列宽调整 */
+  white-space: nowrap;
 }
 
 :deep(.el-transfer-panel__list) {
@@ -3031,35 +3174,35 @@ onMounted(async () => {
 
 /* 责任人名称显示区域样式 */
 .responsible-names {
-  white-space: nowrap;
+  max-width: 200px; /* 根据实际列宽调整 */
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: 200px; /* 根据实际列宽调整 */
+  white-space: nowrap;
 }
 
 /* 添加部门名称的样式 */
 .dept-names {
-  white-space: nowrap;
+  display: inline-block;
+  max-width: 200px; /* 根据实际列宽调整 */
   overflow: hidden;
   text-overflow: ellipsis;
-  max-width: 200px; /* 根据实际列宽调整 */
-  display: inline-block;
+  white-space: nowrap;
   vertical-align: bottom;
 }
 
 /* 部门选择器样式优化 */
 :deep(.department-tree-select .el-select__tags) {
-  white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
+  white-space: nowrap;
   flex-wrap: nowrap;
 }
 
 :deep(.department-tree-select .el-select__tags .el-tag) {
+  display: inline-block;
   max-width: 120px;
   overflow: hidden;
   text-overflow: ellipsis;
-  display: inline-block;
 }
 
 :deep(.department-tree-select .el-select__tags .el-tag + .el-tag) {
@@ -3075,29 +3218,29 @@ onMounted(async () => {
 
 /* 当有多个标签被折叠时显示的 "+N" 标签样式 */
 :deep(.department-tree-select .el-select__collapse-tags) {
-  white-space: nowrap;
   display: inline-block;
+  white-space: nowrap;
 }
 
 .task-edit-form {
-  margin-bottom: 20px;
   padding: 20px;
+  margin-bottom: 20px;
+  background-color: #fafafa;
   border: 1px solid #ebeef5;
   border-radius: 4px;
-  background-color: #fafafa;
 }
 
 /* 按钮文字样式 - 红色字体 */
 :deep(.workload-add-btn .btn-text) {
-  color: #ff0000 !important;
-  font-weight: 900; /* 更粗的字体 */
   font-size: 18px; /* 更大的字号 */
+  font-weight: 900; /* 更粗的字体 */
   line-height: 1;
+  color: #f00 !important;
 }
 
 /* 设计工作量加号按钮 - 启用状态(深蓝色) */
 :deep(.workload-add-btn .el-icon) {
-  color: #ff0000 !important; /* Element Plus 主色调深蓝色,可替换为 #003366 等自定义深蓝色 */
+  color: #f00 !important; /* Element Plus 主色调深蓝色,可替换为 #003366 等自定义深蓝色 */
 }
 
 /* 按钮禁用状态 */
@@ -3108,53 +3251,53 @@ onMounted(async () => {
 /* 调整按钮整体样式,确保icon居中 */
 :deep(.workload-add-btn) {
   display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 0 8px;
-  min-width: auto;
   width: 100%;
   height: 100%;
+  min-width: auto;
+  padding: 0 8px;
+  align-items: center;
+  justify-content: center;
 }
 
 /* 调整输入框append部分的宽度 */
 :deep(.workload-input-with-button .el-input-group__append) {
-  padding: 0;
   width: 45px; /* 稍微增加宽度以容纳文字 */
+  padding: 0;
 }
 
 /* 确保按钮在禁用状态下有正确的样式 */
 :deep(.workload-add-btn:disabled) {
+  cursor: not-allowed;
   background-color: #f5f7fa;
   border-color: #e4e7ed;
-  cursor: not-allowed;
 }
 
 /* 设计工作量错误输入框样式 */
 :deep(.error-input .el-input__inner) {
-  border-color: #f56c6c !important;
   background-color: #fef0f0 !important;
-  box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.4) !important;
+  border-color: #f56c6c !important;
+  box-shadow: 0 0 0 1px rgb(245 108 108 / 40%) !important;
 }
 
 :deep(.error-input .el-input__inner:hover) {
-  border-color: #f56c6c !important;
   background-color: #fef0f0 !important;
+  border-color: #f56c6c !important;
 }
 
 :deep(.error-input .el-input__inner:focus) {
-  border-color: #f56c6c !important;
   background-color: #fef0f0 !important;
-  box-shadow: 0 0 0 1px rgba(245, 108, 108, 0.2) !important;
+  border-color: #f56c6c !important;
+  box-shadow: 0 0 0 1px rgb(245 108 108 / 20%) !important;
 }
 
 /* 设计工作量tooltip样式 */
 :deep(.workload-design-tooltip) {
-  background: #fef0f0 !important;
-  border: 1px solid #fbc4c4 !important;
-  color: #f56c6c !important;
   max-width: 300px;
-  font-size: 12px;
   padding: 8px 12px;
+  font-size: 12px;
+  color: #f56c6c !important;
+  background: #fef0f0 !important;
+  border: 1px solid #fbc4c4 !important;
 }
 
 /* 隐藏 Tooltip 箭头 */

+ 6 - 0
src/views/pms/iotprojecttask/index.vue

@@ -177,6 +177,12 @@
           prop="deptNames"
           :width="columnWidths.deptNames"
         />
+        <el-table-column
+          label="施工状态"
+          align="center"
+          prop="statusLabel"
+          :width="columnWidths.deptNames"
+        />
         <!-- <el-table-column :label="t('project.technology')" align="center" prop="technique" :width="columnWidths.technique">
           <template #default="scope">
             <dict-tag :type="DICT_TYPE.PMS_PROJECT_TECHNOLOGY" :value="scope.row.technique" />

+ 28 - 17
src/views/pms/iotrddailyreport/FillDailyReportForm.vue

@@ -148,15 +148,7 @@ const rules = ref<FormRules<Form>>({
   timeRange: [{ required: true, message: '请选择时间节点', trigger: 'change', type: 'array' }],
   dailyFuel: [{ required: true, message: '请输入当日油耗', trigger: 'change' }],
   nextPlan: [{ required: true, message: '请输入下计划', trigger: 'change' }],
-  reportDetails: [{ required: true, message: '请填写生产动态', type: 'array' }],
-  constructionBrief: [
-    {
-      required: formType.value === 'time',
-      message: '请填写施工简报',
-      type: 'string',
-      trigger: ['blur', 'change']
-    }
-  ]
+  reportDetails: [{ required: true, message: '请填写生产动态', type: 'array' }]
 })
 
 function noProductionTimeRule(id: number) {
@@ -327,7 +319,15 @@ const submitForm = useDebounceFn(async function submitForm() {
       delete form.value[o.value]
     })
 
-    await formRef.value?.validate()
+    if (formType.value === 'time') {
+      await formRef.value?.validateField([
+        'constructionBrief',
+        ...form.value.platformIds.flatMap((pid) => [
+          ...NON_PROD_FIELDS.map((item) => `${pid}.${item.key}`),
+          `${pid}.otherNptReason`
+        ])
+      ])
+    } else await formRef.value?.validate()
 
     const copyForm = cloneDeep(form.value)
 
@@ -433,7 +433,6 @@ async function submitApprovalForm(auditStatus: number) {
 }
 
 function handleOpenForm(id: number, type: 'edit' | 'approval' | 'detail' | 'time') {
-  console.log('id :>> ', id)
   formType.value = type
   form.value = original()
   loadDetail(id).then(() => {
@@ -1324,11 +1323,11 @@ const inContent = async (attachment) => {
             class="col-span-4"
             label="其他非生产原因"
             :prop="`${pid}.otherNptReason`"
-            :rules="
-              form[pid].otherNptTime > 0
-                ? { required: true, message: '请填写原因', trigger: 'change' }
-                : {}
-            "
+            :rules="{
+              required: form[pid].otherNptTime > 0,
+              message: '请填写原因',
+              trigger: ['blur', 'change']
+            }"
           >
             <el-input
               v-model="form[pid].otherNptReason"
@@ -1344,7 +1343,19 @@ const inContent = async (attachment) => {
         </div>
       </template>
 
-      <el-form-item class="mt-4 col-span-2" label="当日施工简报" prop="constructionBrief">
+      <el-form-item
+        class="mt-4 col-span-2"
+        label="当日施工简报"
+        prop="constructionBrief"
+        :rules="[
+          {
+            required: formType === 'time',
+            message: '请填写施工简报',
+            type: 'string',
+            trigger: ['blur', 'change']
+          }
+        ]"
+      >
         <el-input
           v-model="form.constructionBrief"
           type="textarea"

+ 1 - 0
src/views/pms/iotrddailyreport/index.vue

@@ -415,6 +415,7 @@ onMounted(() => {
                   <span v-else class="text-gray-300">-</span>
                 </template>
               </zm-table-column>
+              <zm-table-column prop="constructionBrief" label="当日施工简报" />
               <zm-table-column prop="nextPlan" label="下步工作计划" />
               <zm-table-column label="当日">
                 <zm-table-column prop="cumulativeWorkingWell" label="施工井" />

+ 1 - 1
src/views/pms/iotrydailyreport/index.vue

@@ -196,7 +196,7 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['pms:iot-rh-daily-report:export']"
+          v-hasPermi="['pms:iot-ry-daily-report:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>

+ 3 - 2
src/views/pms/iotrydailyreport/ry-table.vue

@@ -268,7 +268,7 @@ function handleCurrentChange(val: number) {
                             <div class="flex items-center">
                               <div class="i-carbon-calendar mr-1 -translate-y-[0.5px]"></div>
                               <div class="font-medium mr-2">{{
-                                dayjs(row.createTime).format('YYYY-MM-DD')
+                                dayjs(row.reportDetails[0].reportDate).format('YYYY-MM-DD')
                               }}</div>
                               <div class="flex items-center">
                                 <span>{{ formatT(row.reportDetails[0].startTime) }}</span>
@@ -334,7 +334,7 @@ function handleCurrentChange(val: number) {
                           <div class="flex items-center">
                             <div class="i-carbon-calendar mr-1 -translate-y-[0.5px]"></div>
                             <div class="font-medium mr-2">{{
-                              dayjs(row.createTime).format('YYYY-MM-DD')
+                              dayjs(item.reportDate).format('YYYY-MM-DD')
                             }}</div>
                             <div class="flex items-center">
                               <span>{{ formatT(item.startTime) }}</span>
@@ -378,6 +378,7 @@ function handleCurrentChange(val: number) {
                 <span v-else class="text-gray-300">-</span>
               </template>
             </zm-table-column>
+            <zm-table-column prop="constructionBrief" label="当日施工简报" />
             <zm-table-column prop="contractName" label="项目" />
             <zm-table-column prop="drillingWorkingTime" label="进尺工作时间(H)" />
             <zm-table-column prop="otherProductionTime" label="其它生产时间(H)" />

+ 3 - 2
src/views/pms/iotrydailyreport/ry-xj-table.vue

@@ -329,7 +329,7 @@ function handleCurrentChange(val: number) {
                             <div class="flex items-center">
                               <div class="i-carbon-calendar mr-1 -translate-y-[0.5px]"></div>
                               <div class="font-medium mr-2">{{
-                                dayjs(row.createTime).format('YYYY-MM-DD')
+                                dayjs(row.reportDetails[0].reportDate).format('YYYY-MM-DD')
                               }}</div>
                               <div class="flex items-center">
                                 <span>{{ formatT(row.reportDetails[0].startTime) }}</span>
@@ -395,7 +395,7 @@ function handleCurrentChange(val: number) {
                           <div class="flex items-center">
                             <div class="i-carbon-calendar mr-1 -translate-y-[0.5px]"></div>
                             <div class="font-medium mr-2">{{
-                              dayjs(row.createTime).format('YYYY-MM-DD')
+                              dayjs(item.reportDate).format('YYYY-MM-DD')
                             }}</div>
                             <div class="flex items-center">
                               <span>{{ formatT(item.startTime) }}</span>
@@ -433,6 +433,7 @@ function handleCurrentChange(val: number) {
                 <span v-else class="text-gray-300">-</span>
               </template>
             </zm-table-column>
+            <zm-table-column prop="constructionBrief" label="当日施工简报" />
             <zm-table-column prop="contractName" label="项目" />
             <zm-table-column prop="totalStaffNum" label="全员数量" />
             <zm-table-column

+ 1 - 1
src/views/pms/iotrydailyreport/xjindex.vue

@@ -195,7 +195,7 @@ function handleOpenForm(id: number, type: 'edit' | 'readonly') {
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['pms:iot-rh-daily-report:export']"
+          v-hasPermi="['pms:iot-ry-daily-report:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>

+ 94 - 0
src/views/pms/mqtt/mqtt.vue

@@ -0,0 +1,94 @@
+<template>
+  <el-card title="MQTT 消息监听(TS 版本)">
+    <el-alert
+      v-if="mqttMessage"
+      title="收到新消息"
+      type="success"
+      :description="JSON.stringify(mqttMessage, null, 2)"
+      show-icon
+      style="margin-bottom: 20px;"
+    />
+    <el-button
+      type="primary"
+      @click="initMqttConnect"
+      :disabled="isConnected"
+    >
+      连接 MQTT
+    </el-button>
+    <el-button
+      type="danger"
+      @click="disconnectMqttConnect"
+      :disabled="!isConnected"
+      style="margin-left: 10px;"
+    >
+      断开连接
+    </el-button>
+    <el-tag v-if="isConnected" type="success">已连接</el-tag>
+    <el-tag v-else type="danger">未连接</el-tag>
+  </el-card>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { initMqtt, disconnectMqtt, getMqttStatus } from '@/utils/mqtt'
+
+// 定义消息类型接口
+interface MqttReceivedMessage {
+  topic: string
+  message: Record<string, any>
+}
+
+// 响应式数据(添加类型注解)
+const mqttMessage = ref<MqttReceivedMessage | null>(null)
+const isConnected = ref<boolean>(false)
+
+// 计算属性:实时获取连接状态
+const mqttStatus = computed(() => getMqttStatus())
+
+// 初始化 MQTT 连接
+const initMqttConnect = (): void => {
+  initMqtt((message: MqttReceivedMessage) => {
+    // 接收消息并更新到页面
+    mqttMessage.value = message
+    // 业务逻辑处理
+    console.log('收到 MQTT 消息:', message)
+    ElMessage.success(`收到主题 [${message.topic}] 的新消息`)
+  })
+  isConnected.value = true
+}
+
+// 断开 MQTT 连接
+const disconnectMqttConnect = (): void => {
+  disconnectMqtt()
+  isConnected.value = false
+  mqttMessage.value = null
+}
+
+// 组件挂载时检查连接状态
+onMounted(() => {
+  isConnected.value = mqttStatus.value
+  // 自动连接(可选)
+  // if (!isConnected.value) {
+  //   initMqttConnect()
+  // }
+})
+
+// 组件卸载时断开连接,避免内存泄漏
+onUnmounted(() => {
+  if (isConnected.value) {
+    disconnectMqttConnect()
+  }
+})
+</script>
+
+<style scoped>
+.el-card {
+  width: 800px;
+  margin: 20px auto;
+}
+
+.el-tag {
+  margin-left: 10px;
+}
+</style>

+ 235 - 53
src/views/pms/qhse/approve/index.vue

@@ -81,6 +81,11 @@
         />
         <el-table-column prop="deptName" label="部门名称" align="center" />
         <el-table-column prop="dutyPerson" label="现场负责人" align="center" width="100" />
+        <el-table-column prop="actualTime" label="创建时间" align="center" min-width="150">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </template>
+        </el-table-column>
 
         <el-table-column label="操作" align="center" width="180" fixed="right">
           <template #default="{ row }">
@@ -115,73 +120,86 @@
       </div>
     </ContentWrap>
 
-    <!-- 审批流程弹窗 -->
-    <!-- 审批流程弹窗 -->
-    <!-- 审批流程抽屉 -->
     <el-drawer
       :title="approvalDialogTitle"
       v-model="approvalDialogVisible"
       direction="rtl"
-      size="600px"
+      size="650px"
       :with-header="true"
       :close-on-click-modal="false"
       destroy-on-close
     >
       <template #header>
-        <div
-          style="
-            display: flex;
-            justify-content: space-between;
-            align-items: center;
-            border-bottom: 1px solid #ebeef5;
-            padding-bottom: 20px;
-            padding-left: 20px;
-            width: 108%;
-            margin-left: -15px;
-          "
-        >
+        <div class="drawer-header">
           <span>{{ approvalDialogTitle }}</span>
         </div>
       </template>
 
-      <!-- 时间线展示审批记录 -->
-      <el-timeline>
-        <el-timeline-item
-          v-for="(item, index) in approvalProcessList"
-          :key="index"
-          :timestamp="formatDate(item.createTime)"
-          placement="top"
-          :color="getStatusColor(item.status)"
-          :icon="Check"
-        >
-          <el-card shadow="never" class="text-sm">
-            <p>节点名称: {{ item.nodeName }}</p>
-            <p>操作人: {{ item.operator }}</p>
-            <p class="pt-1">审批状态:{{ item.status }}</p>
-            <p class="pt-1">描述:{{ item.description }}</p>
-          </el-card>
-        </el-timeline-item>
-      </el-timeline>
-
-      <!-- 审批建议输入框 -->
-      <!-- <el-form-item label="审批建议" prop="suggestion">
-        <el-input
-          v-model="approvalSuggestion"
-          type="textarea"
-          :rows="3"
-          placeholder="请输入审批建议"
-          maxlength="500"
-          show-word-limit
-        />
-      </el-form-item> -->
-
-      <!-- <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="handleApprovalCancel">取 消</el-button>
-
-          <el-button type="primary" @click="handleApprovalSubmit">上 报</el-button>
+      <!-- 专业化的审批流程时间线 -->
+      <div class="approval-process-container">
+        <el-timeline v-if="approvalProcessList.length > 0">
+          <el-timeline-item
+            v-for="(item, index) in approvalProcessList"
+            :key="index"
+            placement="top"
+            :color="getNodeStatusColor(item.status)"
+            :icon="Check"
+          >
+            <el-card
+              shadow="never"
+              class="approval-card"
+              :class="{ 'status-completed': item.status === '已完成' || item.status === '已批准' }"
+            >
+              <div class="card-header">
+                <div class="node-info">
+                  <el-tag :type="getTagTypeByStatus(item.status)" size="small" class="status-tag">
+                    {{ item.status }}
+                  </el-tag>
+                  <span class="node-name">{{ item.nodeName }}</span>
+                </div>
+                <div class="operator-time">
+                  <span class="time">{{ formatDate(item.createTime) }}</span>
+                </div>
+              </div>
+
+              <div class="card-content">
+                <div class="opinion-section">
+                  <span class="label">操作人:</span>
+                  <span class="opinion">{{ item.operator }}</span>
+                </div>
+
+                <div class="description-section mt-1" v-if="item.description">
+                  <span class="label">审批意见:</span>
+                  <span class="description">{{ item.description }}</span>
+                </div>
+                <div
+                  class="attachment-section"
+                  v-if="item.attachments && item.attachments.length > 0"
+                >
+                  <span class="label">附件:</span>
+                  <div class="attachments">
+                    <el-link
+                      v-for="(attachment, idx) in item.attachments"
+                      :key="idx"
+                      type="primary"
+                      :href="attachment.url"
+                      target="_blank"
+                      class="attachment-link"
+                    >
+                      {{ attachment.name }}
+                    </el-link>
+                  </div>
+                </div>
+              </div>
+            </el-card>
+          </el-timeline-item>
+        </el-timeline>
+
+        <!-- 无数据提示 -->
+        <div v-else class="no-data">
+          <el-empty description="暂无审批流程信息" :image-size="100" />
         </div>
-      </template> -->
+      </div>
     </el-drawer>
 
     <!-- 审批建议弹窗 -->
@@ -420,6 +438,35 @@ const handleSizeChange = (val) => {
   getList()
 }
 
+const getTagTypeByStatus = (status) => {
+  const tagTypeMap = {
+    待处理: 'warning',
+    处理中: 'info',
+    已完成: 'success',
+    已驳回: 'danger',
+    已撤销: 'info',
+    已批准: 'success',
+    审批中: 'primary',
+    上报: 'success',
+    提交上报: 'primary'
+  }
+  return tagTypeMap[status] || 'info'
+}
+const getNodeStatusColor = (status) => {
+  const colorMap = {
+    待处理: '#E6A23C',
+    处理中: '#409EFF',
+    已完成: '#67C23A',
+    已驳回: '#F56C6C',
+    已撤销: '#909399',
+    已批准: '#67C23A',
+    审批中: '#409EFF',
+    上报: '#229242',
+    提交上报: '#409EFF'
+  }
+  return colorMap[status] || '#90939'
+}
+
 const handleCurrentChange = (val) => {
   queryParams.value.pageNo = val
   getList()
@@ -659,4 +706,139 @@ onUnmounted(() => {
     width: 90% !important;
   }
 }
+
+.drawer-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 20px;
+  padding-left: 20px;
+  width: 108%;
+  margin-left: -15px;
+}
+
+.approval-process-container {
+  padding: 10px 15px;
+
+  .no-data {
+    text-align: center;
+    padding: 40px 0;
+  }
+
+  .approval-card {
+    border: 1px solid #ebeef5;
+    border-radius: 6px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+    transition: all 0.3s ease;
+
+    &.status-completed {
+      border-left: 4px solid #67c23a;
+    }
+
+    &:hover {
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+    }
+
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding-bottom: 12px;
+      border-bottom: 1px dashed #ebeef5;
+
+      .node-info {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+
+        .status-tag {
+          font-weight: bold;
+        }
+
+        .node-name {
+          font-weight: 600;
+          color: #303133;
+        }
+      }
+
+      .operator-time {
+        .time {
+          color: #909399;
+          font-size: 13px;
+        }
+      }
+    }
+
+    .card-content {
+      padding: 12px 0;
+
+      .label {
+        font-weight: 600;
+        color: #606266;
+        display: inline-block;
+        width: 70px;
+        vertical-align: top;
+      }
+
+      .opinion,
+      .description {
+        color: #303133;
+        line-height: 1.6;
+      }
+
+      .attachment-section {
+        margin-top: 8px;
+
+        .attachments {
+          display: flex;
+          flex-direction: column;
+          gap: 5px;
+          margin-top: 5px;
+
+          .attachment-link {
+            display: inline-flex;
+            align-items: center;
+            padding-left: 70px;
+            text-decoration: none;
+
+            &:hover {
+              text-decoration: underline;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .approval-process-container {
+    padding: 5px;
+
+    .approval-card {
+      .card-header {
+        flex-direction: column;
+        gap: 8px;
+        align-items: flex-start;
+
+        .node-info {
+          flex-wrap: wrap;
+        }
+      }
+
+      .card-content {
+        .label {
+          width: auto;
+          display: block;
+          margin-bottom: 4px;
+        }
+
+        .attachment-section .attachment-link {
+          padding-left: 0;
+        }
+      }
+    }
+  }
+}
 </style>

+ 304 - 7
src/views/pms/qhse/faultReport/index.vue

@@ -63,7 +63,7 @@
         height="70vh"
         :max-height="tableHeight"
       >
-        <el-table-column prop="actualTime" label="事件时间" align="center" min-width="120">
+        <el-table-column prop="actualTime" label="事件时间" align="center" min-width="150">
           <template #default="{ row }">
             {{ formatDate(row.actualTime) }}
           </template>
@@ -92,8 +92,13 @@
         />
         <el-table-column prop="deptName" label="部门名称" align="center" />
         <el-table-column prop="dutyPerson" label="现场负责人" align="center" width="100" />
+        <el-table-column prop="actualTime" label="创建时间" align="center" min-width="150">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </template>
+        </el-table-column>
 
-        <el-table-column label="操作" align="center" width="200" fixed="right">
+        <el-table-column label="操作" align="center" width="150" fixed="right">
           <template #default="{ row }">
             <!-- <el-button
               link
@@ -111,22 +116,23 @@
               v-hasPermi="['rq:iot-accident-report:query']"
               >详情</el-button
             >
-            <el-button
+            <el-button link type="primary" @click="openApprovalDialog(row)"> 流转信息 </el-button>
+            <!-- <el-button
               link
               type="primary"
               @click="openForm('update', row)"
               :icon="Edit"
               v-hasPermi="['rq:iot-accident-report:update']"
               >编辑</el-button
-            >
-            <el-button
+            > -->
+            <!-- <el-button
               link
               type="danger"
               @click="handleDelete(row)"
               :icon="Delete"
               v-hasPermi="['rq:iot-accident-report:delete']"
               >删除</el-button
-            >
+            > -->
           </template>
         </el-table-column>
       </el-table>
@@ -326,13 +332,95 @@
         </div>
       </template>
     </el-dialog>
+
+    <el-drawer
+      :title="approvalDialogTitle"
+      v-model="approvalDialogVisible"
+      direction="rtl"
+      size="650px"
+      :with-header="true"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <template #header>
+        <div class="drawer-header">
+          <span>{{ approvalDialogTitle }}</span>
+        </div>
+      </template>
+
+      <!-- 专业化的审批流程时间线 -->
+      <div class="approval-process-container">
+        <el-timeline v-if="approvalProcessList.length > 0">
+          <el-timeline-item
+            v-for="(item, index) in approvalProcessList"
+            :key="index"
+            placement="top"
+            :color="getNodeStatusColor(item.status)"
+            :icon="Check"
+          >
+            <el-card
+              shadow="never"
+              class="approval-card"
+              :class="{ 'status-completed': item.status === '已完成' || item.status === '已批准' }"
+            >
+              <div class="card-header">
+                <div class="node-info">
+                  <el-tag :type="getTagTypeByStatus(item.status)" size="small" class="status-tag">
+                    {{ item.status }}
+                  </el-tag>
+                  <span class="node-name">{{ item.nodeName }}</span>
+                </div>
+                <div class="operator-time">
+                  <span class="time">{{ formatDate(item.createTime) }}</span>
+                </div>
+              </div>
+
+              <div class="card-content">
+                <div class="opinion-section">
+                  <span class="label">操作人:</span>
+                  <span class="opinion">{{ item.operator }}</span>
+                </div>
+
+                <div class="description-section mt-1" v-if="item.description">
+                  <span class="label">审批意见:</span>
+                  <span class="description">{{ item.description }}</span>
+                </div>
+                <div
+                  class="attachment-section"
+                  v-if="item.attachments && item.attachments.length > 0"
+                >
+                  <span class="label">附件:</span>
+                  <div class="attachments">
+                    <el-link
+                      v-for="(attachment, idx) in item.attachments"
+                      :key="idx"
+                      type="primary"
+                      :href="attachment.url"
+                      target="_blank"
+                      class="attachment-link"
+                    >
+                      {{ attachment.name }}
+                    </el-link>
+                  </div>
+                </div>
+              </div>
+            </el-card>
+          </el-timeline-item>
+        </el-timeline>
+
+        <!-- 无数据提示 -->
+        <div v-else class="no-data">
+          <el-empty description="暂无审批流程信息" :image-size="100" />
+        </div>
+      </div>
+    </el-drawer>
   </div>
 </template>
 
 <script setup>
 import { ref, reactive, onMounted, computed } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { Search, Refresh, Plus, Edit, Delete, TopRight, View } from '@element-plus/icons-vue'
+import { Search, Refresh, Plus, Edit, Delete, TopRight, View, Check } from '@element-plus/icons-vue'
 import { defaultProps } from '@/utils/tree'
 import { handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
@@ -340,6 +428,7 @@ import { IotFailureApi } from '@/api/pms/qhse/index'
 import UploadImage from '@/components/UploadFile/src/UploadImg.vue'
 import { formatDate } from '@/utils/formatTime'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { IotApprovalApi } from '@/api/pms/qhse/index'
 
 // Data
 const loading = ref(false)
@@ -617,6 +706,79 @@ const cancel = () => {
   reset()
 }
 
+const approvalDialogVisible = ref(false)
+const approvalDialogTitle = ref('审批流程')
+const approvalProcessList = ref([]) // 存储审批流程信息
+const approvalSuggestion = ref('')
+// 根据状态返回标签类型
+const getTagTypeByStatus = (status) => {
+  const tagTypeMap = {
+    待处理: 'warning',
+    处理中: 'info',
+    已完成: 'success',
+    已驳回: 'danger',
+    已撤销: 'info',
+    已批准: 'success',
+    审批中: 'primary',
+    上报: 'success',
+    提交上报: 'primary'
+  }
+  return tagTypeMap[status] || 'info'
+}
+const getNodeStatusColor = (status) => {
+  const colorMap = {
+    待处理: '#E6A23C',
+    处理中: '#409EFF',
+    已完成: '#67C23A',
+    已驳回: '#F56C6C',
+    已撤销: '#909399',
+    已批准: '#67C23A',
+    审批中: '#409EFF',
+    上报: '#229242',
+    提交上报: '#409EFF'
+  }
+  return colorMap[status] || '#90939'
+}
+
+const openApprovalDialog = async (row) => {
+  approvalDialogVisible.value = true
+  approvalDialogTitle.value = `审批流程详情`
+  approvalSuggestion.value = '' // 清空审批建议
+
+  try {
+    const response = await IotApprovalApi.getApprovalProcess(row.id)
+    const processList = response.list || []
+
+    // 按 createTime 升序排序(时间正序)
+    approvalProcessList.value = processList.sort((a, b) => a.createTime - b.createTime)
+
+    // 如果没有数据,添加一条默认的“发起人”记录
+    if (approvalProcessList.value.length === 0) {
+      approvalProcessList.value.push({
+        createTime: Date.now(),
+        operator: '系统管理员',
+        status: '提交上报',
+        description: '发起事故事件上报申请',
+        nodeName: '上报发起',
+        opinion: ''
+      })
+    }
+  } catch (error) {
+    ElMessage.error('获取审批流程失败')
+    // 即使出错也显示一个基本记录
+    approvalProcessList.value = [
+      {
+        createTime: Date.now(),
+        operator: '系统',
+        status: '错误',
+        description: '无法获取审批流程信息',
+        nodeName: '系统通知',
+        opinion: ''
+      }
+    ]
+  }
+}
+
 // 监听窗口大小变化
 const handleResize = () => {
   // 这里可以添加响应式逻辑
@@ -695,4 +857,139 @@ onUnmounted(() => {
 ::v-deep .el-button {
   border-radius: 3px;
 }
+
+.drawer-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 20px;
+  padding-left: 20px;
+  width: 108%;
+  margin-left: -15px;
+}
+
+.approval-process-container {
+  padding: 10px 15px;
+
+  .no-data {
+    text-align: center;
+    padding: 40px 0;
+  }
+
+  .approval-card {
+    border: 1px solid #ebeef5;
+    border-radius: 6px;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+    transition: all 0.3s ease;
+
+    &.status-completed {
+      border-left: 4px solid #67c23a;
+    }
+
+    &:hover {
+      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+    }
+
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding-bottom: 12px;
+      border-bottom: 1px dashed #ebeef5;
+
+      .node-info {
+        display: flex;
+        align-items: center;
+        gap: 10px;
+
+        .status-tag {
+          font-weight: bold;
+        }
+
+        .node-name {
+          font-weight: 600;
+          color: #303133;
+        }
+      }
+
+      .operator-time {
+        .time {
+          color: #909399;
+          font-size: 13px;
+        }
+      }
+    }
+
+    .card-content {
+      padding: 12px 0;
+
+      .label {
+        font-weight: 600;
+        color: #606266;
+        display: inline-block;
+        width: 70px;
+        vertical-align: top;
+      }
+
+      .opinion,
+      .description {
+        color: #303133;
+        line-height: 1.6;
+      }
+
+      .attachment-section {
+        margin-top: 8px;
+
+        .attachments {
+          display: flex;
+          flex-direction: column;
+          gap: 5px;
+          margin-top: 5px;
+
+          .attachment-link {
+            display: inline-flex;
+            align-items: center;
+            padding-left: 70px;
+            text-decoration: none;
+
+            &:hover {
+              text-decoration: underline;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+@media (max-width: 768px) {
+  .approval-process-container {
+    padding: 5px;
+
+    .approval-card {
+      .card-header {
+        flex-direction: column;
+        gap: 8px;
+        align-items: flex-start;
+
+        .node-info {
+          flex-wrap: wrap;
+        }
+      }
+
+      .card-content {
+        .label {
+          width: auto;
+          display: block;
+          margin-bottom: 4px;
+        }
+
+        .attachment-section .attachment-link {
+          padding-left: 0;
+        }
+      }
+    }
+  }
+}
 </style>

+ 8 - 6
src/views/pms/video_center/sip/components/player/DeviceTree.vue

@@ -20,7 +20,7 @@
           <span
             v-if="data.type === 0 && data.online"
             title="在线设备"
-            class="device-online iconfont icon-jiedianleizhukongzhongxin2"
+            class="device-online iconfont icon-jiedianleizhukongzhongxin2 w-5 h-5 rounded-full bg-green-500"
           ></span>
           <span
             v-if="data.type === 0 && !data.online"
@@ -30,7 +30,7 @@
           <span
             v-if="data.type === 3 && data.online"
             title="在线通道"
-            class="device-online iconfont icon-shebeileijiankongdian"
+            class="device-online iconfont icon-shebeileijiankongdian w-5 h-5 rounded-full bg-green-500"
           ></span>
           <span
             v-if="data.type === 3 && !data.online"
@@ -67,9 +67,11 @@
             title="离线通道-枪机"
             class="device-offline iconfont icon-shebeileiqiangjitongdao"
           ></span>
-          <span v-if="data.online" style="padding-left: 1px" class="device-online">{{
-            node.label
-          }}</span>
+
+          <span v-if="data.online" style="padding-left: 1px" class="device-online"
+            >{{ node.label }}
+          </span>
+
           <span v-if="!data.online" style="padding-left: 1px" class="device-offline">{{
             node.label
           }}</span>
@@ -237,7 +239,7 @@ const channelDataHandler = (data, resolve) => {
         id: item.id,
         deviceId: item.deviceId,
         type: type,
-        online: item.status === 3,
+        online: item.basicData.status === 3,
         userData: item.basicData
       }
 

+ 1 - 1
src/views/pms/video_center/sip/components/player/deviceLiveStream.vue

@@ -37,7 +37,7 @@
       ref="playerRef"
       :playerInfo="playinfo"
       class="components-container"
-      style="margin-top: 10px"
+      style="margin-top: 10px; width: 90%; height: 80vh"
     />
   </div>
 </template>

+ 1 - 1
src/views/pms/video_center/sip/components/player/easy.vue

@@ -47,7 +47,7 @@ const config = reactive({
   hasAudio: true,
   isLive: true,
   MSE: false,
-  WCS: false
+  WCS: true
 })
 
 const playCreate = () => {

+ 18 - 20
src/views/pms/video_center/sip/index.vue

@@ -38,7 +38,7 @@
         </el-form-item>
         <el-form-item>
           <el-button type="primary" :icon="Search" size="default" @click="handleQuery"
-            >搜</el-button
+            >搜</el-button
           >
           <el-button :icon="Refresh" size="default" @click="resetQuery">重置</el-button>
         </el-form-item>
@@ -49,14 +49,7 @@
         <el-button type="primary" plain :icon="Plus" size="small" @click="handleAdd">
           添加
         </el-button>
-        <el-button
-          type="danger"
-          plain
-          :icon="Delete"
-          size="small"
-          :disabled="multiple || isGeneralUser"
-          @click="handleDelete"
-        >
+        <el-button type="danger" plain :icon="Delete" size="small" @click="handleDelete">
           删除
         </el-button>
       </div>
@@ -65,9 +58,10 @@
         :data="sipidList"
         @selection-change="handleSelectionChange"
         @cell-dblclick="celldblclick"
+        :header-cell-style="{ background: '#f5f5f5' }"
       >
         <el-table-column type="selection" :selectable="selectable" width="55" align="center" />
-        <el-table-column label="设备编号" align="center" prop="deviceSipId">
+        <el-table-column label="设备编号" align="center" prop="deviceSipId" min-width="120">
           <template #default="scope">
             <el-link
               :underline="false"
@@ -77,7 +71,12 @@
             >
           </template>
         </el-table-column>
-        <el-table-column :label="t('sip.index998533-2')" align="center" prop="channelSipId" />
+        <el-table-column
+          :label="t('sip.index998533-2')"
+          align="center"
+          prop="channelSipId"
+          min-width="120"
+        />
         <el-table-column :label="t('sip.index998533-4')" align="center" prop="status" width="80">
           <template #default="scope">
             <dict-tag
@@ -139,6 +138,7 @@
             v-model="createForm.city"
             @change="changeProvince"
             :props="{ checkStrictly: true }"
+            style="width: 100%"
           />
         </el-form-item>
         <el-form-item :label="t('sip.index998533-9')" prop="deviceType">
@@ -178,7 +178,7 @@
             v-model="createForm.createNum"
             :max="10"
             :placeholder="t('sip.index998533-19')"
-            style="width: 330px"
+            style="width: 100%"
           />
         </el-form-item>
       </el-form>
@@ -190,6 +190,7 @@
       </template>
     </el-dialog>
     <el-dialog :title="title" v-model="bindingOpen" width="450px" append-to-body>
+      <div class="mt-2"></div>
       <el-form :model="form" ref="formRef">
         <el-form-item :label="t('sip.index998533-22')" prop="deviceId">
           <el-select
@@ -208,7 +209,7 @@
             />
           </el-select>
         </el-form-item>
-        <el-form-item :label="t('sip.index998533-24')" prop="sceneId">
+        <!-- <el-form-item :label="t('sip.index998533-24')" prop="sceneId">
           <el-select
             style="width: 210px"
             v-model="form.reSceneModelId"
@@ -223,7 +224,7 @@
               :value="item.sceneModelId"
             />
           </el-select>
-        </el-form-item>
+        </el-form-item> -->
       </el-form>
       <template #footer>
         <div class="dialog-footer">
@@ -591,11 +592,8 @@ const getDeviceList = () => {
     pageSize: 9999
   }
   listDeviceShort(params).then((res) => {
-    if (res.code === 200) {
-      deviceList.value = res.rows
-    } else {
-      // $message.error(res.msg)
-    }
+    console.log('*******************************', res)
+    deviceList.value = res
   })
 }
 
@@ -737,7 +735,7 @@ onMounted(() => {
   //     isGeneralUser.value = false
   // }
   getList()
-  // getDeviceList()
+  getDeviceList()
   // getSceneListDatas()
 })
 </script>

+ 168 - 19
src/views/pms/video_center/sip/splitview.vue

@@ -3,18 +3,23 @@
     <el-card
       id="devicePosition"
       shadow="never"
-      style="height: calc(100% - 40px); border: 0"
+      style="height: calc(100%); border: 0"
       :body-style="{ padding: '0px' }"
       class="border-none"
     >
       <el-container style="height: 100%">
-        <el-aside width="250px" style="background-color: #ffffff">
+        <!-- 左侧设备列表 -->
+        <el-aside width="250px" style="background-color: #ffffff" :class="{ hidden: isFullscreen }">
           <DeviceTree :click-event="clickEvent" />
         </el-aside>
-        <el-main style="padding: 0">
+
+        <!-- 主体内容 -->
+        <div style="padding: 0" class="el-main">
+          <!-- 上方分屏按钮 -->
           <div
             height="5vh"
             style="text-align: left; font-size: 17px; line-height: 5vh; margin-bottom: 10px"
+            :class="{ hidden: isFullscreen }"
           >
             {{ t('sip.splitview998531-1') }}
             <el-button
@@ -50,9 +55,17 @@
             >
               {{ t('sip.splitview998531-4') }}
             </el-button>
+
+            <el-button @click="toggleFullscreen" :type="isFullscreen ? 'danger' : 'primary'">
+              {{ isFullscreen ? '退出全屏' : '全屏' }}
+            </el-button>
           </div>
-          <div style="height: 85vh; display: flex; flex-wrap: wrap">
-            <!-- 渲染所有分屏数量的播放框 -->
+
+          <!-- 分屏播放区域 -->
+          <div
+            style="height: 85vh; display: flex; flex-wrap: wrap"
+            :class="{ 'fullscreen-layout': isFullscreen }"
+          >
             <div
               v-for="i in spilt"
               :key="i"
@@ -61,14 +74,12 @@
               :class="{ redborder: playerIdx == i - 1 }"
               @click="playerIdx = i - 1"
             >
-              <!-- 显示序号,如果该位置没有视频或不在可用通道范围内 -->
               <div
                 v-if="!videoUrl[i - 1] || i - 1 >= availableChannels.length"
                 style="color: #ffffff; font-size: 30px; font-weight: bold"
               >
                 {{ i }}
               </div>
-              <!-- 只有当有视频URL时才渲染播放器 -->
               <player
                 v-if="videoUrl[i - 1]"
                 :ref="(el) => setPlayerRef(el, i - 1)"
@@ -87,7 +98,7 @@
               />
             </div>
           </div>
-        </el-main>
+        </div>
       </el-container>
     </el-card>
   </div>
@@ -132,14 +143,38 @@ const setPlayerRef = (el, index) => {
 
 // 计算属性 - 直播样式
 const liveStyle = computed(() => {
-  let style = { width: '81%', height: '99%' }
-  switch (spilt.value) {
-    case 4:
-      style = { width: '40%', height: '49%' }
-      break
-    case 9:
-      style = { width: '27%', height: '32%' }
-      break
+  let style = {}
+  // switch (spilt.value) {
+  //   case 4:
+  //     style = { width: '40%', height: '49%' }
+  //     break
+  //   case 9:
+  //     style = { width: '27%', height: '32%' }
+  //     break
+  // }
+
+  if (spilt.value === 1) {
+    if (isFullscreen.value) {
+      style = { width: '100%', height: '100%' }
+    } else {
+      style = { width: '81%', height: '99%', 'margin-right': '5px' }
+    }
+  }
+
+  if (spilt.value === 4) {
+    if (isFullscreen.value) {
+      style = { width: '50%', height: '50%' }
+    } else {
+      style = { width: '40%', height: '49%', 'margin-right': '5px' }
+    }
+  }
+
+  if (spilt.value === 9) {
+    if (isFullscreen.value) {
+      style = { width: '33.3%', height: '33.3%' }
+    } else {
+      style = { width: '27%', height: '32%', 'margin-right': '5px' }
+    }
   }
 
   nextTick(() => {
@@ -280,13 +315,76 @@ watch(
   }
 )
 
+// 全屏状态
+const isFullscreen = ref(false)
+
+// 切换全屏
+const toggleFullscreen = () => {
+  const container = document.getElementById('devicePosition') // 获取全屏容器
+  if (!container) return
+
+  if (!isFullscreen.value) {
+    // 进入全屏
+    if (container.requestFullscreen) {
+      container.requestFullscreen()
+    } else if (container.webkitRequestFullscreen) {
+      container.webkitRequestFullscreen() // Safari
+    } else if (container.mozRequestFullScreen) {
+      container.mozRequestFullScreen() // Firefox
+    } else if (container.msRequestFullscreen) {
+      container.msRequestFullscreen() // IE/Edge
+    }
+  } else {
+    // 退出全屏
+    if (document.exitFullscreen) {
+      document.exitFullscreen()
+    } else if (document.webkitExitFullscreen) {
+      document.webkitExitFullscreen() // Safari
+    } else if (document.mozCancelFullScreen) {
+      document.mozCancelFullScreen() // Firefox
+    } else if (document.msExitFullscreen) {
+      document.msExitFullscreen() // IE/Edge
+    }
+  }
+}
+
+// 监听全屏状态变化
+const handleFullscreenChange = () => {
+  isFullscreen.value = !!(
+    document.fullscreenElement ||
+    document.webkitFullscreenElement ||
+    document.mozFullScreenElement ||
+    document.msFullscreenElement
+  )
+
+  // 动态调整主内容区域宽度
+  const mainContent = document.querySelector('.el-main')
+  if (mainContent) {
+    if (isFullscreen.value) {
+      mainContent.style.width = '100vw'
+      mainContent.style.marginLeft = '0'
+    } else {
+      mainContent.style.width = ''
+      mainContent.style.marginLeft = ''
+    }
+  }
+}
+
 // 生命周期钩子
 onMounted(() => {
   checkPlayByParam()
+  document.addEventListener('fullscreenchange', handleFullscreenChange)
+  document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+  document.addEventListener('mozfullscreenchange', handleFullscreenChange)
+  document.addEventListener('MSFullscreenChange', handleFullscreenChange)
 })
 
 onUnmounted(() => {
   clearTimeout(updateLooper.value)
+  document.removeEventListener('fullscreenchange', handleFullscreenChange)
+  document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
+  document.removeEventListener('mozfullscreenchange', handleFullscreenChange)
+  document.removeEventListener('MSFullscreenChange', handleFullscreenChange)
 })
 
 // 销毁事件
@@ -315,7 +413,9 @@ const clickEvent = async (data) => {
     loading.value = true
     try {
       const channels = await getDeviceChannels(deviceId)
-      const cameraChannels = channels.filter((channel) => channel.basicData.model === 'Camera')
+      const cameraChannels = channels.filter(
+        (channel) => channel.basicData.model === 'Camera' && channel.basicData.status === 3
+      )
       availableChannels.value = cameraChannels
 
       playerInfo.value = []
@@ -339,6 +439,11 @@ const clickEvent = async (data) => {
   }
   // 情况2:点击的是具体的通道节点
   else if (data.userData?.channelSipId) {
+    console.log('点击了通道节点,数据:', data)
+    if (data.online === false) {
+      ElMessage.warning('通道未在线')
+      return
+    }
     // 检查是否有选中的空闲槽位
     let targetIndex = selectedEmptySlot.value !== -1 ? selectedEmptySlot.value : playerIdx.value
 
@@ -528,9 +633,9 @@ const clear = (idx) => {
   display: flex;
   align-items: center;
   justify-content: center;
-  margin-right: 10px;
+  /* margin-right: 10px; */
   position: relative;
-  border-radius: 5px;
+  /* border-radius: 5px; */
 }
 
 .empty-box {
@@ -544,4 +649,48 @@ const clear = (idx) => {
   top: 0px;
   height: 100% !important;
 }
+
+.hidden {
+  display: none !important;
+}
+
+/* 全屏时隐藏滚动条 */
+:-webkit-full-screen {
+  overflow: hidden;
+}
+
+:-moz-full-screen {
+  overflow: hidden;
+}
+
+:-ms-fullscreen {
+  overflow: hidden;
+}
+
+/* :fullscreen {
+  height: 100vh !important;
+  width: 100vw !important;
+  margin: 0 !important;
+
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  overflow: hidden !important;
+  max-width: 100vw !important;
+  max-height: 100vh !important;
+} */
+
+.fullscreen-layout {
+  height: 100vh !important;
+  width: 100vw !important;
+  margin: 0 !important;
+  padding: 0 !important;
+  position: fixed !important;
+  top: 0 !important;
+  left: 0 !important;
+  right: 0 !important;
+  overflow: hidden !important;
+  max-width: 100vw !important;
+  max-height: 100vh !important;
+}
 </style>