Explorar el Código

Merge branch 'master' into refactor/productionStatus

Zimo hace 3 días
padre
commit
ca2ae2e636

+ 49 - 0
src/api/pms/qhse/index.ts

@@ -162,3 +162,52 @@ export const IotMeasureRecordApi = {
     return await request.download({ url: `/rq/iot-measure-record/export-excel`, params })
   }
 }
+
+// 危险源管理
+export const IotDangerApi = {
+  // 获得危险源分页
+  getDangerList: async (params) => {
+    return await request.get({ url: `/rq/iot-danger-source/page`, params })
+  },
+  // 删除危险源
+  deleteDanger: async (id) => {
+    return await request.delete({ url: `/rq/iot-danger-source/delete?id=` + id })
+  },
+  // 添加危险源
+  createDanger: async (data) => {
+    return await request.post({ url: `/rq/iot-danger-source/create`, data })
+  },
+  // 修改危险源
+  updateDanger: async (data) => {
+    return await request.put({ url: `/rq/iot-danger-source/update`, data })
+  },
+  // 导出危险源 Excel
+  exportDanger: async (params) => {
+    return await request.download({ url: `/rq/iot-danger-source/export-excel`, params })
+  }
+}
+
+// 环境因素识别
+export const IotEnvironmentApi = {
+  // 获得环境因素分页
+  getEnvironmentList: async (params) => {
+    return await request.get({ url: `/rq/iot-environment-recognize/page`, params })
+  },
+  // 删除环境因素
+  deleteEnvironment: async (id) => {
+    return await request.delete({ url: `/rq/iot-environment-recognize/delete?id=` + id })
+  },
+
+  // 添加环境因素
+  createEnvironment: async (data) => {
+    return await request.post({ url: `/rq/iot-environment-recognize/create`, data })
+  },
+  // 修改环境因素
+  updateEnvironment: async (data) => {
+    return await request.put({ url: `/rq/iot-environment-recognize/update`, data })
+  },
+  // 导出环境因素 Excel
+  exportEnvironment: async (params) => {
+    return await request.download({ url: `/rq/iot-environment-recognize/export-excel`, params })
+  }
+}

+ 7 - 1
src/utils/dict.ts

