Explorar o código

Merge branch 'qhse_fix' of shuzhihua/pms-iot-vue into master

yanghao hai 1 día
pai
achega
cbebd2fbfa

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

@@ -162,3 +162,27 @@ 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 })
+  }
+}

+ 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) {

+ 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>

+ 33 - 14
src/views/pms/monitor/index.vue

@@ -49,7 +49,7 @@
               :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>
@@ -86,7 +86,9 @@
                   <!-- <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>
@@ -391,7 +393,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 +441,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 +457,20 @@ 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 .card-status {
@@ -466,15 +482,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 +499,13 @@ onMounted(async () => {
 }
 
 .device-card .card-row .muted {
-  color: #97a0b5;
+  color: #34475c;
   width: 86px;
   flex-shrink: 0;
 }
 
 .device-card .card-row .value {
-  color: #2b3a4a;
+  color: #fff;
   flex: 1;
 }
 
@@ -497,16 +514,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>

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

@@ -15,21 +15,24 @@
             <el-select v-model="queryParams.type" placeholder="请选择证书类型" style="width: 150px">
               <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="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 +46,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 +68,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 +145,64 @@
       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" />
+        <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>
+
+      <span class="absolute left-19 text-red" v-if="formData.type === 'personal'">*</span>
+      <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 +280,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 +322,8 @@ const formRef = ref()
 const formData = ref({
   type: '', // 证书类型
   classify: '', // 证书类别
-  certBelong: '', // 证书所属公司/个人
+  userId: '',
+
   certOrg: '', // 证书颁发机构
   certStandard: '', // 证书标准
   certIssue: '', // 证书颁发时间
@@ -293,18 +344,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 ''
@@ -323,7 +362,30 @@ const formatDateCorrectly = (timestamp) => {
 const formRules = {
   type: [{ required: true, message: '证书类型不能为空', trigger: 'blur' }],
   classify: [{ required: true, message: '证书类别不能为空', trigger: 'blur' }],
-  certBelong: [{ required: true, message: '所属公司/人不能为空', trigger: 'blur' }],
+  deptId: [{ required: true, message: '所在部门不能为空', trigger: 'blur' }],
+  userId: [
+    {
+      required: false, // 默认不强制验证
+      validator: (rule, value, callback) => {
+        // 只有当证书类型为个人且证书类别为个人证书时才验证
+        if (
+          formData.value.type === 'personal' &&
+          getStrDictOptions(DICT_TYPE.PERSON_CERT).some(
+            (personCert) => personCert.value === formData.value.classify
+          )
+        ) {
+          if (!value) {
+            callback(new Error('个人证书必须选择所属人'))
+          } else {
+            callback()
+          }
+        } else {
+          callback() // 不需要验证时直接通过
+        }
+      },
+      trigger: ['blur', 'change']
+    }
+  ],
   certOrg: [{ required: true, message: '颁发机构不能为空', trigger: 'blur' }],
   certIssue: [{ required: true, message: '颁发时间不能为空', trigger: 'blur' }],
   certExpire: [{ required: true, message: '有效期不能为空', trigger: 'blur' }]
@@ -343,8 +405,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 +501,7 @@ const handleDelete = async (id: number) => {
 const resetForm = () => {
   formData.value = {
     type: '', // 证书类型
-    classify: '', // 证书类别
-    certBelong: '', // 证书所属公司/个人
+    classify: '',
     certOrg: '', // 证书颁发机构
     certStandard: '', // 证书标准
     certIssue: '', // 证书颁发时间
@@ -446,7 +509,8 @@ const resetForm = () => {
     noticeBefore: '', // 到期前提醒
     certPic: '', // 证书图片上传
     remark: '', // 备注
-    deptId: '' // 部门id
+    deptId: '', // 部门id
+    userId: ''
   }
   formRef.value?.clearValidate()
 }
@@ -520,6 +584,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()
 

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

@@ -0,0 +1,559 @@
+<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" />
+
+        <!-- 其他列保持不变 -->
+        <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" 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 label="操作" width="150" align="center">
+          <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="600px" @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="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: '', // 区域/位置
+
+  elementDescription: '', // 危害因素描述
+  maybeResult: '', // 可能导致的后果
+  evalKn: 1, // 可能性
+  evalYz: 1, // 严重性
+  evalFxz: 1, // 风险值(自动计算)
+  riskGrade: '一般风险', // 风险等级
+  controlMethod: '', // 控制措施
+  remark: '' // 备注
+})
+
+// 表单校验规则
+const rules = {
+  region: [{ 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>

+ 35 - 6
src/views/pms/qhse/index.vue

@@ -57,7 +57,11 @@
           <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="classify" width="150">
+            <template #default="scope">
+              <dict-tag :type="DICT_TYPE.MEASURE_TYPE" :value="scope.row.classify" />
+            </template>
+          </el-table-column>
           <el-table-column label="采购日期" align="center" prop="buyDate">
             <template #default="scope">
               {{ formatDateCorrectly(scope.row.buyDate) }}
@@ -169,7 +173,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">
@@ -222,6 +238,14 @@
             />
           </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="remark">
             <el-input v-model="formData.remark" type="textarea" placeholder="请输入描述" />
@@ -247,6 +271,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' })
 
@@ -290,7 +315,8 @@ const formData = ref({
   measureCode: '',
   validity: null, // 有效期
   lastTime: null, // 上次检验/校准日期
-  measurePrice: 0 // 价格
+  measurePrice: 0, // 价格
+  measureCertNo: ''
 })
 
 // 正确格式化日期的函数
@@ -311,8 +337,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' }]
 }
 
 /** 查询列表 */
@@ -463,7 +491,8 @@ const resetForm = () => {
     measureCode: '',
     validity: null, // 有效期
     lastTime: null, // 上次检验/校准日期
-    measurePrice: 0 // 价格
+    measurePrice: 0, // 价格
+    measureCertNo: ''
   }
   formRef.value?.clearValidate()
 }