Kaynağa Gözat

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

yanghao 20 saat önce
ebeveyn
işleme
b0ac0109f3

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

@@ -535,6 +535,19 @@ export const QhseMonthReportApi = {
   }
 }
 
+// 月报汇总
+export const QhseMonthReportSummaryApi = {
+  // 获得QHSE月报汇总分页
+  getQhseMonthReportSummaryList: async (params) => {
+    return await request.get({ url: `/rq/qhse-month-report/page/year-month`, params })
+  },
+
+  // 获得QHSE月报汇总详情
+  getQhseMonthReportSummary: async (params) => {
+    return await request.get({ url: `/rq/qhse-month-report/page`, params })
+  }
+}
+
 // 应检设备证书
 export const InspectDeviceCertApi = {
   // 获得设备证书分页 rq/qhse-device-cert/create

+ 4 - 5
src/components/UploadFile/src/UploadFile.vue

@@ -18,8 +18,7 @@
       :on-success="handleFileSuccess"
       :show-file-list="true"
       class="upload-file-uploader"
-      name="file"
-    >
+      name="file">
       <el-button type="primary">
         <Icon icon="ep:upload-filled" />
         选取文件
@@ -41,8 +40,7 @@
               :underline="false"
               download
               target="_blank"
-              type="primary"
-            >
+              type="primary">
               下载
             </el-link>
           </div>
@@ -93,7 +91,8 @@ const props = defineProps({
     'pptx',
     'mp4',
     'apk',
-    'wgt'
+    'wgt',
+    'bmp'
   ]), // 文件类型, 例如['png', 'jpg', 'jpeg']
   fileSize: propTypes.number.def(50), // 大小限制(MB)
   limit: propTypes.number.def(5), // 数量限制

+ 287 - 249
src/views/pms/qhse/certPerson/index.vue

@@ -1,257 +1,291 @@
 <template>
-  <el-row :gutter="20">
-    <!-- 左侧部门树 -->
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-
-    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
-      <div style="overflow-y: hidden !important; border: none" class="bg-white px-2 py-2">
-        <!-- 搜索工作栏 -->
-        <el-form
-          class="-mb-15px"
-          :model="queryParams"
-          ref="queryFormRef"
-          :inline="true"
-          @submit.prevent
-          border>
-          <el-form-item label="人员名称" prop="username">
-            <el-input
-              v-model="queryParams.username"
-              placeholder="请输入人员名称"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-150px" />
-          </el-form-item>
-          <el-form-item label="姓名" prop="nickname">
-            <el-input
-              v-model="queryParams.nickname"
-              placeholder="请输入姓名"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-150px" />
-          </el-form-item>
-
-          <el-form-item label="岗位" prop="postName">
-            <el-input
-              v-model="queryParams.postName"
-              placeholder="请输入岗位名称"
-              clearable
-              @keyup.enter="handleQuery"
-              class="!w-150px" />
-          </el-form-item>
-
-          <el-form-item>
-            <el-button @click="handleQuery" v-hasPermi="['rq:qhse-cert-person:query']"
-              ><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"
-              plain
-              @click="openForm('create')"
-              v-hasPermi="['rq:qhse-cert-person:create']">
-              <Icon icon="ep:plus" class="mr-5px" /> 新增
-            </el-button>
-            <el-button
-              type="success"
-              plain
-              @click="handleExport"
-              :loading="exportLoading"
-              v-hasPermi="['rq:qhse-cert-person:export']">
-              <Icon icon="ep:download" class="mr-5px" /> 导出
-            </el-button>
-          </el-form-item>
-        </el-form>
+  <div
+    class="grid grid-cols-[auto_1fr] grid-rows-[auto_auto_minmax(0,1fr)] gap-0 gap-x-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      class="row-span-4"
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      @node-click="handleDeptNodeClick" />
+
+    <div class="mb-1">
+      <!-- 搜索工作栏 -->
+      <el-form
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 pt-4 flex items-center flex-wrap min-w-0"
+        :model="queryParams"
+        ref="queryFormRef"
+        :inline="true"
+        @submit.prevent
+        border>
+        <el-form-item label="人员名称" prop="username">
+          <el-input
+            v-model="queryParams.username"
+            placeholder="请输入人员名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-150px" />
+        </el-form-item>
+        <el-form-item label="姓名" prop="nickname">
+          <el-input
+            v-model="queryParams.nickname"
+            placeholder="请输入姓名"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-150px" />
+        </el-form-item>
+
+        <el-form-item label="岗位" prop="postName">
+          <el-input
+            v-model="queryParams.postName"
+            placeholder="请输入岗位名称"
+            clearable
+            @keyup.enter="handleQuery"
+            class="!w-150px" />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button @click="handleQuery" v-hasPermi="['rq:qhse-cert-person:query']"
+            ><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"
+            plain
+            @click="openForm('create')"
+            v-hasPermi="['rq:qhse-cert-person:create']">
+            <Icon icon="ep:plus" class="mr-5px" /> 新增
+          </el-button>
+          <el-button
+            type="success"
+            plain
+            @click="handleExport"
+            :loading="exportLoading"
+            v-hasPermi="['rq:qhse-cert-person:export']">
+            <Icon icon="ep:download" class="mr-5px" /> 导出
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div class="min-w-0"></div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-4 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <zm-table
+              :loading="loading"
+              :data="list"
+              :stripe="true"
+              :show-overflow-tooltip="true"
+              :width="width"
+              :height="height"
+              :max-height="height">
+              <el-table-column :label="t('monitor.serial')" width="70" align="center">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </el-table-column>
+
+              <zm-table-column label="人员名称" align="center" prop="username" />
+              <zm-table-column label="姓名" align="center" prop="nickname" />
+              <zm-table-column label="岗位" align="center" prop="postName" />
+              <zm-table-column label="手机号" align="center" prop="mobile" />
+              <zm-table-column label="部门名称" align="center" prop="deptName" />
+              <zm-table-column label="三小证" align="center">
+                <zm-table-column label="硫化氢" align="center">
+                  <template #default="scope">
+                    <el-button
+                      circle
+                      type="success"
+                      style="border: none"
+                      plain
+                      v-if="scope.row.lsh">
+                      <span class="text-[#259644]">
+                        {{ '✔' }}
+                      </span>
+                    </el-button>
+                    <span v-else></span>
+                  </template>
+                </zm-table-column>
+                <zm-table-column label="井控(A2)" align="center">
+                  <template #default="scope">
+                    <el-button circle type="success" style="border: none" plain v-if="scope.row.jk">
+                      <span class="text-[#259644]">
+                        {{ '✔' }}
+                      </span>
+                    </el-button>
+                    <span v-else></span>
+                  </template>
+                </zm-table-column>
+                <zm-table-column label="HSE" align="center">
+                  <template #default="scope">
+                    <el-button
+                      circle
+                      type="success"
+                      style="border: none"
+                      plain
+                      v-if="scope.row.hse">
+                      <span class="text-[#259644]">
+                        {{ '✔' }}
+                      </span>
+                    </el-button>
+                    <span v-else></span>
+                  </template>
+                </zm-table-column>
+              </zm-table-column>
+
+              <zm-table-column label="司钻证" align="center">
+                <template #default="scope">
+                  <el-button circle type="success" style="border: none" plain v-if="scope.row.sz">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="高处作业证" align="center">
+                <template #default="scope">
+                  <el-button circle type="success" style="border: none" plain v-if="scope.row.gczy">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="电工证" align="center">
+                <template #default="scope">
+                  <el-button circle type="success" style="border: none" plain v-if="scope.row.dg">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="连续油管主操证" align="center" min-width="120px">
+                <template #default="scope">
+                  <el-button
+                    circle
+                    type="success"
+                    style="border: none"
+                    plain
+                    v-if="scope.row.lxygzc">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="吊装证" align="center">
+                <template #default="scope">
+                  <el-button circle type="success" style="border: none" plain v-if="scope.row.dz">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="安全管理证" align="center">
+                <template #default="scope">
+                  <el-button circle type="success" style="border: none" plain v-if="scope.row.aqgl">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="国际井控证" align="center">
+                <template #default="scope">
+                  <el-button circle type="success" style="border: none" plain v-if="scope.row.gjjk">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="消防证" align="center">
+                <template #default="scope">
+                  <el-button
+                    circle
+                    type="success"
+                    style="border: none"
+                    plain
+                    v-if="scope.row.xiaofang">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="急救证" align="center">
+                <template #default="scope">
+                  <el-button
+                    circle
+                    type="success"
+                    style="border: none"
+                    plain
+                    v-if="scope.row.jijiu">
+                    <span class="text-[#259644]">
+                      {{ '✔' }}
+                    </span>
+                  </el-button>
+                  <span v-else></span>
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
+
+              <zm-table-column label="操作" align="center" min-width="120px" action fixed="right">
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="openForm('update', scope.row.id)"
+                    v-hasPermi="['rq:qhse-cert-person:update']">
+                    编辑
+                  </el-button>
+                  <el-button
+                    link
+                    type="danger"
+                    @click="handleDelete(scope.row.id)"
+                    v-hasPermi="['rq:qhse-cert-person:delete']">
+                    删除
+                  </el-button>
+                </template>
+              </zm-table-column>
+            </zm-table>
+          </template>
+        </el-auto-resizer>
       </div>
 
-      <!-- 列表 -->
-      <ContentWrap style="border: none; margin-top: 10px">
-        <zm-table :loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-          <el-table-column :label="t('monitor.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </el-table-column>
-
-          <zm-table-column label="人员名称" align="center" prop="username" />
-          <zm-table-column label="姓名" align="center" prop="nickname" />
-          <zm-table-column label="岗位" align="center" prop="postName" />
-          <zm-table-column label="手机号" align="center" prop="mobile" />
-          <zm-table-column label="部门名称" align="center" prop="deptName" />
-          <zm-table-column label="三小证" align="center">
-            <zm-table-column label="硫化氢" align="center">
-              <template #default="scope">
-                <el-button circle type="success" style="border: none" plain v-if="scope.row.lsh">
-                  <span class="text-[#259644]">
-                    {{ '✔' }}
-                  </span>
-                </el-button>
-                <span v-else></span>
-              </template>
-            </zm-table-column>
-            <zm-table-column label="井控(A2)" align="center">
-              <template #default="scope">
-                <el-button circle type="success" style="border: none" plain v-if="scope.row.jk">
-                  <span class="text-[#259644]">
-                    {{ '✔' }}
-                  </span>
-                </el-button>
-                <span v-else></span>
-              </template>
-            </zm-table-column>
-            <zm-table-column label="HSE" align="center">
-              <template #default="scope">
-                <el-button circle type="success" style="border: none" plain v-if="scope.row.hse">
-                  <span class="text-[#259644]">
-                    {{ '✔' }}
-                  </span>
-                </el-button>
-                <span v-else></span>
-              </template>
-            </zm-table-column>
-          </zm-table-column>
-
-          <zm-table-column label="司钻证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.sz">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="高处作业证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.gczy">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="电工证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.dg">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="连续油管主操证" align="center" min-width="120px">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.lxygzc">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="吊装证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.dz">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-          <!-- 
-          <zm-table-column label="特种作业证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.tzzy">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column> -->
-
-          <zm-table-column label="安全管理证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.aqgl">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="国际井控证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.gjjk">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="消防证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.xiaofang">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="急救证" align="center">
-            <template #default="scope">
-              <el-button circle type="success" style="border: none" plain v-if="scope.row.jijiu">
-                <span class="text-[#259644]">
-                  {{ '✔' }}
-                </span>
-              </el-button>
-              <span v-else></span>
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="备注" align="center" prop="remark" show-overflow-tooltip />
-
-          <zm-table-column label="操作" align="center" min-width="120px" action fixed="right">
-            <template #default="scope">
-              <el-button
-                link
-                type="primary"
-                @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:qhse-cert-person:update']">
-                编辑
-              </el-button>
-              <el-button
-                link
-                type="danger"
-                @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:qhse-cert-person:delete']">
-                删除
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-        <!-- 分页 -->
+      <div class="h-8 mt-2 flex items-center justify-end">
         <Pagination
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
           @pagination="getList" />
-      </ContentWrap>
-    </el-col>
-  </el-row>
+      </div>
+    </div>
+  </div>
 
   <!-- 表单弹窗:添加/修改 -->
   <CertPersonForm ref="formRef" @success="getList" />
@@ -261,9 +295,12 @@
 import download from '@/utils/download'
 import { CertPersonApi } from '@/api/pms/qhse/index'
 import CertPersonForm from './CertPersonForm.vue'
-import DeptTree from '@/views/system/user/DeptTree2.vue'
+
+import { useUserStore } from '@/store/modules/user'
 
 defineOptions({ name: 'CertPerson' })
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
@@ -271,13 +308,14 @@ const { t } = useI18n() // 国际化
 const loading = ref(true) // 列表的加载中
 const list = ref([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const isLeftContentCollapsed = ref(false)
+
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   username: undefined,
   nickname: undefined,
-  postName: undefined
+  postName: undefined,
+  deptId: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出的加载中

+ 320 - 257
src/views/pms/qhse/certificate.vue

@@ -1,266 +1,280 @@
 <!-- src/views/pms/qhse/certificate.vue -->
 <template>
-  <el-row :gutter="20">
-    <!-- 左侧部门树 -->
-    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
-    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
-      <div style="border: none" class="!border-none bg-white rounded-sm pt-4 px-2">
-        <!-- 搜索工作栏 -->
-        <el-form :model="queryParams" ref="queryFormRef" :inline="true">
-          <el-form-item label="证书类型" prop="type">
-            <el-select v-model="queryParams.type" placeholder="请选择证书类型" style="width: 120px">
-              <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="证书类别"
-              clearable
-              class="!w-150px">
-              <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>
-
-          <el-form-item label="所属人" prop="userName">
-            <el-input placeholder="输入所属人" v-model="queryParams.userName" />
-          </el-form-item>
-
-          <el-form-item label="是否过期" prop="expired">
-            <el-select
-              v-model="queryParams.expired"
-              placeholder="请选择是否过期"
-              clearable
-              style="width: 150px">
-              <el-option
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value" />
-            </el-select>
-          </el-form-item>
-
-          <el-form-item label="是否预警" prop="alertWarn">
-            <el-select
-              v-model="queryParams.alertWarn"
-              placeholder="请选择是否预警"
-              clearable
-              style="width: 150px">
-              <el-option
-                v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value" />
-            </el-select>
-          </el-form-item>
-
-          <el-form-item>
-            <el-button @click="handleAdd" type="primary"
-              ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
-            >
-            <el-button @click="handleQuery"
-              ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
-            >
-            <el-button @click="resetQuery"
-              ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
-            >
-            <el-button @click="handleExport" type="success" plain :loading="exportLoading"
-              ><Icon icon="ep:download" class="mr-5px" /> 导出</el-button
-            >
-          </el-form-item>
-        </el-form>
+  <div
+    class="qhse-page grid grid-cols-[auto_1fr] grid-rows-[auto_auto_minmax(0,1fr)_auto] gap-3 gap-x-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
+    <DeptTreeSelect
+      class="row-span-4"
+      :top-id="rootDeptId"
+      :deptId="deptId"
+      v-model="queryParams.deptId"
+      :init-select="false"
+      :show-title="false"
+      request-api="getSimpleDeptList"
+      @node-click="handleDeptNodeClick" />
+
+    <div class="stats-cards">
+      <div
+        class="stats-card stats-card--expired stats-card--clickable"
+        @click="handleStatCardClick('expired')">
+        <div class="flex items-center gap-2">
+          <Icon icon="ep:info-filled" color="#de3b3b" />
+          <div class="stats-card__label">已过期</div>
+        </div>
+
+        <div class="stats-card__value">
+          <CountTo
+            :duration="2600"
+            :end-val="expired"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!" />
+        </div>
+      </div>
+      <div
+        class="stats-card stats-card--warn stats-card--clickable"
+        @click="handleStatCardClick('warn')">
+        <div class="flex items-center gap-2">
+          <Icon icon="ep:bell-filled" color="#d97706" />
+          <div class="stats-card__label">60天预警</div>
+        </div>
+        <div class="stats-card__value">
+          <CountTo
+            :duration="2600"
+            :end-val="warn"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#d97706]!" />
+        </div>
       </div>
+      <div
+        class="stats-card stats-card--total stats-card--clickable"
+        @click="handleStatCardClick('total')">
+        <div class="flex items-center gap-2">
+          <Icon icon="eos-icons:counting" color="#2563eb" />
+          <div class="stats-card__label">证书总数</div>
+        </div>
+
+        <div class="stats-card__value">
+          <CountTo
+            :duration="2600"
+            :end-val="totalCert"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
+        </div>
+      </div>
+      <div
+        class="stats-card stats-card--personal stats-card--clickable"
+        @click="handleStatCardClick('personal')">
+        <div class="flex items-center gap-2">
+          <Icon
+            icon="material-symbols-light:account-circle-outline"
+            class="w-20 h-20"
+            color="#2563eb" />
+          <div class="stats-card__label">个人证书</div>
+        </div>
 
-      <!-- 列表 -->
-      <ContentWrap class="flex-1 overflow-hidden mt-15px" style="border: none">
-        <div class="stats-cards">
-          <div class="stats-card stats-card--expired">
-            <div class="flex items-center gap-2">
-              <Icon icon="ep:info-filled" color="#de3b3b" />
-              <div class="stats-card__label">已过期</div>
-            </div>
-
-            <div class="stats-card__value">
-              <CountTo
-                :duration="2600"
-                :end-val="expired"
-                :start-val="0"
-                class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!" />
-            </div>
-          </div>
-          <div class="stats-card stats-card--warn">
-            <div class="flex items-center gap-2">
-              <Icon icon="ep:bell-filled" color="#d97706" />
-              <div class="stats-card__label">60天预警</div>
-            </div>
-            <div class="stats-card__value">
-              <CountTo
-                :duration="2600"
-                :end-val="warn"
-                :start-val="0"
-                class="stats-card__value text-[40px]! pt-10 text-center! text-[#d97706]!" />
-            </div>
-          </div>
-          <div class="stats-card stats-card--total">
-            <div class="flex items-center gap-2">
-              <Icon icon="eos-icons:counting" color="#2563eb" />
-              <div class="stats-card__label">证书总数</div>
-            </div>
-
-            <div class="stats-card__value">
-              <CountTo
-                :duration="2600"
-                :end-val="totalCert"
-                :start-val="0"
-                class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
-            </div>
-          </div>
-          <div class="stats-card stats-card--personal">
-            <div class="flex items-center gap-2">
-              <Icon
-                icon="material-symbols-light:account-circle-outline"
-                class="w-20 h-20"
-                color="#2563eb" />
-              <div class="stats-card__label">个人证书</div>
-            </div>
-
-            <div class="stats-card__value">
-              <CountTo
-                :duration="2600"
-                :end-val="personal"
-                :start-val="0"
-                class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
-            </div>
-          </div>
-          <div class="stats-card stats-card--organization">
-            <div class="flex items-center gap-2">
-              <Icon icon="bxs:building" class="w-20 h-20" color="#2563eb" />
-              <div class="stats-card__label">组织证书</div>
-            </div>
-            <div class="stats-card__value">
-              <CountTo
-                :duration="2600"
-                :end-val="organization"
-                :start-val="0"
-                class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
-            </div>
-          </div>
+        <div class="stats-card__value">
+          <CountTo
+            :duration="2600"
+            :end-val="personal"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#2563eb]!" />
         </div>
-        <zm-table
-          :loading="loading"
-          :data="list"
-          height="calc(61vh - 220px)"
-          :show-overflow-tooltip="true"
-          :row-style="tableRowStyle"
-          :row-class-name="tableRowClassName">
-          >
-          <zm-table-column :label="t('monitor.serial')" width="70" align="center">
-            <template #default="scope">
-              {{ scope.$index + 1 }}
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="证书类型" align="center" prop="type">
-            <template #default="scope">
-              {{ getCertificateTypeText(scope.row.type) }}
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="证书类别" align="center" width="150" prop="classify">
-            <template #default="scope">
-              <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>
-          </zm-table-column>
-          <zm-table-column label="证书名称" align="center" prop="certName" show-overflow-tooltip />
-
-          <zm-table-column label="所属人" align="center" prop="userName" />
-          <zm-table-column label="所在部门" align="center" prop="deptName" />
-
-          <zm-table-column label="颁发机构" align="center" prop="certOrg" />
-
-          <zm-table-column label="证书标准" align="center" prop="certStandard" />
-
-          <zm-table-column label="颁发时间" align="center" prop="certIssue">
-            <template #default="scope">
-              {{ formatDateCorrectly(scope.row.certIssue) }}
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="有效期" align="center">
-            <template #default="scope">
-              {{ formatDateCorrectly(scope.row.certExpire) }}
-            </template>
-          </zm-table-column>
-
-          <zm-table-column label="到期提醒" align="center">
-            <template #default="scope"> {{ scope.row.noticeBefore }}天前提醒 </template>
-          </zm-table-column>
-
-          <zm-table-column label="备注" align="center" prop="remark" />
-          <zm-table-column
-            :label="t('devicePerson.operation')"
-            align="center"
-            fixed="right"
-            min-width="180px"
-            action>
-            <template #default="scope">
-              <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
-              <el-button link type="danger" @click="handleDelete(scope.row.id)"> 删除 </el-button>
-              <el-button
-                link
-                type="success"
-                v-if="scope.row.certPic"
-                @click="viewFile(scope.row.certPic)">
-                查看证书
-              </el-button>
-            </template>
-          </zm-table-column>
-        </zm-table>
-
-        <!-- 分页 -->
+      </div>
+    </div>
+
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 pt-4 flex items-center flex-wrap min-w-0"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true">
+      <el-form-item label="证书类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择证书类型" style="width: 120px">
+          <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="证书类别" clearable class="!w-150px">
+          <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>
+
+      <el-form-item label="所属人" prop="userName">
+        <el-input placeholder="输入所属人" v-model="queryParams.userName" />
+      </el-form-item>
+
+      <el-form-item label="是否过期" prop="expired">
+        <el-select
+          v-model="queryParams.expired"
+          placeholder="请选择是否过期"
+          clearable
+          style="width: 150px">
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="是否预警" prop="alertWarn">
+        <el-select
+          v-model="queryParams.alertWarn"
+          placeholder="请选择是否预警"
+          clearable
+          style="width: 150px">
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value" />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button @click="handleAdd" type="primary"
+          ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
+        >
+        <el-button @click="handleQuery"
+          ><Icon icon="ep:search" class="mr-5px" /> {{ t('devicePerson.search') }}</el-button
+        >
+        <el-button @click="resetQuery"
+          ><Icon icon="ep:refresh" class="mr-5px" /> {{ t('devicePerson.reset') }}</el-button
+        >
+        <el-button @click="handleExport" type="success" plain :loading="exportLoading"
+          ><Icon icon="ep:download" class="mr-5px" /> 导出</el-button
+        >
+      </el-form-item>
+    </el-form>
+
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-3 min-w-0">
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <zm-table
+              :loading="loading"
+              :data="list"
+              :width="width"
+              :height="height"
+              :show-overflow-tooltip="true"
+              :row-style="tableRowStyle"
+              :row-class-name="tableRowClassName">
+              >
+              <zm-table-column :label="t('monitor.serial')" width="70" align="center">
+                <template #default="scope">
+                  {{ scope.$index + 1 }}
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="证书类型" align="center" prop="type">
+                <template #default="scope">
+                  {{ getCertificateTypeText(scope.row.type) }}
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="证书类别" align="center" width="150" prop="classify">
+                <template #default="scope">
+                  <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>
+              </zm-table-column>
+              <zm-table-column
+                label="证书名称"
+                align="center"
+                prop="certName"
+                show-overflow-tooltip />
+
+              <zm-table-column label="所属人" align="center" prop="userName" />
+              <zm-table-column label="所在部门" align="center" prop="deptName" />
+
+              <zm-table-column label="颁发机构" align="center" prop="certOrg" />
+
+              <zm-table-column label="证书标准" align="center" prop="certStandard" />
+
+              <zm-table-column label="颁发时间" align="center" prop="certIssue">
+                <template #default="scope">
+                  {{ formatDateCorrectly(scope.row.certIssue) }}
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="有效期" align="center">
+                <template #default="scope">
+                  {{ formatDateCorrectly(scope.row.certExpire) }}
+                </template>
+              </zm-table-column>
+
+              <zm-table-column label="到期提醒" align="center">
+                <template #default="scope"> {{ scope.row.noticeBefore }}天前提醒 </template>
+              </zm-table-column>
+
+              <zm-table-column label="备注" align="center" prop="remark" />
+              <zm-table-column
+                :label="t('devicePerson.operation')"
+                align="center"
+                fixed="right"
+                min-width="180px"
+                action>
+                <template #default="scope">
+                  <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
+                  <el-button link type="danger" @click="handleDelete(scope.row.id)">
+                    删除
+                  </el-button>
+                  <el-button
+                    link
+                    type="success"
+                    v-if="scope.row.certPic"
+                    @click="viewFile(scope.row.certPic)">
+                    查看证书
+                  </el-button>
+                </template>
+              </zm-table-column>
+            </zm-table>
+          </template>
+        </el-auto-resizer>
+      </div>
+
+      <div class="h-8 mt-2 flex items-center justify-end">
         <Pagination
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
           @pagination="getList" />
-      </ContentWrap>
+      </div>
+    </div>
 
-      <ContentWrap style="margin-top: -5px">
-        <el-alert title="证书已过期红色预警" type="error" show-icon :closable="false">
-          <template #icon>
-            <Bell />
-          </template>
-        </el-alert>
-
-        <el-alert
-          title="证书60天橙色预警"
-          type="warning"
-          show-icon
-          :closable="false"
-          style="margin-top: 5px">
-          <template #icon>
-            <Bell />
-          </template>
-        </el-alert>
-      </ContentWrap>
-    </el-col>
-  </el-row>
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-3 min-w-0">
+      <el-alert title="证书已过期红色预警" type="error" show-icon :closable="false">
+        <template #icon>
+          <Bell />
+        </template>
+      </el-alert>
+
+      <el-alert
+        title="证书60天橙色预警"
+        type="warning"
+        show-icon
+        :closable="false"
+        style="margin-top: 5px">
+        <template #icon>
+          <Bell />
+        </template>
+      </el-alert>
+    </div>
+  </div>
 
   <!-- 新增/编辑证书对话框 -->
-  <el-dialog
+  <Dialog
     :title="dialogTitle"
     v-model="dialogVisible"
     width="600px"
@@ -270,7 +284,7 @@
       ref="formRef"
       :model="formData"
       :rules="formRules"
-      label-width="120px"
+      label-width="auto"
       v-loading="formLoading">
       <el-form-item label="证书类型" prop="type">
         <el-select
@@ -374,7 +388,7 @@
         <!-- <UploadImage v-model="formData.certPic" /> -->
         <UploadFile
           v-model="formData.certPic"
-          :file-type="['pdf', 'jpg', 'png', 'jpeg']"
+          :file-type="['pdf', 'jpg', 'png', 'jpeg', 'bmp']"
           :file-size="100"
           class="min-w-80px" />
       </el-form-item>
@@ -384,9 +398,9 @@
       <el-button @click="closeDialog">取 消</el-button>
       <el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
     </template>
-  </el-dialog>
+  </Dialog>
 
-  <el-dialog v-model="dialogFileView" title="证书附件">
+  <Dialog v-model="dialogFileView" title="证书附件">
     <div
       v-for="(file, index) in fileList"
       :key="index"
@@ -407,7 +421,7 @@
         <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
       </div>
     </template>
-  </el-dialog>
+  </Dialog>
 </template>
 
 <script setup lang="ts">
@@ -427,6 +441,9 @@ const userStore = useUserStore()
 
 defineOptions({ name: 'IotQHSECertificate' })
 
+const rootDeptId = 156
+const deptId = useUserStore().getUser.deptId || rootDeptId
+
 const loading = ref(true) // 列表的加载中
 const formLoading = ref(false) // 表单加载中
 const submitLoading = ref(false) // 提交按钮加载中
@@ -566,6 +583,39 @@ const handleQuery = () => {
   getList()
 }
 
+const handleStatCardClick = (type: 'expired' | 'warn' | 'total' | 'personal') => {
+  queryParams.pageNo = 1
+
+  if (type === 'expired') {
+    queryParams.expired = true
+    queryParams.alertWarn = undefined
+    queryParams.type = undefined
+    getList()
+    return
+  }
+
+  if (type === 'warn') {
+    queryParams.alertWarn = true
+    queryParams.expired = undefined
+    queryParams.type = undefined
+    getList()
+    return
+  }
+
+  if (type === 'personal') {
+    queryParams.type = 'personal'
+    queryParams.expired = undefined
+    queryParams.alertWarn = undefined
+    getList()
+    return
+  }
+
+  queryParams.type = undefined
+  queryParams.expired = undefined
+  queryParams.alertWarn = undefined
+  getList()
+}
+
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryParams.deptId = ''
@@ -839,9 +889,9 @@ onMounted(async () => {
 
 .stats-cards {
   display: grid;
-  grid-template-columns: repeat(5, minmax(0, 1fr));
+  grid-template-columns: repeat(4, minmax(0, 1fr));
   gap: 12px;
-  margin-bottom: 16px;
+  margin-bottom: 5px;
 }
 
 .stats-card {
@@ -852,6 +902,19 @@ onMounted(async () => {
   box-shadow: 0 4px 12px rgb(31 91 184 / 8%);
 }
 
+.stats-card--clickable {
+  cursor: pointer;
+  transition:
+    transform 0.18s ease,
+    box-shadow 0.18s ease,
+    border-color 0.18s ease;
+}
+
+.stats-card--clickable:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 10px 24px rgb(31 91 184 / 14%);
+}
+
 .stats-card__label {
   font-size: 14px;
   font-weight: 600;

+ 1 - 1
src/views/pms/qhse/deviceCert/DeviceCertForm.vue

@@ -104,7 +104,7 @@
           <el-form-item label="附件" prop="file">
             <UploadFile
               v-model="formData.file"
-              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx']"
+              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx', 'bmp']"
               :limit="3"
               :file-size="100"
               class="min-w-80px" />

+ 1 - 1
src/views/pms/qhse/emergencyDrill/CertDrawer.vue

@@ -174,7 +174,7 @@
           <el-form-item label="附件" prop="file">
             <UploadFile
               v-model="formData.file"
-              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx']"
+              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx', 'bmp']"
               :limit="3"
               :file-size="100"
               class="min-w-80px" />

+ 1 - 1
src/views/pms/qhse/emergencyDrill/EmergencyDrillForm.vue

@@ -71,7 +71,7 @@
           <el-form-item label="附件" prop="file">
             <UploadFile
               v-model="formData.file"
-              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx']"
+              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx', 'bmp']"
               :limit="3"
               :file-size="100"
               class="min-w-80px" />

+ 1 - 1
src/views/pms/qhse/emergencyDrill/bookCheck/EmergencyBookForm.vue

@@ -80,7 +80,7 @@
           <el-form-item label="附件" prop="file">
             <UploadFile
               v-model="formData.file"
-              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx']"
+              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx', 'bmp']"
               :limit="3"
               :file-size="100"
               class="min-w-80px" />

+ 72 - 38
src/views/pms/qhse/index.vue

@@ -11,6 +11,46 @@
       request-api="getSimpleDeptList"
       @node-click="handleDeptNodeClick" />
 
+    <div class="stats-cards min-w-0">
+      <div
+        class="stats-card stats-card--expired stats-card--clickable"
+        @click="handleStatCardClick('expired')">
+        <div class="stats-card__header">
+          <el-icon class="stats-card__icon" :size="28">
+            <Icon icon="ep:info-filled" />
+          </el-icon>
+          <div class="stats-card__label">已过期</div>
+        </div>
+        <div class="stats-card__value text-[40px]! pt-10 text-center!">
+          <CountTo
+            :duration="2600"
+            :end-val="expired"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!" />
+        </div>
+      </div>
+      <div
+        class="stats-card stats-card--warn stats-card--clickable"
+        @click="handleStatCardClick('warn')">
+        <div class="stats-card__header">
+          <el-icon class="stats-card__icon" :size="28">
+            <Icon icon="ep:bell-filled" />
+          </el-icon>
+          <div class="stats-card__label">90天预警</div>
+        </div>
+        <div class="stats-card__value text-[40px]! pt-10 text-center!">
+          <CountTo
+            :duration="2600"
+            :end-val="warn"
+            :start-val="0"
+            class="stats-card__value text-[40px]! pt-10 text-center! text-[#df8a28]!" />
+        </div>
+      </div>
+      <div class="stats-chart-card">
+        <div class="stats-card__label">分类统计</div>
+        <div ref="staticChartRef" class="stats-chart"></div>
+      </div>
+    </div>
     <el-form
       ref="queryFormRef"
       :model="queryParams"
@@ -77,43 +117,6 @@
       </div>
     </el-form>
 
-    <div class="stats-cards min-w-0">
-      <div class="stats-card stats-card--expired">
-        <div class="stats-card__header">
-          <el-icon class="stats-card__icon" :size="28">
-            <Icon icon="ep:info-filled" />
-          </el-icon>
-          <div class="stats-card__label">已过期</div>
-        </div>
-        <div class="stats-card__value text-[40px]! pt-10 text-center!">
-          <CountTo
-            :duration="2600"
-            :end-val="expired"
-            :start-val="0"
-            class="stats-card__value text-[40px]! pt-10 text-center! text-[#e35656]!" />
-        </div>
-      </div>
-      <div class="stats-card stats-card--warn">
-        <div class="stats-card__header">
-          <el-icon class="stats-card__icon" :size="28">
-            <Icon icon="ep:bell-filled" />
-          </el-icon>
-          <div class="stats-card__label">90天预警</div>
-        </div>
-        <div class="stats-card__value text-[40px]! pt-10 text-center!">
-          <CountTo
-            :duration="2600"
-            :end-val="warn"
-            :start-val="0"
-            class="stats-card__value text-[40px]! pt-10 text-center! text-[#df8a28]!" />
-        </div>
-      </div>
-      <div class="stats-chart-card">
-        <div class="stats-card__label">分类统计</div>
-        <div ref="staticChartRef" class="stats-chart"></div>
-      </div>
-    </div>
-
     <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-3 min-w-0">
       <div class="flex-1 relative min-h-0">
         <el-auto-resizer class="absolute">
@@ -571,7 +574,8 @@ const formRules = {
   classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
   deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
   measureCertNo: [{ required: true, message: '证书编码不能为空', trigger: 'blur' }],
-  validity: [{ required: true, message: '有效期不能为空', trigger: 'blur' }]
+  validity: [{ required: true, message: '有效期不能为空', trigger: 'blur' }],
+  serialNo: [{ required: true, message: '序列号不能为空', trigger: 'blur' }]
 }
 
 /** 查询列表 */
@@ -647,6 +651,23 @@ const handleQuery = () => {
   getList()
 }
 
+const handleStatCardClick = (type: 'expired' | 'warn') => {
+  queryParams.pageNo = 1
+
+  if (type === 'expired') {
+    queryParams.expired = true
+    queryParams.alertWarn = undefined
+    getList()
+    return
+  }
+
+  if (type === 'warn') {
+    queryParams.alertWarn = true
+    queryParams.expired = undefined
+    getList()
+  }
+}
+
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
@@ -1039,6 +1060,19 @@ onUnmounted(() => {
   box-shadow: 0 4px 12px rgb(31 91 184 / 8%);
 }
 
+.stats-card--clickable {
+  cursor: pointer;
+  transition:
+    transform 0.18s ease,
+    box-shadow 0.18s ease,
+    border-color 0.18s ease;
+}
+
+.stats-card--clickable:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 10px 24px rgb(31 91 184 / 14%);
+}
+
 .stats-card__label {
   font-size: 14px;
   font-weight: 600;

+ 2 - 5
src/views/pms/qhse/iotmeasuredetect/IotMeasureDetectForm.vue

@@ -15,10 +15,7 @@
               placeholder="计量器具"
               style="width: 300px">
               <template #append>
-                <el-link
-                  :disabled="measureSelectLocked"
-                  @click="selectMeasure"
-                  :underline="false">
+                <el-link :disabled="measureSelectLocked" @click="selectMeasure" :underline="false">
                   选择
                 </el-link>
               </template>
@@ -92,7 +89,7 @@
           <el-form-item label="附件" prop="file">
             <UploadFile
               v-model="formData.file"
-              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx']"
+              :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'xls', 'xlsx', 'bmp']"
               :limit="3"
               :file-size="100"
               class="min-w-80px" />

+ 1 - 1
src/views/pms/qhse/jsa/IotSocSummaryForm.vue

@@ -45,7 +45,7 @@
       <el-form-item label="附件" prop="jsaFile">
         <UploadFile
           v-model="formData.jsaFile"
-          :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg']"
+          :file-type="['doc', 'docx', 'pdf', 'jpg', 'png', 'jpeg', 'bmp']"
           :limit="3"
           :file-size="100"
           class="min-w-80px" />

+ 0 - 1
src/views/pms/qhse/monthlyReport/index.vue

@@ -256,7 +256,6 @@ const loadTableData = async () => {
       pageNo: pagination.pageNo,
       pageSize: pagination.pageSize,
       createTime: queryParams.createTime,
-
       title: queryParams.title,
       deptId: queryParams.deptId
     }

+ 1 - 1
src/views/pms/qhse/ptw/index.vue

@@ -280,7 +280,7 @@
       <el-form-item label="附件" prop="file">
         <UploadFile
           v-model="formData.file"
-          :file-type="['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'jpeg']"
+          :file-type="['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'jpeg', 'bmp']"
           :limit="3"
           :file-size="100"
           class="min-w-80px" />

+ 21 - 35
src/views/pms/qhse/reportSummary/index.vue

@@ -1,5 +1,5 @@
 <script lang="ts" setup>
-import { QhseMonthReportApi } from '@/api/pms/qhse'
+import { QhseMonthReportSummaryApi, QhseMonthReportApi } from '@/api/pms/qhse'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import QhseMonthReportPreviewDrawer from './preview-drawer.vue'
 import type { QhseMonthReportItem, QhseMonthReportListItem } from './types'
@@ -9,12 +9,10 @@ const loading = ref(false)
 const list = ref<QhseMonthReportListItem[]>([])
 const total = ref(0)
 const visible = ref(false)
-const currentId = ref<number>()
 
 const queryFormRef = ref()
 const queryParams = reactive({
-  title: '',
-  createTime: undefined as string[] | undefined
+  yearMonths: ''
 })
 const pagination = reactive({
   pageNo: 1,
@@ -26,7 +24,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents<QhseMonthReportListItem>()
 async function getList() {
   loading.value = true
   try {
-    const res = await QhseMonthReportApi.getQhseMonthReportPage({
+    const res = await QhseMonthReportSummaryApi.getQhseMonthReportSummaryList({
       ...queryParams,
       pageNo: pagination.pageNo,
       pageSize: pagination.pageSize
@@ -49,8 +47,9 @@ function resetQuery() {
   getList()
 }
 
-function handleView(row: QhseMonthReportItem) {
-  currentId.value = row.id
+let yearMonths = ref<string | undefined>('')
+async function handleView(row: QhseMonthReportItem) {
+  yearMonths.value = row.yearMonths
   visible.value = true
 }
 
@@ -65,12 +64,6 @@ function handleCurrentChange(pageNo: number) {
   getList()
 }
 
-function formatDate(value?: string | number | Date) {
-  if (!value) return '-'
-  const date = dayjs(value)
-  return date.isValid() ? date.format('YYYY-MM-DD') : '-'
-}
-
 onMounted(() => {
   getList()
 })
@@ -85,17 +78,12 @@ onMounted(() => {
       inline
       label-position="left"
       class="report-summary-query min-w-0 overflow-hidden rounded-lg bg-white p-4 shadow dark:bg-[#1d1e1f]">
-      <el-form-item label="月报标题" prop="title">
-        <el-input v-model="queryParams.title" placeholder="请输入月报标题" clearable />
-      </el-form-item>
-      <el-form-item label="创建日期" prop="createTime">
+      <el-form-item label="月报期次" prop="yearMonths">
         <el-date-picker
-          v-model="queryParams.createTime"
-          type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          class="!w-300px" />
+          v-model="queryParams.yearMonths"
+          type="month"
+          value-format="YYYY-MM"
+          class="w-full!" />
       </el-form-item>
       <el-form-item class="report-summary-query__actions">
         <el-button type="primary" @click="handleQuery">搜索</el-button>
@@ -103,7 +91,8 @@ onMounted(() => {
       </el-form-item>
     </el-form>
 
-    <div class="report-summary-data-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-h-0">
+    <div
+      class="report-summary-data-panel bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-h-0">
       <div class="flex-1 min-h-0 relative">
         <el-auto-resizer class="report-summary-table-view absolute">
           <template #default="{ width, height }">
@@ -114,14 +103,14 @@ onMounted(() => {
               :max-height="height"
               :height="height"
               show-border>
-              <ZmTableColumn label="月报标题" prop="title" :min-width="260" />
-              <ZmTableColumn label="工单填报人" prop="personName" width="160" />
-              <ZmTableColumn
+              <ZmTableColumn label="月报期次" prop="yearMonths" />
+              <ZmTableColumn label="月报标题" prop="title" />
+              <!-- <ZmTableColumn
                 label="创建日期"
                 prop="createTime"
                 cover-formatter
                 :real-value="({ createTime }) => formatDate(createTime)"
-                width="160" />
+                width="160" /> -->
               <ZmTableColumn label="操作" width="100" fixed="right">
                 <template #default="{ row }">
                   <el-button size="default" link type="success" @click="handleView(row)">
@@ -141,13 +130,10 @@ onMounted(() => {
                 <strong>{{ item.title || '-' }}</strong>
               </div>
               <div class="report-summary-card__field">
-                <span>填报人</span>
-                <strong>{{ item.personName || '-' }}</strong>
-              </div>
-              <div class="report-summary-card__field">
-                <span>创建日期</span>
-                <strong>{{ formatDate(item.createTime) }}</strong>
+                <span>月报期次</span>
+                <strong>{{ item.yearMonths || '-' }}</strong>
               </div>
+
               <div class="report-summary-card__actions">
                 <el-button size="default" link type="success" @click="handleView(item)">
                   查看
@@ -174,7 +160,7 @@ onMounted(() => {
 
   <QhseMonthReportPreviewDrawer
     v-model:visible="visible"
-    :id="currentId"
+    :yearMonths="yearMonths"
     @update:visible="visible = $event" />
 </template>
 

+ 161 - 179
src/views/pms/qhse/reportSummary/preview-drawer.vue

@@ -1,17 +1,12 @@
 <script lang="ts" setup>
-import { QhseMonthReportApi } from '@/api/pms/qhse'
-import type {
-  QhseMonthReportItem,
-  ReportCompanyColumn,
-  ReportMetricRow,
-  ReportMetricValue
-} from './types'
+import { QhseMonthReportSummaryApi } from '@/api/pms/qhse'
+import type { QhseMonthReportItem, ReportCompanyColumn, ReportMetricRow } from './types'
 import dayjs from 'dayjs'
 import { computed, ref, watch } from 'vue'
 
 interface Props {
   visible: boolean
-  id?: number
+  yearMonths: string | undefined
 }
 
 interface TableRow {
@@ -26,165 +21,129 @@ const props = defineProps<Props>()
 const emits = defineEmits(['update:visible'])
 
 const loading = ref(false)
-const report = ref<QhseMonthReportItem>()
-
-const companyColumns: ReportCompanyColumn[] = [
-  { key: 'rhxy', label: '瑞恒兴域' },
-  { key: 'scrd', label: '四川瑞都' },
-  { key: 'sxty', label: '陕西瑞鹰' },
-  { key: 'eys', label: '俄油服' },
-  { key: 'rqny', label: '瑞气能源' },
-  { key: 'rljs', label: '瑞霖技术' },
-  { key: 'bjhq', label: '北京总部' }
-]
+const reportList = ref<QhseMonthReportItem[]>([])
 
 const metricRows: ReportMetricRow[] = [
-  { category: '人工时与里程', label: '员工人数', field: 'employee', unit: '人' },
-  { category: '人工时与里程', label: '分包商人数', field: 'subcontractors', unit: '人' },
+  { category: '人工时和安全行驶公里数', label: '员工人数', field: 'employee', unit: '人' },
+  { category: '人工时和安全行驶公里数', label: '分包商人数', field: 'subcontractors', unit: '人' },
   {
-    category: '人工时与里程',
-    label: '安全行驶里程数(公里)',
+    category: '人工时和安全行驶公里数',
+    label: '安全行驶里程数(公里)',
     field: 'drivingMileage',
     unit: '公里'
   },
-  { category: '人工时与里程', label: '总人工时数(小时)', field: 'totalManHours', unit: '小时' },
-  { category: '被动性指标', label: '无事故累计天数(天)', field: 'withoutAccident', unit: '天' },
-  { category: '被动性指标', label: '死亡事故(起)', field: 'fatality', unit: '起' },
-  { category: '被动性指标', label: '损失工时事故(起)', field: 'injury', unit: '起' },
-  { category: '被动性指标', label: '工作受限事件(起)', field: 'restrictedCase', unit: '起' },
-  { category: '被动性指标', label: '医疗处理事件(起)', field: 'medicalCase', unit: '起' },
-  { category: '被动性指标', label: '急救箱事件(起)', field: 'firstAidCase', unit: '起' },
-  { category: '被动性指标', label: '交通事故(起)', field: 'vehicleAccident', unit: '起' },
-  { category: '被动性指标', label: '未遂事件(起)', field: 'nearMiss', unit: '起' },
-  { category: '被动性指标', label: '泄漏事件(起)', field: 'spill', unit: '起' },
-  { category: '被动性指标', label: '违反保命规则的次数(次)', field: 'lifeSavingRules', unit: '次' },
-  { category: '主动性指标', label: '班前会(次)', field: 'toolboxTalk', unit: '次' },
-  { category: '主动性指标', label: 'QHSE管理委员会会议', field: 'committeeMeeting', unit: '次' },
-  { category: '主动性指标', label: 'QHSE月度例会', field: 'monthlyMeeting', unit: '次' },
-  { category: '主动性指标', label: '公司级隐患排查', field: 'companyHazard', unit: '次' },
-  { category: '主动性指标', label: 'QHSE检查', field: 'qhseInspection', unit: '次' },
-  { category: '主动性指标', label: '安全观察卡', field: 'socCards', unit: '张' },
-  { category: '主动性指标', label: '工作许可审核', field: 'ptwAudit', unit: '份' },
-  { category: '主动性指标', label: '工作安全分析', field: 'jsa', unit: '次' },
-  { category: '主动性指标', label: '演练次数', field: 'drills', unit: '次' },
-  { category: '主动性指标', label: 'QHSE培训次数', field: 'training', unit: '次' },
-  { category: '主动性指标', label: 'QHSE培训人次', field: 'participantsTraining', unit: '人次' },
-  { category: '主动性指标', label: 'QHSE培训学时数', field: 'trainingsHours', unit: '小时' },
-  { category: '环境数据', label: '水消耗', field: 'waterConsumption', unit: '吨' },
-  { category: '环境数据', label: '柴油消耗', field: 'dieselConsumption', unit: '升' },
-  { category: '环境数据', label: '用电量', field: 'electricityConsumption', unit: '千瓦·小时' },
-  { category: '环境数据', label: '天然气消耗量', field: 'naturalGasConsumption', unit: '立方米' },
-  { category: '其他信息', label: '备注', field: 'remark', unit: '/' }
+  {
+    category: '人工时和安全行驶公里数',
+    label: '总人工时数(小时)',
+    field: 'totalManHours',
+    unit: '小时'
+  },
+  { category: 'QHSE被动性指标统计', label: '无事故累计天数', field: 'withoutAccident', unit: '天' },
+  { category: 'QHSE被动性指标统计', label: '死亡事故(起)', field: 'fatality', unit: '起' },
+  { category: 'QHSE被动性指标统计', label: '损失工时事故(起)', field: 'injury', unit: '起' },
+  {
+    category: 'QHSE被动性指标统计',
+    label: '工作受限事件(起)',
+    field: 'restrictedCase',
+    unit: '起'
+  },
+  { category: 'QHSE被动性指标统计', label: '医疗处理事件(起)', field: 'medicalCase', unit: '起' },
+  { category: 'QHSE被动性指标统计', label: '急救箱事件(起)', field: 'firstAidCase', unit: '起' },
+  { category: 'QHSE被动性指标统计', label: '交通事故(起)', field: 'vehicleAccident', unit: '起' },
+  { category: 'QHSE被动性指标统计', label: '未遂事件(起)', field: 'nearMiss', unit: '起' },
+  { category: 'QHSE被动性指标统计', label: '泄漏事件(起)', field: 'spill', unit: '起' },
+  {
+    category: 'QHSE被动性指标统计',
+    label: '违反保命规则的次数(次)',
+    field: 'lifeSavingRules',
+    unit: '次'
+  },
+  { category: 'QHSE主动性指标统计', label: '班前会(次)', field: 'toolboxTalk', unit: '次' },
+  {
+    category: 'QHSE主动性指标统计',
+    label: 'QHSE管理委员会会议(次)',
+    field: 'committeeMeeting',
+    unit: '次'
+  },
+  {
+    category: 'QHSE主动性指标统计',
+    label: 'QHSE月度例会(次)',
+    field: 'monthlyMeeting',
+    unit: '次'
+  },
+  {
+    category: 'QHSE主动性指标统计',
+    label: '公司级隐患排查(次)',
+    field: 'companyHazard',
+    unit: '次'
+  },
+  { category: 'QHSE主动性指标统计', label: 'QHSE检查(次)', field: 'qhseInspection', unit: '次' },
+  { category: 'QHSE主动性指标统计', label: '安全观察卡(张)', field: 'socCards', unit: '张' },
+  { category: 'QHSE主动性指标统计', label: '工作许可审核(份)', field: 'ptwAudit', unit: '份' },
+  { category: 'QHSE主动性指标统计', label: '工作安全分析(次)', field: 'jsa', unit: '次' },
+  { category: 'QHSE主动性指标统计', label: '演练次数', field: 'drills', unit: '次' },
+  { category: 'QHSE主动性指标统计', label: 'QHSE培训次数', field: 'training', unit: '次' },
+  {
+    category: 'QHSE主动性指标统计',
+    label: 'QHSE培训人次',
+    field: 'participantsTraining',
+    unit: '人次'
+  },
+  {
+    category: 'QHSE主动性指标统计',
+    label: 'QHSE培训学时数(小时)',
+    field: 'trainingsHours',
+    unit: '小时'
+  },
+  { category: '环境数据', label: '水消耗(吨)', field: 'waterConsumption', unit: '吨' },
+  { category: '环境数据', label: '柴油消耗(升)', field: 'dieselConsumption', unit: '升' },
+  {
+    category: '环境数据',
+    label: '用电量(千瓦时)',
+    field: 'electricityConsumption',
+    unit: '千瓦时'
+  },
+  {
+    category: '环境数据',
+    label: '天然气消耗量(立方米)',
+    field: 'naturalGasConsumption',
+    unit: '立方米'
+  }
+  // { category: '其他信息', label: '备注', field: 'remark', unit: '/' }
 ]
 
-const categoryRowSpanMap = computed(() => {
-  return metricRows.reduce<Record<string, number>>((acc, item) => {
+const companyColumns = computed<ReportCompanyColumn[]>(() =>
+  reportList.value.map((item, index) => ({
+    key: getCompanyKey(index),
+    label: item.deptName || `公司${index + 1}`
+  }))
+)
+
+const companyRowMap = computed<Record<string, QhseMonthReportItem>>(() =>
+  Object.fromEntries(reportList.value.map((item, index) => [getCompanyKey(index), item]))
+)
+
+const categoryRowSpanMap = computed(() =>
+  metricRows.reduce<Record<string, number>>((acc, item) => {
     acc[item.category] = (acc[item.category] || 0) + 1
     return acc
   }, {})
-})
+)
 
-const firstRowIndexByCategory = computed(() => {
-  return metricRows.reduce<Record<string, number>>((acc, item, index) => {
+const firstRowIndexByCategory = computed(() =>
+  metricRows.reduce<Record<string, number>>((acc, item, index) => {
     if (acc[item.category] === undefined) acc[item.category] = index
     return acc
   }, {})
-})
-
-const mockMetricValueMap = computed<Record<string, Record<string, ReportMetricValue>>>(() => ({
-  employee: { rhxy: 32, scrd: 28, sxty: 25, eys: 18, rqny: 20, rljs: 14, bjhq: 12 },
-  subcontractors: { rhxy: 15, scrd: 12, sxty: 9, eys: 7, rqny: 6, rljs: 5, bjhq: 0 },
-  drivingMileage: {
-    rhxy: 12680.5,
-    scrd: 11024.2,
-    sxty: 9480.8,
-    eys: 6855.6,
-    rqny: 7742.4,
-    rljs: 4136.5,
-    bjhq: 980.2
-  },
-  totalManHours: {
-    rhxy: 3824,
-    scrd: 3416,
-    sxty: 2988,
-    eys: 2210,
-    rqny: 2456,
-    rljs: 1768,
-    bjhq: 960
-  },
-  withoutAccident: { rhxy: 186, scrd: 186, sxty: 186, eys: 132, rqny: 186, rljs: 186, bjhq: 186 },
-  fatality: { rhxy: 0, scrd: 0, sxty: 0, eys: 0, rqny: 0, rljs: 0, bjhq: 0 },
-  injury: { rhxy: 0, scrd: 1, sxty: 0, eys: 0, rqny: 0, rljs: 0, bjhq: 0 },
-  restrictedCase: { rhxy: 1, scrd: 0, sxty: 0, eys: 0, rqny: 1, rljs: 0, bjhq: 0 },
-  medicalCase: { rhxy: 1, scrd: 1, sxty: 0, eys: 0, rqny: 0, rljs: 0, bjhq: 0 },
-  firstAidCase: { rhxy: 2, scrd: 1, sxty: 1, eys: 0, rqny: 1, rljs: 0, bjhq: 0 },
-  vehicleAccident: { rhxy: 0, scrd: 0, sxty: 0, eys: 1, rqny: 0, rljs: 0, bjhq: 0 },
-  nearMiss: { rhxy: 3, scrd: 2, sxty: 1, eys: 1, rqny: 2, rljs: 1, bjhq: 0 },
-  spill: { rhxy: 0, scrd: 0, sxty: 0, eys: 0, rqny: 1, rljs: 0, bjhq: 0 },
-  lifeSavingRules: { rhxy: 0, scrd: 1, sxty: 0, eys: 0, rqny: 0, rljs: 0, bjhq: 0 },
-  toolboxTalk: { rhxy: 28, scrd: 24, sxty: 22, eys: 18, rqny: 20, rljs: 16, bjhq: 6 },
-  committeeMeeting: { rhxy: 1, scrd: 1, sxty: 1, eys: 1, rqny: 1, rljs: 1, bjhq: 1 },
-  monthlyMeeting: { rhxy: 1, scrd: 1, sxty: 1, eys: 1, rqny: 1, rljs: 1, bjhq: 1 },
-  companyHazard: { rhxy: 6, scrd: 5, sxty: 4, eys: 3, rqny: 4, rljs: 2, bjhq: 1 },
-  qhseInspection: { rhxy: 10, scrd: 9, sxty: 8, eys: 6, rqny: 7, rljs: 5, bjhq: 3 },
-  socCards: { rhxy: 42, scrd: 38, sxty: 31, eys: 22, rqny: 27, rljs: 18, bjhq: 12 },
-  ptwAudit: { rhxy: 18, scrd: 15, sxty: 13, eys: 8, rqny: 9, rljs: 6, bjhq: 2 },
-  jsa: { rhxy: 21, scrd: 18, sxty: 16, eys: 10, rqny: 12, rljs: 8, bjhq: 3 },
-  drills: { rhxy: 2, scrd: 2, sxty: 1, eys: 1, rqny: 1, rljs: 1, bjhq: 1 },
-  training: { rhxy: 5, scrd: 4, sxty: 4, eys: 3, rqny: 3, rljs: 2, bjhq: 2 },
-  participantsTraining: { rhxy: 96, scrd: 82, sxty: 74, eys: 48, rqny: 56, rljs: 35, bjhq: 24 },
-  trainingsHours: { rhxy: 64, scrd: 56, sxty: 48, eys: 32, rqny: 36, rljs: 24, bjhq: 16 },
-  waterConsumption: {
-    rhxy: 82.5,
-    scrd: 74.2,
-    sxty: 65.8,
-    eys: 48.6,
-    rqny: 53.4,
-    rljs: 31.8,
-    bjhq: 12.2
-  },
-  dieselConsumption: {
-    rhxy: 2680,
-    scrd: 2410,
-    sxty: 2085,
-    eys: 1530,
-    rqny: 1695,
-    rljs: 980,
-    bjhq: 220
-  },
-  electricityConsumption: {
-    rhxy: 4250,
-    scrd: 3980,
-    sxty: 3650,
-    eys: 2420,
-    rqny: 2860,
-    rljs: 1680,
-    bjhq: 920
-  },
-  naturalGasConsumption: {
-    rhxy: 1260,
-    scrd: 1140,
-    sxty: 980,
-    eys: 660,
-    rqny: 720,
-    rljs: 450,
-    bjhq: 180
-  },
-  remark: {
-    rhxy: '现场管理平稳',
-    scrd: '专项培训已完成',
-    sxty: '持续推进隐患整改',
-    eys: '强化车辆安全检查',
-    rqny: '开展环保专项复盘',
-    rljs: '重点盯控作业许可',
-    bjhq: '推进体系宣贯'
-  }
-}))
+)
 
-const tableRows = computed<TableRow[]>(() => {
-  return metricRows.map((row) => {
+const tableRows = computed<TableRow[]>(() =>
+  metricRows.map((row) => {
     const companyValues = Object.fromEntries(
-      companyColumns.map((company) => [company.key, getMetricCompanyValue(row.field, company.key)])
+      companyColumns.value.map((company) => [
+        company.key,
+        getMetricCompanyValue(row.field, company.key)
+      ])
     )
 
     return {
@@ -195,13 +154,17 @@ const tableRows = computed<TableRow[]>(() => {
       ...companyValues
     }
   })
-})
+)
 
-async function loadDetail(id: number) {
+async function loadDetail() {
   loading.value = true
   try {
-    const res = await QhseMonthReportApi.getQhseMonthReport(id)
-    report.value = ((res as any)?.data ?? res ?? {}) as QhseMonthReportItem
+    const res = await QhseMonthReportSummaryApi.getQhseMonthReportSummary({
+      yearMonths: props.yearMonths
+    })
+    reportList.value = ((res as any)?.data?.list ??
+      (res as any)?.list ??
+      []) as QhseMonthReportItem[]
   } finally {
     loading.value = false
   }
@@ -209,38 +172,44 @@ async function loadDetail(id: number) {
 
 function handleVisibleChange(visible: boolean) {
   emits('update:visible', visible)
-  if (!visible) report.value = undefined
+  if (!visible) reportList.value = []
 }
 
-function formatDisplayValue(field: keyof QhseMonthReportItem) {
-  const value = report.value?.[field]
+function getCompanyKey(index: number) {
+  return `company_${index}`
+}
+
+function formatMetricValue(value: QhseMonthReportItem[keyof QhseMonthReportItem]) {
   if (value === undefined || value === null || value === '') return '-'
-  if (field === 'createTime') {
-    const date = dayjs(value)
-    return date.isValid() ? date.format('YYYY-MM-DD') : String(value)
+  const numericValue = Number(value)
+  if (!Number.isNaN(numericValue) && String(value).trim() !== '') {
+    return Number.isInteger(numericValue) ? String(numericValue) : numericValue.toFixed(2)
   }
   return String(value)
 }
 
 function getMetricCompanyValue(field: keyof QhseMonthReportItem, companyKey: string) {
-  const rowData = mockMetricValueMap.value[String(field)] || {}
-  const value = rowData[companyKey]
-  if (value === undefined || value === null || value === '') return '-'
-  if (typeof value === 'number' && !Number.isInteger(value)) return value.toFixed(2)
-  return String(value)
+  const rowData = companyRowMap.value[companyKey]
+  return formatMetricValue(rowData?.[field])
 }
 
 function getMetricSummaryValue(field: keyof QhseMonthReportItem) {
-  const rowData = mockMetricValueMap.value[String(field)] || {}
-  const values = companyColumns
-    .map((company) => rowData[company.key])
+  const values = reportList.value
+    .map((item) => item[field])
     .filter((value) => value !== undefined && value !== null && value !== '')
 
   if (!values.length) return '-'
-  if (values.every((value) => typeof value === 'number')) {
-    const total = values.reduce((sum, value) => sum + Number(value), 0)
+
+  if (field === 'remark') {
+    return values.map((value) => String(value)).join(';')
+  }
+
+  const numericValues = values.map((value) => Number(value))
+  if (numericValues.every((value) => !Number.isNaN(value))) {
+    const total = numericValues.reduce((sum, value) => sum + value, 0)
     return Number.isInteger(total) ? String(total) : total.toFixed(2)
   }
+
   return values.map((value) => String(value)).join(';')
 }
 
@@ -259,11 +228,24 @@ function tableSpanMethod({
   return { rowspan: categoryRowSpanMap.value[row.category], colspan: 1 }
 }
 
+const previewMeta = computed(() => {
+  const firstItem = reportList.value[0]
+  if (!firstItem) return { personName: '-', createTime: '-' }
+
+  const date = dayjs(firstItem.createTime)
+  return {
+    personName: firstItem.personName || '-',
+    createTime: date.isValid() ? date.format('YYYY-MM-DD') : '-'
+  }
+})
+
 watch(
-  () => [props.visible, props.id] as const,
-  ([visible, id]) => {
-    if (!visible || !id) return
-    loadDetail(id)
+  () => [props.visible, props.yearMonths] as const,
+  async ([visible, yearMonths], previous = [false, undefined] as const) => {
+    const [previousVisible, previousYearMonths] = previous
+    if (!visible || !yearMonths) return
+    if (visible === previousVisible && yearMonths === previousYearMonths) return
+    await loadDetail()
   },
   { immediate: true }
 )
@@ -281,9 +263,9 @@ watch(
       <div class="qhse-report-preview__sheet">
         <div class="qhse-report-preview__sheet-title">QHSE 月度报告</div>
         <div class="qhse-report-preview__meta">
-          <span>年月:{{ formatDisplayValue('yearMonths') }}</span>
-          <span>填报人:{{ formatDisplayValue('personName') }}</span>
-          <span>创建日期:{{ formatDisplayValue('createTime') }}</span>
+          <span>年月:{{ props.yearMonths || '-' }}</span>
+          <!-- <span>填报人:{{ previewMeta.personName }}</span>
+          <span>创建日期:{{ previewMeta.createTime }}</span> -->
         </div>
 
         <div class="qhse-report-preview__table-wrap">
@@ -298,9 +280,9 @@ watch(
               prop="category"
               label="基本信息"
               fixed="left"
-              width="140"
+              width="170"
               align="center" />
-            <el-table-column prop="label" label="单位" fixed="left" width="220" align="center" />
+            <el-table-column prop="label" label="单位" fixed="left" width="240" align="center" />
             <el-table-column
               v-for="company in companyColumns"
               :key="company.key"

+ 2 - 2
src/views/pms/qhse/safety/index.vue

@@ -332,7 +332,7 @@
       <el-form-item label="照片" prop="hazardFile">
         <UploadFile
           v-model="formData.hazardFile"
-          :file-type="['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'jpeg']"
+          :file-type="['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'jpeg', 'bmp']"
           :limit="3"
           :file-size="100"
           class="min-w-80px" />
@@ -378,7 +378,7 @@
       <el-form-item label="整改附件" prop="rectifyFile">
         <UploadFile
           v-model="formData.rectifyFile"
-          :file-type="['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'jpeg']"
+          :file-type="['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'jpeg', 'bmp']"
           :limit="3"
           :file-size="100"
           class="min-w-80px" />