@@ -314,7 +314,13 @@ export enum DICT_TYPE {
 
   DEVICE_GROUP_TYPE = 'device_group_type',
   EVENT_TYPE = 'event_type',
-  EVENT_STATE = 'event_state'
+  EVENT_STATE = 'event_state',
+
+  // QHSE
+  MEASURE_TYPE = 'measure_type',
+  PERSON_CERT = 'person_cert',
+  ORG_CERT = 'org_cert',
+  DANGER_GRADE = 'danger_grade'
 }
 
 export function realValue(type: any, value: string) {

+ 9 - 0
src/views/pms/device/completeSet/DeviceCompleteSet.vue

@@ -64,6 +64,15 @@
             </template>
           </el-table-column>
 
+          <el-table-column label="在线状态" align="center">
+            <template #default="scope">
+              <el-tag type="success" :underline="false" v-if="scope.row.ifOnline === true"
+                >在线</el-tag
+              >
+              <el-tag type="info" :underline="false" v-else>离线</el-tag>
+            </template>
+          </el-table-column>
+
           <el-table-column label="描述" align="center" prop="remark" />
           <el-table-column label="设备数量" align="center" prop="deviceCount">
             <template #default="scope">

+ 371 - 240
src/views/pms/iotopeationfill/index.vue

@@ -6,62 +6,70 @@
       </ContentWrap>
     </el-col>
     <el-col :span="20" :xs="24">
-    <ContentWrap>
-      <!-- 搜索工作栏 -->
-      <el-form
-        class="-mb-15px"
-        :model="queryParams"
-        ref="queryFormRef"
-        :inline="true"
-        label-width="68px"
-      >
-        <el-form-item :label="t('operationFill.name')" prop="orderName" style="margin-left: 15px">
-          <el-input
-            v-model="queryParams.orderName"
-            :placeholder="t('operationFill.nameHolder')"
-            clearable
-            @keyup.enter="handleQuery"
-            class="!w-240px"
-          />
-        </el-form-item>
-        <el-form-item :label="t('operationFill.status')" prop="orderStatus">
-          <el-select
-            v-model="queryParams.orderStatus"
-            :placeholder="t('operationFill.status')"
-            clearable
-            class="!w-240px"
+      <ContentWrap>
+        <!-- 搜索工作栏 -->
+        <el-form
+          class="-mb-15px"
+          :model="queryParams"
+          ref="queryFormRef"
+          :inline="true"
+          label-width="68px"
+        >
+          <el-form-item :label="t('operationFill.name')" prop="orderName" style="margin-left: 15px">
+            <el-input
+              v-model="queryParams.orderName"
+              :placeholder="t('operationFill.nameHolder')"
+              clearable
+              @keyup.enter="handleQuery"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item :label="t('operationFill.status')" prop="orderStatus">
+            <el-select
+              v-model="queryParams.orderStatus"
+              :placeholder="t('operationFill.status')"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+            :label="t('operationFill.createTime')"
+            prop="createTime"
+            label-width="100px"
           >
-            <el-option
-              v-for="dict in getStrDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              :start-placeholder="t('operationFill.start')"
+              :end-placeholder="t('operationFill.end')"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-220px"
             />
-          </el-select>
-        </el-form-item>
-        <el-form-item :label="t('operationFill.createTime')" prop="createTime" label-width="100px">
-          <el-date-picker
-            v-model="queryParams.createTime"
-            value-format="YYYY-MM-DD HH:mm:ss"
-            type="daterange"
-            :start-placeholder="t('operationFill.start')"
-            :end-placeholder="t('operationFill.end')"
-            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-            class="!w-220px"
-          />
-        </el-form-item>
-        <el-form-item>
-          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />{{t('operationFill.search')}}</el-button>
-          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />{{t('operationFill.reset')}}</el-button>
-<!--          <el-button-->
-<!--            type="primary"-->
-<!--            plain-->
-<!--            @click="openForm('create')"-->
-<!--            v-hasPermi="['rq:iot-inspect-order:create']"-->
-<!--          >-->
-<!--            <Icon icon="ep:plus" class="mr-5px" /> 新增-->
-<!--          </el-button>-->
-<!--          <el-button
+          </el-form-item>
+          <el-form-item>
+            <el-button @click="handleQuery"
+              ><Icon icon="ep:search" class="mr-5px" />{{ t('operationFill.search') }}</el-button
+            >
+            <el-button @click="resetQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" />{{ t('operationFill.reset') }}</el-button
+            >
+            <!--          <el-button-->
+            <!--            type="primary"-->
+            <!--            plain-->
+            <!--            @click="openForm('create')"-->
+            <!--            v-hasPermi="['rq:iot-inspect-order:create']"-->
+            <!--          >-->
+            <!--            <Icon icon="ep:plus" class="mr-5px" /> 新增-->
+            <!--          </el-button>-->
+            <!--          <el-button
             type="success"
             plain
             @click="handleExport"
@@ -70,79 +78,151 @@
           >
             <Icon icon="ep:download" class="mr-5px" /> 导出
           </el-button>-->
-        </el-form-item>
-      </el-form>
-    </ContentWrap>
-
-    <!-- 列表 -->
-    <ContentWrap>
-      <el-table v-loading="loading" :data="list" :stripe="true"  >
-        <el-table-column :label="t('common.index')" min-width="60" align="center">
-          <template #default="scope">
-            {{ scope.$index + 1 }}
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('bomList.name')" align="center" prop="orderName" min-width="320"/>
-        <el-table-column :label="t('operationFill.duty')" align="center" prop="userName" min-width="100"/>
-        <el-table-column :label="t('operationFill.orderDevice')" align="center" prop="fillList" min-width="150" :show-overflow-tooltip="true"/>
-        <el-table-column :label="t('operationFill.status')" align="center" prop="orderStatus" min-width="120">
-          <template #default="scope">
-            <el-tooltip
-              v-if="scope.row.orderStatus === 3"
-              effect="dark"
-              :content="scope.row.reason"
-              placement="top"
-            >
-              <dict-tag :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS" :value="scope.row.orderStatus" />
-            </el-tooltip>
-            <dict-tag
-              v-else
-              :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS"
-              :value="scope.row.orderStatus"
-            />
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('operationFill.deviceCount')" align="center" prop="allDev" min-width="120">
-          <template #default="scope">
-            <el-tag  type="info"> {{scope.row.allDev}}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('operationFill.fillCount')" align="center" prop="fillDev" min-width="120">
-          <template #default="scope">
-            <el-tag  type="success"> {{scope.row.fillDev}}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column :label="t('operationFill.unFillCount')" align="center" prop="unFillDev" min-width="120">
-          <template #default="scope">
-            <el-tag  type="danger"> {{scope.row.unFillDev}}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column
-          :label="t('dict.createTime')"
-          align="center"
-          prop="createTime"
-          :formatter="dateFormatter"
-          min-width="170"
-        />
-        <el-table-column
-          :label="t('dict.fillTime')"
-          align="center"
-          prop="updateTime"
-          :formatter="dateFormatter"
-          min-width="170"
-        />
-        <el-table-column :label="t('operationFill.operation')" align="center" min-width="120px" fixed="right">
-          <template #default="scope">
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
 
-              <div v-if="scope.row.orderStatus == 0||scope.row.orderStatus == 2">
+      <!-- 列表 -->
+      <ContentWrap>
+        <el-table v-loading="loading" :data="list" :stripe="true">
+          <el-table-column :label="t('common.index')" min-width="60" align="center">
+            <template #default="scope">
+              {{ scope.$index + 1 }}
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('bomList.name')"
+            align="center"
+            prop="orderName"
+            min-width="320"
+          />
+          <el-table-column
+            :label="t('operationFill.duty')"
+            align="center"
+            prop="userName"
+            min-width="100"
+          />
+          <el-table-column
+            :label="t('operationFill.orderDevice')"
+            align="center"
+            prop="fillList"
+            min-width="150"
+          >
+            <template #default="scope">
+              <el-popover
+                style="padding: 0"
+                placement="left"
+                :width="300"
+                :hide-after="0"
+                trigger="hover"
+                popper-class="project-popover"
+              >
+                <template #reference>
+                  <span class="cursor-pointer">{{ truncateText(scope.row.fillList, 20) }}</span>
+                </template>
+                <div class="scrollable-tooltip">
+                  <p>{{ scope.row.fillList }}</p>
+                </div>
+              </el-popover>
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('operationFill.status')"
+            align="center"
+            prop="orderStatus"
+            min-width="120"
+          >
+            <template #default="scope">
+              <el-tooltip
+                v-if="scope.row.orderStatus === 3"
+                effect="dark"
+                :content="scope.row.reason"
+                placement="top"
+              >
+                <dict-tag
+                  :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS"
+                  :value="scope.row.orderStatus"
+                />
+              </el-tooltip>
+              <dict-tag
+                v-else
+                :type="DICT_TYPE.OPERATION_FILL_ORDER_STATUS"
+                :value="scope.row.orderStatus"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('operationFill.deviceCount')"
+            align="center"
+            prop="allDev"
+            min-width="120"
+          >
+            <template #default="scope">
+              <el-tag type="info"> {{ scope.row.allDev }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('operationFill.fillCount')"
+            align="center"
+            prop="fillDev"
+            min-width="120"
+          >
+            <template #default="scope">
+              <el-tag type="success"> {{ scope.row.fillDev }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('operationFill.unFillCount')"
+            align="center"
+            prop="unFillDev"
+            min-width="120"
+          >
+            <template #default="scope">
+              <el-tag type="danger"> {{ scope.row.unFillDev }}</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column
+            :label="t('dict.createTime')"
+            align="center"
+            prop="createTime"
+            :formatter="dateFormatter"
+            min-width="170"
+          />
+          <el-table-column
+            :label="t('dict.fillTime')"
+            align="center"
+            prop="updateTime"
+            :formatter="dateFormatter"
+            min-width="170"
+          />
+          <el-table-column
+            :label="t('operationFill.operation')"
+            align="center"
+            min-width="120px"
+            fixed="right"
+          >
+            <template #default="scope">
+              <div v-if="scope.row.orderStatus == 0 || scope.row.orderStatus == 2">
                 <el-button
                   link
                   type="primary"
-                  @click="openWrite(scope.row.deptId+','+scope.row.userId+','+scope.row.createTime+','+scope.row.id+','+scope.row.orderStatus)"
+                  @click="
+                    openWrite(
+                      scope.row.deptId +
+                        ',' +
+                        scope.row.userId +
+                        ',' +
+                        scope.row.createTime +
+                        ',' +
+                        scope.row.id +
+                        ',' +
+                        scope.row.orderStatus
+                    )
+                  "
                   v-hasPermi="['rq:iot-opeation-fill:update']"
                   v-if="scope.row.orderStatus !== 1"
                 >
-                  {{t('operationFill.fill')}}
+                  {{ t('operationFill.fill') }}
                 </el-button>
                 <el-button
                   link
@@ -151,82 +231,110 @@
                   v-if="scope.row.orderStatus !== 1"
                   @click="openDialog(scope.row.id)"
                 >
-                  {{t('operationFill.ignore')}}
+                  {{ t('operationFill.ignore') }}
                 </el-button>
               </div>
               <div v-else-if="scope.row.orderStatus === 3">
                 <el-button
                   link
                   type="success"
-                  @click="openWrite(scope.row.deptId+','+scope.row.userId+','+scope.row.createTime+','+scope.row.id+','+scope.row.orderStatus)"
+                  @click="
+                    openWrite(
+                      scope.row.deptId +
+                        ',' +
+                        scope.row.userId +
+                        ',' +
+                        scope.row.createTime +
+                        ',' +
+                        scope.row.id +
+                        ',' +
+                        scope.row.orderStatus
+                    )
+                  "
                 >
-                  {{t('operationFill.view')}}
+                  {{ t('operationFill.view') }}
                 </el-button>
               </div>
               <div v-else>
                 <el-button
                   link
                   type="primary"
-                  @click="openWrite(scope.row.deptId+','+scope.row.userId+','+scope.row.createTime+','+scope.row.id+','+0)"
+                  @click="
+                    openWrite(
+                      scope.row.deptId +
+                        ',' +
+                        scope.row.userId +
+                        ',' +
+                        scope.row.createTime +
+                        ',' +
+                        scope.row.id +
+                        ',' +
+                        0
+                    )
+                  "
                   v-hasPermi="['rq:iot-opeation-fill:update']"
                   v-if="isSameDay(scope.row.createTime)"
                 >
-                  {{t('fault.edit')}}
+                  {{ t('fault.edit') }}
                 </el-button>
                 <el-button
                   link
                   type="success"
-                  @click="openWrite(scope.row.deptId+','+scope.row.userId+','+scope.row.createTime+','+scope.row.id+','+scope.row.orderStatus)"
+                  @click="
+                    openWrite(
+                      scope.row.deptId +
+                        ',' +
+                        scope.row.userId +
+                        ',' +
+                        scope.row.createTime +
+                        ',' +
+                        scope.row.id +
+                        ',' +
+                        scope.row.orderStatus
+                    )
+                  "
                 >
-                  {{t('operationFill.view')}}
+                  {{ t('operationFill.view') }}
                 </el-button>
               </div>
 
-
-            <!-- 编辑按钮 -->
-
-
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-dialog
-        v-model="dialogVisible"
-        title="忽略理由"
-        :width="600"
-        :before-close="handleClose"
-        append-to-body
-        :close-on-click-modal="false"
-      >
-        <el-form
-          ref="reasonFormRef"
-          :model="form"
-          :rules="rules"
-          label-width="60px"
+              <!-- 编辑按钮 -->
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-dialog
+          v-model="dialogVisible"
+          title="忽略理由"
+          :width="600"
+          :before-close="handleClose"
+          append-to-body
+          :close-on-click-modal="false"
         >
-          <el-form-item label="理由" prop="reason">
-            <el-input
-              type="textarea"
-              v-model="form.reason"
-              placeholder="请输入忽略理由"
-              :rows="4"
-              resize="none"
-            />
-          </el-form-item>
-        </el-form>
-
-        <template #footer>
-          <el-button @click="handleCancel">取消</el-button>
-          <el-button type="primary" @click="handleConfirm">确定</el-button>
-        </template>
-      </el-dialog>
-      <!-- 分页 -->
-      <Pagination
-        :total="total"
-        v-model:page="queryParams.pageNo"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
-    </ContentWrap>
+          <el-form ref="reasonFormRef" :model="form" :rules="rules" label-width="60px">
+            <el-form-item label="理由" prop="reason">
+              <el-input
+                type="textarea"
+                v-model="form.reason"
+                placeholder="请输入忽略理由"
+                :rows="4"
+                resize="none"
+              />
+            </el-form-item>
+          </el-form>
+
+          <template #footer>
+            <el-button @click="handleCancel">取消</el-button>
+            <el-button type="primary" @click="handleConfirm">确定</el-button>
+          </template>
+        </el-dialog>
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
     </el-col>
   </el-row>
   <!-- 表单弹窗:添加/修改 -->
@@ -234,25 +342,23 @@
 </template>
 
 <script setup lang="ts">
-
-
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotInspectOrderApi, IotInspectOrderVO } from '@/api/pms/inspect/order'
 //import IotInspectOrderForm from './IotInspectOrderForm.vue'
-import {DICT_TYPE, getStrDictOptions} from "@/utils/dict";
-import DeptTree from "@/views/system/user/DeptTree.vue";
-import {onMounted, ref} from "vue";
-import {IotOpeationFillApi, IotOpeationFillVO} from "@/api/pms/iotopeationfill";
-import {useUserStore} from "@/store/modules/user";
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+import { onMounted, ref } from 'vue'
+import { IotOpeationFillApi, IotOpeationFillVO } from '@/api/pms/iotopeationfill'
+import { useUserStore } from '@/store/modules/user'
 const { push } = useRouter()
-const { query} = useRoute() // 查询参数
-const deptId= query.deptId;
-const orderStatus = query.orderStatus;
-const createTime = query.createTime;
+const { query } = useRoute() // 查询参数
+const deptId = query.deptId
+const orderStatus = query.orderStatus
+const createTime = query.createTime
 /** 巡检工单 列表 */
 defineOptions({ name: 'IotOpeationFill1' })
-const dialogVisible = ref(false);
+const dialogVisible = ref(false)
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
@@ -269,15 +375,14 @@ const queryParams = reactive({
   createTime: [],
   deptId: useUserStore().getUser.deptId,
   deviceIds: undefined,
-  orderStatus:undefined
+  orderStatus: undefined
   //userId:useUserStore().getUser.id
 })
 
-
 const form = reactive({
   id: undefined,
-  reason: '',
-});
+  reason: ''
+})
 
 // 表单验证规则
 const rules = {
@@ -285,45 +390,47 @@ const rules = {
     { required: true, message: '请输入忽略理由', trigger: 'blur' },
     { min: 2, message: '理由长度不能少于2个字符', trigger: 'blur' }
   ]
-};
+}
 // 打开对话框
-const openDialog = (id:number) => {
-  dialogVisible.value = true;
-  form.id = id;
-  form.reason = '';
-};
+const openDialog = (id: number) => {
+  dialogVisible.value = true
+  form.id = id
+  form.reason = ''
+}
+
+const truncateText = (text: string, maxLength: number) => {
+  if (!text) return ''
+  return text.length > maxLength ? text.substring(0, maxLength) + '...' : text
+}
 // 取消按钮处理
 const handleCancel = () => {
-  dialogVisible.value = false;
-  resetForm();
-};
+  dialogVisible.value = false
+  resetForm()
+}
 
 // 确定按钮处理
 const handleConfirm = async () => {
   // 表单验证
   try {
-    await reasonFormRef.value.validate();
+    await reasonFormRef.value.validate()
     // 验证通过,调用接口
     await IotOpeationFillApi.updateIotOpeationFill1(form)
-    ElMessage.success('操作成功');
-    dialogVisible.value = false;
-    resetForm();
+    ElMessage.success('操作成功')
+    dialogVisible.value = false
+    resetForm()
   } catch (error) {
-    return;
+    return
   }
-};
+}
 // 重置表单
 const resetForm = () => {
-  reasonFormRef.value?.resetFields();
-};
-const reasonFormRef = ref(null);
-
-
+  reasonFormRef.value?.resetFields()
+}
+const reasonFormRef = ref(null)
 
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中
 
-
 // 新增变量用于控制悬浮提示
 const hoverRowId = ref<number | null>(null)
 
@@ -347,8 +454,7 @@ const getTooltipContent = (row: IotOpeationFillVO) => {
   `
 }
 
-
- // 判断两个日期是否为同一天
+// 判断两个日期是否为同一天
 const isSameDay = (dateString) => {
   if (!dateString) return false
 
@@ -357,12 +463,13 @@ const isSameDay = (dateString) => {
   const today = new Date()
 
   // 比较年、月、日
-  return targetDate.getFullYear() === today.getFullYear() &&
+  return (
+    targetDate.getFullYear() === today.getFullYear() &&
     targetDate.getMonth() === today.getMonth() &&
     targetDate.getDate() === today.getDate()
+  )
 }
 
-
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
   await getList()
@@ -394,11 +501,11 @@ const resetQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (id?: number) => {
-  push({ name: 'InspectOrderDetail', params:{id} })
+  push({ name: 'InspectOrderDetail', params: { id } })
 }
 
 const openWrite = (id?: string) => {
-  push({ name: 'FillOrderInfo',params:{id}})
+  push({ name: 'FillOrderInfo', params: { id } })
 }
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
@@ -430,49 +537,73 @@ const handleExport = async () => {
 
 /** 初始化 **/
 onMounted(async () => {
-
   // 计算近一周时间
-  const end = new Date();
-  const start = new Date();
-  start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000);
+  const end = new Date()
+  const start = new Date()
+  start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000)
 
   // 格式化日期为后端需要的格式
   const formatDate = (date) => {
-    const year = date.getFullYear();
-    const month = String(date.getMonth() + 1).padStart(2, '0');
-    const day = String(date.getDate()).padStart(2, '0');
-    const hours = String(date.getHours()).padStart(2, '0');
-    const minutes = String(date.getMinutes()).padStart(2, '0');
-    const seconds = String(date.getSeconds()).padStart(2, '0');
-    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
-  };
+    const year = date.getFullYear()
+    const month = String(date.getMonth() + 1).padStart(2, '0')
+    const day = String(date.getDate()).padStart(2, '0')
+    const hours = String(date.getHours()).padStart(2, '0')
+    const minutes = String(date.getMinutes()).padStart(2, '0')
+    const seconds = String(date.getSeconds()).padStart(2, '0')
+    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+  }
 
-  queryParams.createTime = [formatDate(start), formatDate(end)];
+  queryParams.createTime = [formatDate(start), formatDate(end)]
 
-  if(deptId != null){
-    queryParams.deptId = deptId;
+  if (deptId != null) {
+    queryParams.deptId = deptId
   }
-  if(orderStatus === 4){
+  if (orderStatus === 4) {
     queryParams.orderStatus = null
   }
-  if(orderStatus != null && orderStatus != 4){
-    queryParams.orderStatus = orderStatus;
+  if (orderStatus != null && orderStatus != 4) {
+    queryParams.orderStatus = orderStatus
   }
-  if(createTime){
-    const timeArr = createTime.split(',');
+  if (createTime) {
+    const timeArr = createTime.split(',')
     if (timeArr.length === 2) {
-      queryParams.createTime = timeArr;
+      queryParams.createTime = timeArr
     } else {
       // 处理格式不正确的情况,可以给个默认值或提示
-      console.warn('createTime参数格式不正确');
+      console.warn('createTime参数格式不正确')
     }
     //queryParams.createTime = createTime;
   }
 
-
-
-
-
   getList()
 })
 </script>
+
+<style lang="scss">
+.scrollable-tooltip {
+  min-height: 100px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  /* white-space: pre-wrap;
+  word-break: break-word; */
+  background-color: rgba(0, 0, 0, 0.8);
+  color: #fff;
+  font-size: 14px;
+}
+.project-popover.el-popover.el-popper {
+  opacity: 0.8;
+  background: #000000;
+  border: none !important;
+  font-size: 14px;
+  color: #ffffff;
+  font-weight: 400;
+  min-width: 54px;
+
+  .el-popper__arrow::before {
+    background: #000000;
+    border: none;
+  }
+}
+</style>
+
+<style scoped lang="scss"></style>

+ 1 - 1
src/views/pms/iotrydailyreport/ry-form.vue

@@ -356,7 +356,7 @@ const submitForm = async () => {
     FORM_KEYS.forEach((key) => (submitData[key] = form.value[key]))
     submitData.fillOrderCreateTime = form.value.createTime
     submitData.projectClassification = '1'
-    if (props.type === 'edit') {
+    if (props.type === 'edit' && props.noValidateStatus) {
       submitData.editFlag = 'Y'
     }
 

+ 1 - 1
src/views/pms/iotrydailyreport/ry-xj-form.vue

@@ -329,7 +329,7 @@ const submitForm = async () => {
     FORM_KEYS.forEach((key) => (submitData[key] = form.value[key]))
     submitData.fillOrderCreateTime = form.value.createTime
     submitData.projectClassification = '2'
-    if (props.type === 'edit') {
+    if (props.type === 'edit' && props.noValidateStatus) {
       submitData.editFlag = 'Y'
     }
 

+ 13 - 0
src/views/pms/maintain/index.vue

@@ -50,6 +50,19 @@
               :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
             />
           </el-form-item>
+          <el-form-item label="创建时间" label-width="70px" prop="createTime">
+            <el-date-picker
+              size="small"
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              class="!w-220px"
+              :clearable="true"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            />
+          </el-form-item>
           <el-form-item :label="t('maintain.status')" label-width="40px" prop="status">
             <el-select
               v-model="queryParams.status"

+ 74 - 22
src/views/pms/monitor/index.vue

@@ -49,25 +49,38 @@
               :lg="6"
               class="mt-2"
             >
-              <el-card shadow="hover" class="device-card" style="border: 0">
+              <el-card shadow="hover" class="device-card hhh rounded-sm">
                 <div class="card-header">
                   <div class="flex justify-between w-full">
                     <div>
                       <div class="card-title">{{ item.name }}</div>
                       <div class="card-dept">{{ item.deptName }}</div>
                     </div>
-                    <el-image
-                      v-if="item.type === '1'"
-                      :src="img1"
-                      style="width: 40px; height: 40px"
-                    />
-                    <el-image v-else :src="img2" style="width: 40px; height: 40px" />
+                    <el-tag
+                      :type="getOnlineStatusTagType(item.ifOnline)"
+                      size="small"
+                      class="status-tag"
+                    >
+                      {{ item.ifOnline ? '在线' : '离线' }}
+                    </el-tag>
                   </div>
                 </div>
-                <div class="card-body">
+                <div class="card-body" style="margin-top: -20px">
                   <div class="card-row">
                     <span class="muted">成套类型:</span>
-                    <dict-tag :type="DICT_TYPE.DEVICE_GROUP_TYPE" :value="item.type" />
+                    <span class="type-with-icon">
+                      <dict-tag :type="DICT_TYPE.DEVICE_GROUP_TYPE" :value="item.type" />
+                      <el-image
+                        v-if="item.type === '1'"
+                        :src="img1"
+                        style="width: 40px; height: 40px; margin-left: 40px"
+                      />
+                      <el-image
+                        v-else
+                        :src="img2"
+                        style="width: 40px; height: 40px; margin-left: 40px"
+                      />
+                    </span>
                   </div>
                   <div class="card-row"
                     ><span class="muted">设备数量:</span>
@@ -83,10 +96,18 @@
                         : '无'
                     }}
                   </div>
+                  <div class="card-row">
+                    <span class="muted">在线状态:</span>
+                    <el-tag :type="getOnlineStatusTagType(item.ifOnline)" size="small">
+                      {{ item.ifOnline ? '在线' : '离线' }}
+                    </el-tag>
+                  </div>
                   <!-- <div class="card-row"><span class="muted">描述:</span> {{ item.remark }}</div> -->
                 </div>
                 <template #footer>
-                  <el-button type="primary" link @click="handleEdit(item)"> 查看详情 </el-button>
+                  <el-button type="primary" link @click="handleEdit(item)"
+                    ><Icon icon="ep:view" class="mr-5px" /> 查看详情
+                  </el-button>
                 </template>
               </el-card>
             </el-col>
@@ -153,6 +174,11 @@ const formData = ref({
   remark: ''
 })
 
+// 获取在线状态的标签类型
+const getOnlineStatusTagType = (ifOnline: boolean) => {
+  return ifOnline ? 'success' : 'info'
+}
+
 // 表单验证规则
 const formRules = {
   name: [{ required: true, message: '成套名称不能为空', trigger: 'blur' }],
@@ -391,7 +417,17 @@ onMounted(async () => {
 })
 </script>
 
-<style scoped>
+<style scoped lang="scss">
+::v-deep .el-card {
+  border-radius: 5px !important;
+}
+
+::v-deep .hhh.el-card {
+  background-color: transparent !important;
+  border-radius: 5px !important;
+  overflow: hidden;
+  box-shadow: 0 6px 18px rgba(28, 39, 63, 0.06);
+}
 .transfer-container {
   display: flex;
   flex-direction: column;
@@ -429,11 +465,15 @@ onMounted(async () => {
   border-radius: 12px;
   overflow: hidden;
   box-shadow: 0 6px 18px rgba(28, 39, 63, 0.06);
+  /* 渐变蓝背景 */
+  background: linear-gradient(to top, #fff 0%, #effaff 100%);
+  color: #717e9d !important;
 }
 
 /* 移除 el-card 默认内边距,统一由子元素控制 */
 .device-card ::v-deep .el-card__body {
   padding: 0;
+  color: #717e9d !important;
 }
 
 .device-card .card-header {
@@ -441,20 +481,24 @@ onMounted(async () => {
   justify-content: space-between;
   align-items: center;
   padding: 18px 16px;
-  background: linear-gradient(90deg, #2dd4bf 0%, #0ea5e9 100%);
-  color: #fff;
 }
 
 .device-card .card-title {
   font-weight: 700;
   font-size: 16px;
   line-height: 1.2;
+  color: #34475c;
 }
 
 .device-card .card-dept {
   font-size: 12px;
-  opacity: 0.9;
+  opacity: 0.95;
   margin-top: 6px;
+  color: #34475c;
+}
+
+.device-card .status-tag {
+  margin-bottom: 8px;
 }
 
 .device-card .card-status {
@@ -466,15 +510,16 @@ onMounted(async () => {
 }
 
 .device-card .card-body {
-  background: #fff;
+  background: transparent;
   padding: 14px 16px;
+  font-size: 14px;
+  color: #34475c;
 }
 
 .device-card .card-row {
   display: flex;
   align-items: center;
-  padding: 12px 0;
-  border-bottom: 1px solid #f5f7fa;
+  padding: 10px 0;
 }
 
 .device-card .card-row:last-child {
@@ -482,13 +527,18 @@ onMounted(async () => {
 }
 
 .device-card .card-row .muted {
-  color: #97a0b5;
+  color: #34475c;
   width: 86px;
   flex-shrink: 0;
 }
 
+.device-card .type-with-icon {
+  display: inline-flex;
+  align-items: center;
+}
+
 .device-card .card-row .value {
-  color: #2b3a4a;
+  color: #fff;
   flex: 1;
 }
 
@@ -497,16 +547,18 @@ onMounted(async () => {
   justify-content: space-between;
   align-items: center;
   padding: 12px 16px 16px;
-  background: #fff;
+  background: transparent;
+  color: #fff;
 }
 
 .device-card .detail-link {
-  color: var(--el-color-primary, #409eff);
+  color: #fff;
   font-weight: 500;
 }
 
 .device-card .id-label {
-  color: #97a0b5;
+  color: rgba(255, 255, 255, 0.85);
+  font-size: 12px;
   font-size: 12px;
 }
 </style>

+ 109 - 38
src/views/pms/qhse/certificate.vue

@@ -22,14 +22,18 @@
           <el-form-item label="证书类别" prop="classify">
             <el-select
               v-model="queryParams.classify"
-              placeholder="请选择证书类别"
-              style="width: 150px"
+              placeholder="证书类别"
+              clearable
+              class="!w-240px"
             >
-              <el-option label="职业资格证" value="professional" />
-              <el-option label="技能证书" value="skill" />
-              <el-option label="学历证书" value="education" />
-              <el-option label="荣誉证书" value="honor" />
-              <el-option label="其他" value="other" />
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT).concat(
+                  getStrDictOptions(DICT_TYPE.ORG_CERT)
+                )"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
             </el-select>
           </el-form-item>
 
@@ -43,7 +47,7 @@
             <el-button @click="resetQuery"
               ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
             >
-            <el-button @click="handleExport" type="success" plain
+            <el-button @click="handleExport" type="success" plain :loading="exportLoading"
               ><Icon icon="ep:download" class="mr-5px" /> 导出Excel</el-button
             >
           </el-form-item>
@@ -65,9 +69,14 @@
             </template>
           </el-table-column>
 
-          <el-table-column label="证书类别" align="center" prop="classify">
+          <el-table-column label="证书类别" align="center" width="150" prop="classify">
             <template #default="scope">
-              {{ getCertificateCategoryText(scope.row.classify) }}
+              <dict-tag
+                v-if="scope.row.type === 'organization'"
+                :type="DICT_TYPE.ORG_CERT"
+                :value="scope.row.classify"
+              />
+              <dict-tag v-else :type="DICT_TYPE.PERSON_CERT" :value="scope.row.classify" />
             </template>
           </el-table-column>
 
@@ -137,25 +146,65 @@
       v-loading="formLoading"
     >
       <el-form-item label="证书类型" prop="type">
-        <el-select v-model="formData.type" placeholder="请选择证书类型">
+        <el-select
+          v-model="formData.type"
+          placeholder="请选择证书类型"
+          @change="formData.classify = ''"
+        >
           <el-option label="个人证书" value="personal" />
           <el-option label="组织证书" value="organization" />
           <el-option label="其他" value="other" />
         </el-select>
       </el-form-item>
 
-      <el-form-item label="证书类别" prop="classify">
-        <el-select v-model="formData.classify" placeholder="请选择证书类别">
-          <el-option label="职业资格证" value="professional" />
-          <el-option label="技能证书" value="skill" />
-          <el-option label="学历证书" value="education" />
-          <el-option label="荣誉证书" value="honor" />
-          <el-option label="其他" value="other" />
+      <span class="absolute left-16 text-red" v-if="formData.type !== 'other'">*</span>
+      <el-form-item label="证书类别" prop="classify" v-show="formData.type !== 'other'">
+        <el-select
+          v-if="formData.type === 'personal'"
+          v-model="formData.classify"
+          placeholder="证书类别"
+          clearable
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PERSON_CERT)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+        <el-select v-else v-model="formData.classify" placeholder="证书类别" clearable>
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.ORG_CERT)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
         </el-select>
       </el-form-item>
 
-      <el-form-item label="所属公司/人" prop="certBelong">
-        <el-input v-model="formData.certBelong" placeholder="请输入所属公司或人员" />
+      <el-form-item label="所在部门" prop="deptId">
+        <el-tree-select
+          clearable
+          v-model="formData.deptId"
+          :data="deptList2"
+          :props="defaultProps"
+          check-strictly
+          node-key="id"
+          filterable
+          placeholder="请选择所在部门"
+          @change="handleDeptChange"
+        />
+      </el-form-item>
+
+      <el-form-item label="所属人" prop="userId">
+        <el-select v-model="formData.userId" placeholder="请选择所属人" clearable>
+          <el-option
+            v-for="dict in userList"
+            :key="dict.id"
+            :label="dict.nickname"
+            :value="dict.id"
+          />
+        </el-select>
       </el-form-item>
 
       <el-form-item label="颁发机构" prop="certOrg">
@@ -233,6 +282,9 @@ const deptList = ref<Tree[]>([]) // 树形结构
 const deptList2 = ref<Tree[]>([]) // 树形结构
 import { formatDate } from '@/utils/formatTime'
 import UploadImage from '@/components/UploadFile/src/UploadImg.vue'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { defaultProps } from '@/utils/tree'
+import { selectedDeptsEmployee } from '@/api/system/user'
 
 defineOptions({ name: 'IotQHSECertificate' })
 
@@ -272,7 +324,8 @@ const formRef = ref()
 const formData = ref({
   type: '', // 证书类型
   classify: '', // 证书类别
-  certBelong: '', // 证书所属公司/个人
+  userId: '',
+
   certOrg: '', // 证书颁发机构
   certStandard: '', // 证书标准
   certIssue: '', // 证书颁发时间
@@ -293,18 +346,6 @@ const getCertificateTypeText = (type: string) => {
   return map[type] || type
 }
 
-// 获取证书类别文本
-const getCertificateCategoryText = (category: string) => {
-  const map: Record<string, string> = {
-    professional: '职业资格证',
-    skill: '技能证书',
-    education: '学历证书',
-    honor: '荣誉证书',
-    other: '其他'
-  }
-  return map[category] || category
-}
-
 // 正确格式化日期的函数
 const formatDateCorrectly = (timestamp) => {
   if (!timestamp) return ''
@@ -322,8 +363,26 @@ const formatDateCorrectly = (timestamp) => {
 // 表单验证规则
 const formRules = {
   type: [{ required: true, message: '证书类型不能为空', trigger: 'blur' }],
-  classify: [{ required: true, message: '证书类别不能为空', trigger: 'blur' }],
-  certBelong: [{ required: true, message: '所属公司/人不能为空', trigger: 'blur' }],
+  classify: [
+    {
+      required: false, // 默认设为非必填
+      validator: (rule, value, callback) => {
+        // 只有当证书类型不是 "other" 时才验证
+        if (formData.value.type !== 'other') {
+          if (!value) {
+            callback(new Error('证书类别不能为空'))
+          } else {
+            callback()
+          }
+        } else {
+          callback() // other 类型时不需要验证
+        }
+      },
+      trigger: ['blur', 'change']
+    }
+  ],
+  deptId: [{ required: true, message: '所在部门不能为空', trigger: 'blur' }],
+
   certOrg: [{ required: true, message: '颁发机构不能为空', trigger: 'blur' }],
   certIssue: [{ required: true, message: '颁发时间不能为空', trigger: 'blur' }],
   certExpire: [{ required: true, message: '有效期不能为空', trigger: 'blur' }]
@@ -343,8 +402,10 @@ const getList = async () => {
 
 const handleExport = async () => {
   try {
+    exportLoading.value = true
     const response = await IotMeasureCertApi.exportIotMeasureCert(queryParams)
     downloadFile(response)
+    exportLoading.value = false
   } catch (error) {
     ElMessage.error('导出失败,请重试')
     console.error('导出错误:', error)
@@ -437,8 +498,7 @@ const handleDelete = async (id: number) => {
 const resetForm = () => {
   formData.value = {
     type: '', // 证书类型
-    classify: '', // 证书类别
-    certBelong: '', // 证书所属公司/个人
+    classify: '',
     certOrg: '', // 证书颁发机构
     certStandard: '', // 证书标准
     certIssue: '', // 证书颁发时间
@@ -446,7 +506,8 @@ const resetForm = () => {
     noticeBefore: '', // 到期前提醒
     certPic: '', // 证书图片上传
     remark: '', // 备注
-    deptId: '' // 部门id
+    deptId: '', // 部门id
+    userId: ''
   }
   formRef.value?.clearValidate()
 }
@@ -520,6 +581,16 @@ const downloadFile = (response: any) => {
   window.URL.revokeObjectURL(url)
 }
 
+let userList = ref([])
+const handleDeptChange = async (value) => {
+  const res = await selectedDeptsEmployee({
+    deptIds: value
+  })
+
+  userList.value = res
+  console.log('value>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', userList.value)
+}
+
 onMounted(async () => {
   getList()
 

+ 680 - 0
src/views/pms/qhse/factor/index.vue

@@ -0,0 +1,680 @@
+<template>
+  <div class="factor-matrix">
+    <!-- 筛选表单 -->
+    <ContentWrap style="border: 0">
+      <el-form
+        class="pt-2"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        label-width="80px"
+      >
+        <el-form-item label="工序" prop="process">
+          <el-input
+            v-model="queryParams.process"
+            placeholder="请输入工序"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-200px"
+          />
+        </el-form-item>
+        <el-form-item label="步骤分解" prop="stepBreak">
+          <el-input
+            v-model="queryParams.stepBreak"
+            placeholder="请输入步骤分解"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-200px"
+          />
+        </el-form-item>
+        <el-form-item label="环境因素" prop="environmentElement">
+          <el-input
+            v-model="queryParams.environmentElement"
+            placeholder="请输入环境因素"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-200px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
+          <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
+          <el-button type="primary" @click="openAddDialog" color="#626aef"
+            ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
+          >
+          <el-button type="success" plain @click="handleExport" :loading="exportLoading">
+            <Icon icon="ep:download" class="mr-5px" /> 导出
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 表格 -->
+    <ContentWrap style="border: 0">
+      <el-table
+        :data="tableData"
+        border
+        stripe
+        style="width: 100%"
+        :header-cell-style="{ background: '#f5f7fa', color: '#333' }"
+        :cell-style="{ padding: '12px 8px' }"
+        height="68vh"
+      >
+        <el-table-column label="序号" width="70" align="center" fixed="left">
+          <template #default="scope">
+            {{ scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <el-table-column label="工序" prop="process" width="140" align="center" fixed="left" />
+        <el-table-column label="步骤分解" prop="stepBreak" width="140" align="center" />
+        <el-table-column label="环境因素" prop="environmentElement" width="180" align="center" />
+
+        <el-table-column label="时态" width="240" align="center">
+          <el-table-column label="过去" prop="timeBefore" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.timeBefore">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="现在" prop="timeNow" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.timeNow">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="将来" prop="timeFuture" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.timeFuture">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <el-table-column label="状态" width="240" align="center">
+          <el-table-column label="正常" prop="statusNormal" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.statusNormal">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="异常" prop="statusException" width="60" align="center">
+            <template #default="{ row }">
+              <el-button
+                circle
+                type="success"
+                style="border: none"
+                plain
+                v-if="row.statusException"
+              >
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="紧急" prop="statusDanger" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.statusDanger">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <el-table-column label="环境影响类型" width="700" align="center">
+          <el-table-column label="能源/资源耗用" prop="typeEnergy" width="100" align="center">
+            <template #default="{ row }">
+              <!-- <span>
+                {{ row.typeEnergy ? '✔' : '' }}
+              </span> -->
+
+              <el-button circle type="success" style="border: none" plain v-if="row.typeEnergy">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="水体" prop="typeWater" width="60" align="center">
+            <template #default="{ row }">
+              <!-- <span>
+                {{ row.typeWater ? '✔' : '' }}
+              </span> -->
+              <el-button circle type="success" style="border: none" plain v-if="row.typeWater">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="大气" prop="typeGas" width="60" align="center">
+            <template #default="{ row }">
+              <!-- <span>
+                {{ row.typeGas ? '✔' : '' }}
+              </span> -->
+
+              <el-button circle type="success" style="border: none" plain v-if="row.typeGas">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="噪音" prop="typeNoise" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.typeNoise">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="废弃物" prop="typeWaste" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.typeWaste">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="土壤" prop="typeSoil" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.typeSoil">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+          <el-table-column label="其他" prop="typeOther" width="60" align="center">
+            <template #default="{ row }">
+              <el-button circle type="success" style="border: none" plain v-if="row.typeOther">
+                <span class="text-[#259644]">
+                  {{ '✔' }}
+                </span>
+              </el-button>
+              <span v-else></span>
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <el-table-column label="控制措施" prop="controlMethod" min-width="200" align="center" />
+        <el-table-column label="创建日期" prop="createTime" width="160" align="center">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="备注" prop="remark" width="150" align="center" />
+
+        <el-table-column label="操作" width="120" fixed="right" align="center">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="editRow(row)">编辑</el-button>
+            <el-button type="danger" link @click="deleteRow(row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="mt-2 flex justify-right">
+        <el-pagination
+          v-model:current-page="pagination.pageNo"
+          v-model:page-size="pagination.pageSize"
+          :total="total"
+          layout="total, sizes, prev, pager, next"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+          background
+        />
+      </div>
+    </ContentWrap>
+
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog :title="dialogTitle" v-model="dialogVisible" width="50%" destroy-on-close>
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="120px"
+        v-loading="formLoading"
+      >
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="工序" prop="process">
+              <el-input v-model="formData.process" placeholder="请输入工序" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="步骤分解" prop="stepBreak">
+              <el-input v-model="formData.stepBreak" placeholder="请输入步骤分解" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="环境因素" prop="environmentElement">
+              <el-input v-model="formData.environmentElement" placeholder="请输入环境因素" />
+            </el-form-item>
+          </el-col>
+          <!-- <el-col :span="12">
+            <el-form-item label="控制措施" prop="controlMethod">
+              <el-input
+                type="textarea"
+                v-model="formData.controlMethod"
+                placeholder="请输入控制措施"
+              />
+            </el-form-item>
+          </el-col> -->
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="时态" prop="timeValue">
+              <el-radio-group v-model="formData.timeValue">
+                <el-radio label="timeBefore">过去</el-radio>
+                <el-radio label="timeNow">现在</el-radio>
+                <el-radio label="timeFuture">将来</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="状态" prop="statusValue">
+              <el-radio-group v-model="formData.statusValue">
+                <el-radio label="statusNormal">正常</el-radio>
+                <el-radio label="statusException">异常</el-radio>
+                <el-radio label="statusDanger">紧急</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="环境影响类型" prop="typeValue">
+              <el-radio-group v-model="formData.typeValue">
+                <el-radio label="typeEnergy">能源/资源耗用</el-radio>
+                <el-radio label="typeWater">水体污染</el-radio>
+                <el-radio label="typeGas">大气污染</el-radio>
+                <el-radio label="typeNoise">噪声污染</el-radio>
+                <el-radio label="typeWaste">废弃物</el-radio>
+                <el-radio label="typeSoil">土壤污染</el-radio>
+                <el-radio label="typeOther">其他</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="控制措施" prop="controlMethod">
+              <el-input
+                type="textarea"
+                v-model="formData.controlMethod"
+                placeholder="请输入控制措施"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="所在部门" prop="deptId">
+              <el-tree-select
+                clearable
+                v-model="formData.deptId"
+                :data="deptList2"
+                :props="defaultProps"
+                check-strictly
+                node-key="id"
+                filterable
+                placeholder="请选择所在部门"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="备注">
+              <el-input v-model="formData.remark" placeholder="请输入备注" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="cancelForm">取 消</el-button>
+        <el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { IotEnvironmentApi } from '@/api/pms/qhse/index'
+import { formatDate } from '@/utils/formatTime'
+import { defaultProps } from '@/utils/tree'
+import { handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+
+// 表格数据
+const deptList2 = ref<Tree[]>([]) // 树形结构
+const tableData = ref([])
+const total = ref(0)
+
+// 筛选参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  process: '',
+  stepBreak: '',
+  environmentElement: ''
+})
+
+const pagination = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+
+// 分页和查询
+const queryFormRef = ref()
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+const handleSizeChange = (val) => {
+  queryParams.pageSize = val
+  queryParams.pageNo = 1 // 重置为第一页
+
+  getList()
+}
+
+const handleCurrentChange = (val) => {
+  queryParams.pageNo = val
+  getList()
+}
+
+const downloadFile = (response) => {
+  // 创建 blob 对象
+  const blob = new Blob([response], {
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
+  })
+
+  // 获取文件名
+  let fileName = '环境因素评价矩阵.xlsx'
+  const disposition = response.headers ? response.headers['content-disposition'] : ''
+  if (disposition) {
+    const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
+    const matches = filenameRegex.exec(disposition)
+    if (matches != null && matches[1]) {
+      fileName = matches[1].replace(/['"]/g, '')
+    }
+  }
+
+  // 创建下载链接
+  const url = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.setAttribute('download', fileName)
+
+  // 触发下载
+  document.body.appendChild(link)
+  link.click()
+
+  // 清理
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}
+const exportLoading = ref(false)
+const handleExport = async () => {
+  try {
+    exportLoading.value = true
+    // 调用导出接口
+    const response = await IotEnvironmentApi.exportEnvironment(queryParams)
+
+    // 下载文件
+    downloadFile(response)
+    exportLoading.value = false
+  } catch (error) {
+    ElMessage.error('导出失败,请重试')
+    console.error('导出错误:', error)
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+const getList = async () => {
+  const res = await IotEnvironmentApi.getEnvironmentList(queryParams)
+  tableData.value = res.list
+  total.value = res.total
+}
+
+// 弹窗相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const submitLoading = ref(false)
+const isEdit = ref(false)
+
+// 表单数据
+const formData = ref({
+  process: '',
+  stepBreak: '',
+  environmentElement: '',
+  timeValue: '', // 单选时态值
+  statusValue: '', // 单选状态值
+  typeValue: '', // 单选影响类型值
+  controlMethod: '',
+  remark: '',
+  deptId: null as number | null
+})
+
+// 表单验证规则
+const formRules = {
+  stepBreak: [{ required: true, message: '请输入步骤分解', trigger: 'blur' }],
+  environmentElement: [{ required: true, message: '请输入环境因素', trigger: 'blur' }],
+  controlMethod: [{ required: true, message: '请输入控制措施', trigger: 'blur' }],
+  timeValue: [{ required: true, message: '请选择时态', trigger: 'change' }],
+  statusValue: [{ required: true, message: '请选择状态', trigger: 'change' }],
+  typeValue: [{ required: true, message: '请选择环境影响类型', trigger: 'change' }]
+}
+
+// 表单引用
+const formRef = ref()
+
+// 打开新增对话框
+const openAddDialog = () => {
+  isEdit.value = false
+  dialogTitle.value = '新增环境因素'
+  resetFormData()
+  dialogVisible.value = true
+}
+
+// 编辑行
+const editRow = (row) => {
+  isEdit.value = true
+  dialogTitle.value = '编辑环境因素'
+
+  // 构造表单数据
+  formData.value = {
+    ...row,
+    timeValue: '', // 初始化单选值
+    statusValue: '', // 初始化单选值
+    typeValue: '' // 初始化单选值
+  }
+
+  // 设置单选值
+  if (row.timeBefore) formData.value.timeValue = 'timeBefore'
+  if (row.timeNow) formData.value.timeValue = 'timeNow'
+  if (row.timeFuture) formData.value.timeValue = 'timeFuture'
+
+  if (row.statusNormal) formData.value.statusValue = 'statusNormal'
+  if (row.statusException) formData.value.statusValue = 'statusException'
+  if (row.statusDanger) formData.value.statusValue = 'statusDanger'
+
+  if (row.typeEnergy) formData.value.typeValue = 'typeEnergy'
+  if (row.typeWater) formData.value.typeValue = 'typeWater'
+  if (row.typeGas) formData.value.typeValue = 'typeGas'
+  if (row.typeNoise) formData.value.typeValue = 'typeNoise'
+  if (row.typeWaste) formData.value.typeValue = 'typeWaste'
+  if (row.typeSoil) formData.value.typeValue = 'typeSoil'
+  if (row.typeOther) formData.value.typeValue = 'typeOther'
+
+  dialogVisible.value = true
+}
+
+// 删除行
+const deleteRow = (row) => {
+  ElMessageBox.confirm('确定要删除这条记录吗?', '警告', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      // 这里应该调用实际的API删除
+      await IotEnvironmentApi.deleteEnvironment(row.id)
+
+      ElMessage.success('删除成功')
+      getList() // 重新获取列表
+    })
+    .catch(() => {
+      // 取消删除
+    })
+}
+
+// 重置表单数据
+const resetFormData = () => {
+  formData.value = {
+    process: '',
+    stepBreak: '',
+    environmentElement: '',
+    timeValue: '',
+    statusValue: '',
+    typeValue: '',
+    controlMethod: '',
+    remark: '',
+    deptId: null
+  }
+  nextTick(() => {
+    formRef.value?.clearValidate()
+  })
+}
+
+// 取消表单
+const cancelForm = () => {
+  dialogVisible.value = false
+  resetFormData()
+}
+
+// 提交表单
+const submitForm = async () => {
+  if (!formRef.value) return
+
+  try {
+    await formRef.value.validate()
+    submitLoading.value = true
+
+    // 根据单选值更新布尔值
+    const updatedData = {
+      ...formData.value,
+      timeBefore: formData.value.timeValue === 'timeBefore',
+      timeNow: formData.value.timeValue === 'timeNow',
+      timeFuture: formData.value.timeValue === 'timeFuture',
+      statusNormal: formData.value.statusValue === 'statusNormal',
+      statusException: formData.value.statusValue === 'statusException',
+      statusDanger: formData.value.statusValue === 'statusDanger',
+      typeEnergy: formData.value.typeValue === 'typeEnergy',
+      typeWater: formData.value.typeValue === 'typeWater',
+      typeGas: formData.value.typeValue === 'typeGas',
+      typeNoise: formData.value.typeValue === 'typeNoise',
+      typeWaste: formData.value.typeValue === 'typeWaste',
+      typeSoil: formData.value.typeValue === 'typeSoil',
+      typeOther: formData.value.typeValue === 'typeOther'
+    }
+
+    // 删除临时单选字段
+    delete updatedData.timeValue
+    delete updatedData.statusValue
+    delete updatedData.typeValue
+
+    if (isEdit.value) {
+      // 更新操作
+      await IotEnvironmentApi.updateEnvironment(updatedData)
+      ElMessage.success('更新成功')
+    } else {
+      // 创建操作
+      await IotEnvironmentApi.createEnvironment(updatedData)
+      ElMessage.success('创建成功')
+    }
+
+    dialogVisible.value = false
+    getList()
+  } catch (error) {
+    console.error('表单验证失败:', error)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+onMounted(async () => {
+  // 初始化数据
+  getList()
+  deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+</script>
+
+<style scoped lang="scss">
+::v-deep .el-table__header th {
+  border: 0.5px solid #999;
+}
+
+.factor-matrix .toolbar {
+  margin: 12px 0;
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.el-table .row-actions {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  margin-top: 6px;
+}
+
+.el-checkbox .el-checkbox__input {
+  margin-left: 6px;
+}
+</style>

+ 568 - 0
src/views/pms/qhse/hazard/index.vue

@@ -0,0 +1,568 @@
+<template>
+  <div class="hazard-table-container">
+    <ContentWrap style="border: 0">
+      <!-- 搜索工作栏 -->
+      <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
+        <el-form-item label="风险等级" prop="riskGrade">
+          <el-select
+            v-model="queryParams.riskGrade"
+            placeholder="请选择风险等级"
+            clearable
+            style="width: 200px"
+          >
+            <el-option
+              v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+          <el-button type="primary" @click="openForm('create')" color="#626aef">
+            <Icon icon="ep:plus" class="mr-5px" /> 新增
+          </el-button>
+          <el-button type="success" plain @click="handleExport" :loading="exportLoading">
+            <Icon icon="ep:download" class="mr-5px" /> 导出
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <ContentWrap style="border: 0">
+      <el-table
+        :data="tableData"
+        border
+        style="width: 100%"
+        :header-cell-style="{ background: '#f5f7fa', color: '#333' }"
+        :cell-style="{ padding: '12px 8px' }"
+        height="70vh"
+      >
+        <!-- 区域/位置 列(已合并) -->
+        <el-table-column prop="region" label="区域/位置" width="150" align="center" fixed="left" />
+
+        <!-- 其他列保持不变 -->
+        <el-table-column label="序号" width="70" align="center">
+          <template #default="scope">
+            {{ scope.$index + 1 }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="elementDescription"
+          label="危害因素描述"
+          width="200"
+          align="center"
+        />
+        <el-table-column prop="maybeResult" min-width="320" label="可导致的后果" align="center" />
+
+        <!-- 风险评价列保持不变 -->
+        <el-table-column label="风险评价" width="320" align="center">
+          <el-table-column prop="evalKn" label="可能性 (L)" width="80" align="center">
+            <template #default="{ row }">
+              {{ row.evalKn }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="evalYz" label="严重性 (S)" width="80" align="center">
+            <template #default="{ row }">
+              {{ row.evalYz }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="evalFxz" label="风险值 (R)" width="80" align="center">
+            <template #default="{ row }">
+              {{ row.evalFxz }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="riskGrade" label="风险等级" width="100" align="center">
+            <template #default="scope">
+              <div class="bg-[#ffff00] w-full rounded-md" v-if="scope.row.riskGrade === 'normal'">
+                一般风险
+              </div>
+              <div class="bg-[#ffc000] w-full rounded-md" v-else-if="scope.row.riskGrade === 'big'">
+                较大风险
+              </div>
+              <div class="bg-[#0070c0] w-full text-white rounded-md" v-else>低风险</div>
+            </template>
+          </el-table-column>
+        </el-table-column>
+
+        <el-table-column
+          prop="controlMethod"
+          label="控制措施"
+          min-width="200"
+          show-overflow-tooltip
+          align="center"
+        />
+        <el-table-column prop="charge" label="责任人 " min-width="100" align="center" />
+        <el-table-column label="操作" width="150" align="center" fixed="right">
+          <template #default="{ row }">
+            <div class="flex gap-3 justify-center">
+              <el-link
+                :underline="false"
+                size="small"
+                type="primary"
+                @click="openForm('edit', row)"
+              >
+                编辑
+              </el-link>
+              <el-link :underline="false" size="small" type="danger" @click="deleteRow(row)">
+                删除
+              </el-link>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="mt-2 flex justify-right">
+        <el-pagination
+          v-model:current-page="pagination.pageNo"
+          v-model:page-size="pagination.pageSize"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+          background
+        />
+      </div>
+    </ContentWrap>
+
+    <!-- 新增/编辑弹窗 -->
+    <!-- 新增/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="50%" @close="resetForm">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
+        <el-row :gutter="20">
+          <!-- 第一行 -->
+          <el-col :span="12">
+            <el-form-item label="区域/位置" prop="region">
+              <el-input v-model="formData.region" placeholder="请输入区域/位置" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="危害因素描述" prop="elementDescription">
+              <el-input v-model="formData.elementDescription" placeholder="请输入危害因素描述" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 第二行 -->
+          <el-col :span="12">
+            <el-form-item label="可能导致的后果" prop="maybeResult">
+              <el-input
+                v-model="formData.maybeResult"
+                placeholder="请输入可能导致的后果"
+                type="textarea"
+                :rows="1"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="风险评价可能性" prop="evalKn">
+              <el-input-number
+                v-model="formData.evalKn"
+                controls-position="right"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 第三行 -->
+          <el-col :span="12">
+            <el-form-item label="风险评价严重性" prop="evalYz">
+              <el-input-number
+                v-model="formData.evalYz"
+                controls-position="right"
+                style="width: 100%"
+              />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="风险评价风险值" prop="evalFxz">
+              <el-input-number v-model="formData.evalFxz" disabled style="width: 100%" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <!-- 第四行 -->
+          <el-col :span="12">
+            <el-form-item label="风险等级" prop="riskGrade">
+              <el-select
+                v-model="formData.riskGrade"
+                placeholder="请选择风险等级"
+                clearable
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="dict in getStrDictOptions(DICT_TYPE.DANGER_GRADE)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="责任人" prop="charge">
+              <el-input v-model="formData.charge" placeholder="请输入责任人" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input
+                v-model="formData.remark"
+                type="textarea"
+                placeholder="请输入备注"
+                :rows="1"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <!-- 控制措施单独一行(占满) -->
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="控制措施" prop="controlMethod">
+              <el-input
+                v-model="formData.controlMethod"
+                type="textarea"
+                :rows="4"
+                placeholder="请输入控制措施"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <template #footer>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="submitForm">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, onMounted } from 'vue'
+import { IotDangerApi } from '@/api/pms/qhse/index'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+
+// 查询参数
+const queryParams = reactive({
+  riskGrade: ''
+})
+
+// 表格数据
+const tableData = ref([])
+
+// 弹窗控制
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增')
+const formData = reactive({
+  region: '', // 区域/位置
+  charge: '',
+  elementDescription: '', // 危害因素描述
+  maybeResult: '', // 可能导致的后果
+  evalKn: 1, // 可能性
+  evalYz: 1, // 严重性
+  evalFxz: 1, // 风险值(自动计算)
+  riskGrade: '一般风险', // 风险等级
+  controlMethod: '', // 控制措施
+  remark: '' // 备注
+})
+
+// 表单校验规则
+const rules = {
+  region: [{ required: true, message: '请输入区域/位置', trigger: 'blur' }],
+  charge: [{ required: true, message: '请输入责任人', trigger: 'blur' }],
+  elementDescription: [{ required: true, message: '请输入危害因素描述', trigger: 'blur' }],
+  maybeResult: [{ required: true, message: '请输入可能导致的后果', trigger: 'blur' }],
+  evalKn: [{ required: true, message: '请输入风险评价可能性', trigger: 'change' }],
+  evalYz: [{ required: true, message: '请输入风险评价严重性', trigger: 'change' }],
+  riskGrade: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
+  controlMethod: [{ required: true, message: '请输入控制措施', trigger: 'blur' }]
+}
+
+watch(
+  () => [formData.evalKn, formData.evalYz],
+  ([kn, yz]) => {
+    if (kn && yz) {
+      formData.evalFxz = kn * yz
+    }
+  },
+  { immediate: true }
+)
+// 搜索
+const handleQuery = () => {
+  pagination.pageNo = 1 // 搜索后回到第一页
+  loadTableData()
+}
+
+const downloadFile = (response) => {
+  // 创建 blob 对象
+  const blob = new Blob([response], {
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
+  })
+
+  // 获取文件名
+  let fileName = '危险源.xlsx'
+  const disposition = response.headers ? response.headers['content-disposition'] : ''
+  if (disposition) {
+    const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
+    const matches = filenameRegex.exec(disposition)
+    if (matches != null && matches[1]) {
+      fileName = matches[1].replace(/['"]/g, '')
+    }
+  }
+
+  // 创建下载链接
+  const url = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.setAttribute('download', fileName)
+
+  // 触发下载
+  document.body.appendChild(link)
+  link.click()
+
+  // 清理
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}
+
+const handleExport = async () => {
+  try {
+    exportLoading.value = true
+    // 调用导出接口
+    const response = await IotDangerApi.exportDanger(queryParams)
+
+    // 下载文件
+    downloadFile(response)
+    exportLoading.value = false
+  } catch (error) {
+    ElMessage.error('导出失败,请重试')
+    console.error('导出错误:', error)
+  } finally {
+  }
+}
+
+// 重置查询
+const resetQuery = () => {
+  queryParams.riskGrade = '' // 清空风险等级筛选
+  pagination.pageNo = 1 // 重置为第一页
+  loadTableData()
+}
+
+// 删除确认
+const deleteRow = async (row) => {
+  try {
+    await ElMessageBox.confirm(`确认删除 ${row.elementDescription} 吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    await IotDangerApi.deleteDanger(row.id)
+    ElMessage.success('删除成功')
+    loadTableData() // 重新加载数据
+  } catch (error) {
+    // 用户取消或删除失败
+    if (error !== 'cancel') {
+      ElMessage.error('删除失败')
+    }
+  }
+}
+
+// 每页数量变化
+const handleSizeChange = (val) => {
+  pagination.pageSize = val
+  pagination.pageNo = 1 // 重置为第一页
+  loadTableData()
+}
+
+// 预先计算合并信息
+const spanArr = ref([])
+const pos = ref(0)
+
+// 计算合并信息
+const getSpanArr = (data) => {
+  spanArr.value = []
+  pos.value = 0
+
+  data.forEach((item, index) => {
+    if (index === 0) {
+      spanArr.value.push(1)
+      pos.value = 0
+    } else {
+      if (data[index].region === data[index - 1].region) {
+        spanArr.value[pos.value] += 1
+        spanArr.value.push(0)
+      } else {
+        spanArr.value.push(1)
+        pos.value = index
+      }
+    }
+  })
+}
+
+// 行合并方法
+const objectSpanMethod = ({ row, column, rowIndex, columnIndex }) => {
+  if (columnIndex === 0) {
+    const _row = spanArr.value[rowIndex]
+    const _col = _row > 0 ? 1 : 0
+    return {
+      rowspan: _row,
+      colspan: _col
+    }
+  }
+  return {
+    rowspan: 1,
+    colspan: 1
+  }
+}
+
+// 当前页变化
+const handleCurrentChange = (val) => {
+  pagination.pageNo = val
+  loadTableData()
+}
+
+// 打开表单
+const openForm = (type, row = null) => {
+  dialogTitle.value = type === 'create' ? '新增' : '编辑'
+  if (type === 'edit') {
+    Object.assign(formData, row)
+    // 计算风险值 R = L × S
+    formData.riskValue = formData.possibility * formData.severity
+  } else {
+    resetForm()
+  }
+  dialogVisible.value = true
+}
+
+// 重置表单
+const resetForm = () => {
+  Object.keys(formData).forEach((key) => {
+    formData[key] = ''
+  })
+}
+
+// 提交表单
+const formRef = ref(null)
+const submitForm = async () => {
+  await formRef.value.validate()
+  try {
+    const params = {
+      ...formData,
+      evalFxz: formData.evalFxz // 使用已计算的值
+    }
+
+    if (dialogTitle.value === '新增') {
+      await IotDangerApi.createDanger(params)
+      ElMessage.success('新增成功')
+    } else {
+      params.id = formData.id
+      await IotDangerApi.updateDanger(params)
+      ElMessage.success('修改成功')
+    }
+
+    loadTableData()
+    dialogVisible.value = false
+  } catch (error) {
+    ElMessage.error('提交失败')
+  }
+}
+
+// 加载数据
+const exportLoading = ref(false)
+let total = ref(0)
+const pagination = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+const loadTableData = async () => {
+  try {
+    const params = {
+      pageNo: pagination.pageNo,
+      pageSize: pagination.pageSize,
+      riskGrade: queryParams.riskGrade // 添加搜索参数
+    }
+    const res = await IotDangerApi.getDangerList(params)
+    tableData.value = res.list || []
+    total.value = res.total || 0
+
+    // // 按 region 排序(支持中文)
+    // tableData.value.sort((a, b) => {
+    //   return a.region.localeCompare(b.region, 'zh-CN')
+    // })
+
+    // 计算合并信息
+    getSpanArr(tableData.value)
+  } catch (error) {
+    console.error('加载失败:', error)
+  }
+}
+
+// 页面挂载后加载数据
+onMounted(() => {
+  loadTableData()
+})
+</script>
+
+<style scoped lang="scss">
+::v-deep .el-button {
+  border-radius: 0;
+}
+
+::v-deep .el-select__wrapper {
+  border-radius: 0 !important;
+  height: 26px;
+}
+.hazard-table-container {
+  margin: 20px;
+  margin-top: 10px;
+}
+
+.area-header {
+  font-weight: bold;
+  text-align: center;
+  padding: 12px 0;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #ddd;
+}
+
+.sub-row {
+  padding: 12px 0;
+  text-align: left;
+}
+
+.risk-evaluation {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  align-items: center;
+}
+
+.risk-item {
+  font-size: 12px;
+  padding: 4px 8px;
+  border-radius: 4px;
+  background-color: #f5f7fa;
+  color: #333;
+}
+
+.risk-level {
+  font-weight: bold;
+  color: #333;
+  padding: 6px 12px;
+  border-radius: 4px;
+}
+</style>

+ 49 - 32
src/views/pms/qhse/index.vue

@@ -57,22 +57,22 @@
           <el-table-column label="责任人" align="center" prop="dutyPerson" />
           <el-table-column label="品牌" align="center" prop="brand" />
           <el-table-column label="规格型号" align="center" prop="modelName" />
-          <el-table-column label="分类" align="center" prop="classify" />
-          <el-table-column label="采购日期" align="center" prop="buyDate">
+          <el-table-column label="分类" align="center" prop="classify" width="150">
             <template #default="scope">
-              {{ formatDateCorrectly(scope.row.buyDate) }}
+              <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
             </template>
           </el-table-column>
-          <el-table-column label="有效期" align="center" prop="validity">
+          <el-table-column label="采购日期" align="center" prop="buyDate">
             <template #default="scope">
-              {{ formatDateCorrectly(scope.row.validity) }}
+              {{ formatDateCorrectly(scope.row.buyDate) }}
             </template>
           </el-table-column>
-          <el-table-column label="上次检验日期" align="center" prop="lastTime" min-width="150">
+          <!-- <el-table-column label="有效期" align="center" prop="validity">
             <template #default="scope">
-              {{ formatDateCorrectly(scope.row.lastTime) }}
+              {{ formatDateCorrectly(scope.row.validity) }}
             </template>
-          </el-table-column>
+          </el-table-column> -->
+
           <el-table-column label="价格" align="center" prop="measurePrice">
             <template #default="scope">
               {{ scope.row.measurePrice }}
@@ -169,7 +169,19 @@
       <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="分类" prop="classify">
-            <el-input v-model="formData.classify" placeholder="请输入分类" />
+            <el-select
+              v-model="formData.classify"
+              placeholder="请选择平台"
+              clearable
+              class="!w-240px"
+            >
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.MEASURE_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -198,20 +210,14 @@
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="上次检验日期" prop="lastTime">
-            <el-date-picker
-              v-model="formData.lastTime"
-              type="date"
-              value-format="x"
-              placeholder="请选择上次检验/校准日期"
-              style="width: 100%"
-            />
+          <el-form-item label="备注" prop="remark">
+            <el-input v-model="formData.remark" type="textarea" placeholder="请输入描述" />
           </el-form-item>
         </el-col>
       </el-row>
 
       <el-row :gutter="20">
-        <el-col :span="12">
+        <!-- <el-col :span="12">
           <el-form-item label="有效期" prop="validity">
             <el-date-picker
               v-model="formData.validity"
@@ -221,13 +227,21 @@
               style="width: 100%"
             />
           </el-form-item>
-        </el-col>
+        </el-col> -->
+        <!-- <el-col :span="12">
+          <el-form-item label="证书编码" prop="measureCertNo">
+            <el-input v-model="formData.measureCertNo" placeholder="请输入证书编码" />
+          </el-form-item>
+        </el-col> -->
+      </el-row>
+
+      <!-- <el-row :gutter="20">
         <el-col :span="12">
           <el-form-item label="备注" prop="remark">
             <el-input v-model="formData.remark" type="textarea" placeholder="请输入描述" />
           </el-form-item>
         </el-col>
-      </el-row>
+      </el-row> -->
     </el-form>
 
     <template #footer>
@@ -247,6 +261,7 @@ import { ElMessageBox } from 'element-plus'
 const deptList = ref<Tree[]>([]) // 树形结构
 const deptList2 = ref<Tree[]>([]) // 树形结构
 import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
 
 defineOptions({ name: 'IotQHSEMeasure' })
 
@@ -288,9 +303,10 @@ const formData = ref({
   remark: '',
   deptId: '',
   measureCode: '',
-  validity: null, // 有效期
-  lastTime: null, // 上次检验/校准日期
-  measurePrice: 0 // 价格
+  // validity: null, // 有效期
+
+  measurePrice: 0, // 价格
+  measureCertNo: ''
 })
 
 // 正确格式化日期的函数
@@ -311,8 +327,10 @@ const formatDateCorrectly = (timestamp) => {
 const formRules = {
   measureName: [{ required: true, message: '计量器具名称不能为空', trigger: 'blur' }],
   dutyPerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
-  classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }]
-  // measureCode: [{ required: true, message: '编码不能为空', trigger: 'blur' }]
+  classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
+  deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
+  measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
+  validity: [{ required: true, message: '有效期不能为空', trigger: 'blur' }]
 }
 
 /** 查询列表 */
@@ -410,8 +428,7 @@ const handleEdit = (row) => {
     ...row,
     // 确保日期字段正确处理
     buyDate: row.buyDate ? ensureMillisecondTimestamp(row.buyDate) : null,
-    validity: row.validity ? ensureMillisecondTimestamp(row.validity) : null,
-    lastTime: row.lastTime ? ensureMillisecondTimestamp(row.lastTime) : null
+    validity: row.validity ? ensureMillisecondTimestamp(row.validity) : null
   }
 
   dialogVisible.value = true
@@ -461,9 +478,10 @@ const resetForm = () => {
     remark: '',
     deptId: '',
     measureCode: '',
-    validity: null, // 有效期
-    lastTime: null, // 上次检验/校准日期
-    measurePrice: 0 // 价格
+    // validity: null, // 有效期
+
+    measurePrice: 0, // 价格
+    measureCertNo: ''
   }
   formRef.value?.clearValidate()
 }
@@ -487,8 +505,7 @@ const submitForm = async () => {
       ...formData.value,
       // 确保日期字段以正确的格式提交
       buyDate: formData.value.buyDate,
-      validity: formData.value.validity,
-      lastTime: formData.value.lastTime
+      validity: formData.value.validity
     }
 
     if (isEdit.value) {

+ 105 - 59
src/views/pms/qhse/iotmeasuredetect/IotMeasureDetectForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="50%">
     <el-form
       ref="formRef"
       :model="formData"
@@ -7,62 +7,102 @@
       label-width="120px"
       v-loading="formLoading"
     >
-      <el-form-item label="计量器具" prop="measureId">
-        <el-input v-model="formData.measureName" disabled placeholder="选择计量器具">
-          <template #append>
-            <el-link @click="selectMeasure" :underline="false">选择计量器具</el-link>
-          </template>
-        </el-input>
-      </el-form-item>
-      <el-form-item label="检测/校准日期" prop="detectDate">
-        <el-date-picker
-          v-model="formData.detectDate"
-          type="date"
-          value-format="x"
-          placeholder="选择检测/校准日期"
-          style="width: 200px"
-        />
-      </el-form-item>
-      <el-form-item label="检测/校准机构" prop="detectOrg">
-        <el-input
-          v-model="formData.detectOrg"
-          placeholder="请输入检测/校准机构"
-          style="width: 200px"
-        />
-      </el-form-item>
-      <el-form-item label="检测/校准内容" prop="detectContent">
-        <Editor v-model="formData.detectContent" height="150px" />
-      </el-form-item>
-      <el-form-item label="检测/校准有效期" prop="validityPeriod">
-        <el-date-picker
-          v-model="formData.validityPeriod"
-          type="date"
-          value-format="x"
-          placeholder="选择检测/校准有效期"
-          style="width: 200px"
-        />
-      </el-form-item>
-      <el-form-item label="校准金额" prop="detectAmount">
-        <el-input
-          v-model="formData.detectAmount"
-          placeholder="请输入校准金额"
-          style="width: 200px"
-        />
-      </el-form-item>
-      <el-form-item label="部门" prop="deptId">
-        <el-tree-select
-          style="width: 220px"
-          clearable
-          v-model="formData.deptId"
-          :data="deptList2"
-          :props="defaultProps"
-          check-strictly
-          node-key="id"
-          filterable
-          placeholder="请选择所在部门"
-        />
-      </el-form-item>
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="计量器具" prop="measureId">
+            <el-input
+              v-model="formData.measureName"
+              disabled
+              placeholder="计量器具"
+              style="width: 300px"
+            >
+              <template #append>
+                <el-link @click="selectMeasure" :underline="false">选择</el-link>
+              </template>
+            </el-input>
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="证书编码" prop="measureCertNo">
+            <el-input v-model="formData.measureCertNo" placeholder="请输入证书编码" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="检测/校准日期" prop="detectDate">
+            <el-date-picker
+              v-model="formData.detectDate"
+              type="date"
+              value-format="x"
+              placeholder="选择检测/校准日期"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="检测/校准机构" prop="detectOrg">
+            <el-input v-model="formData.detectOrg" placeholder="请输入检测/校准机构" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="检测/校准标准" prop="detectStandard">
+            <el-input v-model="formData.detectStandard" placeholder="请输入检测/校准标准" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="检测/校准有效期" prop="validityPeriod">
+            <el-date-picker
+              v-model="formData.validityPeriod"
+              type="date"
+              value-format="x"
+              placeholder="选择检测/校准有效期"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="校准金额" prop="detectAmount">
+            <el-input v-model="formData.detectAmount" placeholder="请输入校准金额" />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="部门" prop="deptId">
+            <el-tree-select
+              style="width: 100%"
+              clearable
+              v-model="formData.deptId"
+              :data="deptList2"
+              :props="defaultProps"
+              check-strictly
+              node-key="id"
+              filterable
+              placeholder="请选择所在部门"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="24">
+          <el-form-item label="检测/校准内容" prop="detectContent">
+            <Editor v-model="formData.detectContent" height="150px" />
+          </el-form-item>
+        </el-col>
+      </el-row>
     </el-form>
+
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -182,17 +222,21 @@ const formData = ref({
   detectDate: undefined,
   detectOrg: undefined,
   detectContent: undefined,
+  detectStandard: undefined,
   validityPeriod: undefined,
   detectAmount: undefined,
   deptId: undefined,
   measureName: '',
-  measureId: ''
+  measureId: '',
+  measureCertNo: ''
 })
 const formRules = reactive({
   detectDate: [{ required: true, message: '检测/校准日期不能为空', trigger: 'blur' }],
   detectOrg: [{ required: true, message: '检测/校准机构不能为空', trigger: 'blur' }],
   detectContent: [{ required: true, message: '检测/校准内容不能为空', trigger: 'blur' }],
-  validityPeriod: [{ required: true, message: '检测/校准有效期不能为空', trigger: 'blur' }]
+  validityPeriod: [{ required: true, message: '检测/校准有效期不能为空', trigger: 'blur' }],
+  measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
+  detectStandard: [{ required: true, message: '检测/校准标准不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 const measureList = ref([])
@@ -277,7 +321,9 @@ const resetForm = () => {
     detectAmount: undefined,
     deptId: undefined,
     measureName: '',
-    measureId: ''
+    measureId: '',
+    measureCertNo: '',
+    detectStandard: undefined
   }
   formRef.value?.resetFields()
 }

+ 2 - 0
src/views/pms/qhse/iotmeasuredetect/index.vue

@@ -59,12 +59,14 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="计量器具名称" align="center" prop="measureName" />
+      <el-table-column label="证书编码" align="center" prop="measureCertNo" />
       <el-table-column label="检测/校准日期" align="center" prop="detectDate">
         <template #default="scope">
           <span>{{ formatDateCorrectly(scope.row.detectDate) }}</span>
         </template>
       </el-table-column>
       <el-table-column label="检测/校准机构" align="center" prop="detectOrg" />
+      <el-table-column label="检测/校准标准" align="center" prop="detectStandard" />
       <el-table-column label="检测/校准内容" align="center" prop="detectContent">
         <template #default="scope">
           <div v-html="scope.row.detectContent"></div>