Ver código fonte

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

yanghao 1 dia atrás
pai
commit
ca47a3933a

+ 1 - 0
package.json

@@ -59,6 +59,7 @@
     "dayjs": "^1.11.10",
     "diagram-js": "^12.8.0",
     "dingtalk-jsapi": "^3.1.0",
+    "docx-preview": "^0.3.7",
     "driver.js": "^1.3.1",
     "echarts": "^5.6.0",
     "echarts-wordcloud": "^2.1.0",

+ 77 - 0
pnpm-lock.yaml

@@ -110,6 +110,9 @@ importers:
       dingtalk-jsapi:
         specifier: ^3.1.0
         version: 3.2.2
+      docx-preview:
+        specifier: ^0.3.7
+        version: 0.3.7
       driver.js:
         specifier: ^1.3.1
         version: 1.3.1
@@ -2892,6 +2895,9 @@ packages:
   core-js@3.39.0:
     resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
 
+  core-util-is@1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+
   cosmiconfig-typescript-loader@5.1.0:
     resolution: {integrity: sha512-7PtBB+6FdsOvZyJtlF3hEPpACq7RQX6BVGsgC7/lfVXnKMvNCu/XY3ykreqG5w/rBNdu2z8LCIKoF3kpHHdHlA==}
     engines: {node: '>=v16'}
@@ -3212,6 +3218,9 @@ packages:
     resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
     engines: {node: '>=6.0.0'}
 
+  docx-preview@0.3.7:
+    resolution: {integrity: sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==}
+
   dom-align@1.12.4:
     resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==}
 
@@ -3784,6 +3793,9 @@ packages:
     resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
     engines: {node: '>= 4'}
 
+  immediate@3.0.6:
+    resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+
   immer@9.0.21:
     resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
 
@@ -3900,6 +3912,9 @@ packages:
   is-url@1.2.4:
     resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
 
+  isarray@1.0.0:
+    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+
   isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
@@ -3994,6 +4009,9 @@ packages:
     resolution: {integrity: sha512-idqReg23J0PVRAADmZMc5xQM3xeOX5bTB6OTyMnzq33IXJXmn9iJuWIEvGmrN80rQf4d7uLTMEDwpzujNcI0Rg==}
     hasBin: true
 
+  jszip@3.10.1:
+    resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
+
   katex@0.16.11:
     resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
     hasBin: true
@@ -4024,6 +4042,9 @@ packages:
   lezer-feel@1.9.0:
     resolution: {integrity: sha512-x8z6pCih3I3BOq3kBbhw6VUOU9Sg61PBJ1nigTgDl1yM5f0OPzEjK7GRJXutrSJDiUK8zwgqBvUJFlSfGLNZUg==}
 
+  lie@3.3.0:
+    resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
+
   lilconfig@3.1.2:
     resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
     engines: {node: '>=14'}
@@ -4471,6 +4492,9 @@ packages:
   package-manager-detector@0.2.5:
     resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==}
 
+  pako@1.0.11:
+    resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
+
   parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
@@ -4734,6 +4758,9 @@ packages:
   react-is@18.3.1:
     resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
 
+  readable-stream@2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+
   readable-stream@3.6.2:
     resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
     engines: {node: '>= 6'}
@@ -4854,6 +4881,9 @@ packages:
   rw@1.3.3:
     resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
 
+  safe-buffer@5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+
   safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
 
@@ -4899,6 +4929,9 @@ packages:
     resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
     engines: {node: '>= 0.4'}
 
+  setimmediate@1.0.5:
+    resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
+
   shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -5005,6 +5038,9 @@ packages:
     resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
     engines: {node: '>=18'}
 
+  string_decoder@1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+
   string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
 
@@ -8612,6 +8648,8 @@ snapshots:
 
   core-js@3.39.0: {}
 
+  core-util-is@1.0.3: {}
+
   cosmiconfig-typescript-loader@5.1.0(@types/node@20.17.9)(cosmiconfig@9.0.0(typescript@5.3.3))(typescript@5.3.3):
     dependencies:
       '@types/node': 20.17.9
@@ -8950,6 +8988,10 @@ snapshots:
     dependencies:
       esutils: 2.0.3
 
+  docx-preview@0.3.7:
+    dependencies:
+      jszip: 3.10.1
+
   dom-align@1.12.4: {}
 
   dom-serializer@2.0.0:
@@ -9590,6 +9632,8 @@ snapshots:
 
   ignore@6.0.2: {}
 
+  immediate@3.0.6: {}
+
   immer@9.0.21: {}
 
   immutable@5.0.3: {}
@@ -9670,6 +9714,8 @@ snapshots:
 
   is-url@1.2.4: {}
 
+  isarray@1.0.0: {}
+
   isexe@2.0.0: {}
 
   jackspeak@3.4.3:
@@ -9754,6 +9800,13 @@ snapshots:
 
   jsonrepair@3.1.0: {}
 
+  jszip@3.10.1:
+    dependencies:
+      lie: 3.3.0
+      pako: 1.0.11
+      readable-stream: 2.3.8
+      setimmediate: 1.0.5
+
   katex@0.16.11:
     dependencies:
       commander: 8.3.0
@@ -9783,6 +9836,10 @@ snapshots:
       '@lezer/lr': 1.4.6
       min-dash: 4.2.3
 
+  lie@3.3.0:
+    dependencies:
+      immediate: 3.0.6
+
   lilconfig@3.1.2: {}
 
   lines-and-columns@1.2.4: {}
@@ -10256,6 +10313,8 @@ snapshots:
 
   package-manager-detector@0.2.5: {}
 
+  pako@1.0.11: {}
+
   parent-module@1.0.1:
     dependencies:
       callsites: 3.1.0
@@ -10463,6 +10522,16 @@ snapshots:
 
   react-is@18.3.1: {}
 
+  readable-stream@2.3.8:
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+
   readable-stream@3.6.2:
     dependencies:
       inherits: 2.0.4
@@ -10600,6 +10669,8 @@ snapshots:
 
   rw@1.3.3: {}
 
+  safe-buffer@5.1.2: {}
+
   safe-buffer@5.2.1: {}
 
   safe-json-parse@4.0.0:
@@ -10643,6 +10714,8 @@ snapshots:
       gopd: 1.0.1
       has-property-descriptors: 1.0.2
 
+  setimmediate@1.0.5: {}
+
   shebang-command@2.0.0:
     dependencies:
       shebang-regex: 3.0.0
@@ -10745,6 +10818,10 @@ snapshots:
       get-east-asian-width: 1.3.0
       strip-ansi: 7.1.0
 
+  string_decoder@1.1.1:
+    dependencies:
+      safe-buffer: 5.1.2
+
   string_decoder@1.3.0:
     dependencies:
       safe-buffer: 5.2.1

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

@@ -341,6 +341,14 @@ export const IotSocSummaryApi = {
   // 获取详情
   getIotSocSummary: async (id) => {
     return await request.get({ url: `/rq/iot-soc-summary/get?id=` + id })
+  },
+  // SOC卡预览
+  previewIotSocSummary: async (id) => {
+    return await request.download({ url: `/rq/iot-soc-summary/safety-card/preview?id=${id}` })
+  },
+  // 下载SOC卡
+  downloadIotSocSummary: async (id) => {
+    return await request.download({ url: `/rq/iot-soc-summary/safety-card/download/${id}` })
   }
 }
 
@@ -371,3 +379,31 @@ export const QHSEJsaApi = {
     return await request.get({ url: `/rq/qhse-jsa/get?id=` + id })
   }
 }
+
+// PTW 管理
+export const QHSEPtwApi = {
+  // 获得PTW管理分页
+  getPtwList: async (params) => {
+    return await request.get({ url: `/rq/qhse-ptw/page`, params })
+  },
+  // 删除PTW管理
+  deletePtw: async (id) => {
+    return await request.delete({ url: `/rq/qhse-ptw/delete?id=` + id })
+  },
+  // 添加PTW管理
+  createPtw: async (data) => {
+    return await request.post({ url: `/rq/qhse-ptw/create`, data })
+  },
+  // 获取详情
+  getPtw: async (id) => {
+    return await request.get({ url: `/rq/qhse-ptw/get?id=` + id })
+  },
+  // 修改PTW管理
+  updatePtw: async (data) => {
+    return await request.put({ url: `/rq/qhse-ptw/update`, data })
+  },
+  // 导出PTW管理 Excel
+  exportPtw: async (params) => {
+    return await request.download({ url: `/rq/qhse-ptw/export-excel`, params })
+  }
+}

+ 3 - 1
src/utils/dict.ts

@@ -322,7 +322,9 @@ export enum DICT_TYPE {
   ORG_CERT = 'org_cert',
   DANGER_GRADE = 'danger_grade',
   ACCIDENT_REPORT_STATUS = 'accident_report_status',
-  QHSE_HAZARD_STATUS = 'qhse_hazard_status'
+  QHSE_HAZARD_STATUS = 'qhse_hazard_status',
+  QHSE_PTW_TYPE = 'qhse_ptw_type',
+  QHSE_PTW_GRADE = 'qhse_ptw_grade'
 }
 
 export function realValue(type: any, value: string) {

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

@@ -74,6 +74,7 @@
           </el-table-column>
 
           <el-table-column label="JSA序号" align="center" prop="jsaXh" />
+          <el-table-column label="编号" align="center" prop="jsaNo" />
 
           <el-table-column label="日期" align="center" prop="jsaTime">
             <template #default="{ row }">

+ 612 - 0
src/views/pms/qhse/ptw/index.vue

@@ -0,0 +1,612 @@
+<template>
+  <el-row :gutter="20">
+    <!-- 左侧部门树 -->
+    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
+    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
+      <ContentWrap>
+        <!-- 搜索工作栏 -->
+        <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
+          <el-form-item label="地址" prop="address">
+            <el-input placeholder="请输入地址" v-model="queryParams.address" />
+          </el-form-item>
+
+          <el-form-item label="状态" prop="status">
+            <el-select
+              v-model="queryParams.status"
+              placeholder="请选择状态"
+              clearable
+              style="width: 180px"
+            >
+              <el-option
+                v-for="dict in getStrDictOptions(DICT_TYPE.QHSE_HAZARD_STATUS)"
+                :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>
+      </ContentWrap>
+
+      <!-- 列表 -->
+      <ContentWrap class="flex-1 overflow-hidden mt-15px">
+        <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          height="calc(85vh - 130px)"
+          :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>
+
+          <el-table-column label="PTW编号" align="center" prop="ptwNo" width="100" />
+          <el-table-column label="PTW序号" align="center" prop="ptwXh" width="100" />
+
+          <el-table-column label="时间" align="center" show-overflow-tooltip>
+            <template #default="{ row }">
+              {{ formatDate(row.ptwTime) }}
+            </template>
+          </el-table-column>
+
+          <el-table-column label="作业票类型" align="center">
+            <template #default="{ row }">
+              <dict-tag :type="DICT_TYPE.QHSE_PTW_TYPE" :value="row.ptwType" />
+            </template>
+          </el-table-column>
+
+          <el-table-column label="作业分级" align="center">
+            <template #default="{ row }">
+              <dict-tag :type="DICT_TYPE.QHSE_PTW_GRADE" :value="row.ptwGrade" />
+            </template>
+          </el-table-column>
+
+          <el-table-column
+            label="作业地点"
+            align="center"
+            prop="workLocation"
+            show-overflow-tooltip
+          />
+          <el-table-column
+            label="作业内容"
+            align="center"
+            prop="workContent"
+            show-overflow-tooltip
+          />
+
+          <el-table-column
+            label="作业人员"
+            align="center"
+            prop="workPerson"
+            show-overflow-tooltip
+          />
+
+          <el-table-column label="监护人" align="center" prop="guardian" show-overflow-tooltip />
+          <el-table-column
+            label="作业负责人"
+            align="center"
+            prop="workDuty"
+            show-overflow-tooltip
+          />
+          <el-table-column label="部门" align="center" prop="deptName" show-overflow-tooltip />
+
+          <el-table-column label="附件" align="center">
+            <template #default="{ row }">
+              <el-link
+                v-if="row.file"
+                :underline="false"
+                type="primary"
+                size="small"
+                @click="viewFile(row.file)"
+                >查看</el-link
+              >
+              <span v-else>-</span>
+            </template>
+          </el-table-column>
+
+          <el-table-column prop="createTime" label="创建时间" align="center" min-width="150">
+            <template #default="{ row }">
+              {{ formatDate(row.createTime) }}
+            </template>
+          </el-table-column>
+
+          <el-table-column
+            label="备注"
+            align="center"
+            prop="remark"
+            :show-overflow-tooltip="true"
+          />
+
+          <el-table-column
+            :label="t('devicePerson.operation')"
+            align="center"
+            fixed="right"
+            min-width="150px"
+          >
+            <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>
+            </template>
+          </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <Pagination
+          :total="total"
+          v-model:page="queryParams.pageNo"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+  <!-- 新增/编辑证书对话框 -->
+  <el-dialog
+    :title="dialogTitle"
+    v-model="dialogVisible"
+    width="600px"
+    destroy-on-close
+    @close="closeDialog"
+  >
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="120px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="序号" prop="ptwXh">
+        <el-input v-model="formData.ptwXh" placeholder="请输入序号" />
+      </el-form-item>
+
+      <el-form-item label="作业内容" prop="workContent">
+        <el-input
+          type="textarea"
+          :rows="2"
+          v-model="formData.workContent"
+          placeholder="请输入作业内容"
+        />
+      </el-form-item>
+
+      <el-form-item label="作业分级" prop="ptwGrade">
+        <el-select v-model="formData.ptwGrade" placeholder="请选择作业分级">
+          <el-option
+            v-for="item in getDictOptions(DICT_TYPE.QHSE_PTW_GRADE)"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <!-- 作业票类型 -->
+      <el-form-item label="作业票类型" prop="ptwType">
+        <el-select v-model="formData.ptwType" placeholder="请选择作业票类型">
+          <el-option
+            v-for="item in getDictOptions(DICT_TYPE.QHSE_PTW_TYPE)"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="作业地点" prop="ptwXh">
+        <el-input v-model="formData.workLocation" placeholder="请输入作业地点" />
+      </el-form-item>
+
+      <el-form-item label="作业人员" prop="workPerson">
+        <el-input v-model="formData.workPerson" placeholder="请输入作业人员" />
+      </el-form-item>
+
+      <el-form-item label="监护人" prop="guardian">
+        <el-input v-model="formData.guardian" placeholder="请输入监护人" />
+      </el-form-item>
+
+      <el-form-item label="作业负责人" prop="workDuty">
+        <el-input v-model="formData.workDuty" placeholder="请输入作业负责人" />
+      </el-form-item>
+
+      <el-form-item label="附件" prop="file">
+        <UploadFile
+          v-model="formData.file"
+          :file-type="['doc', 'docx', 'xls', 'xlsx', 'pdf', 'jpg', 'png', 'jpeg']"
+          :limit="3"
+          :file-size="100"
+          class="min-w-80px"
+        />
+      </el-form-item>
+
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          type="textarea"
+          v-model="formData.remark"
+          :rows="2"
+          placeholder="请输入备注"
+          style="width: 100%"
+        />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="closeDialog">取 消</el-button>
+      <el-button type="primary" @click="submitForm" :loading="submitLoading">确 定</el-button>
+    </template>
+  </el-dialog>
+
+  <el-dialog v-model="dialogFileView" title="附件" width="500">
+    <div
+      v-for="(file, index) in fileList"
+      :key="index"
+      class="flex items-center justify-between mt-5"
+    >
+      <span class="file-name-text">{{ extractFileName(file) }}</span>
+      <div>
+        <el-button link type="primary" @click="viewFileInfo(file)">
+          <Icon icon="ep:view" class="mr-2px" />查看</el-button
+        >
+        <el-button link type="primary" @click="handleDownload(file)">
+          <Icon icon="ep:download" class="mr-2px" />下载</el-button
+        >
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer mt-10">
+        <el-button type="primary" @click="dialogFileView = false"> 确认 </el-button>
+      </div>
+    </template>
+  </el-dialog>
+
+  <FilePreviewDialog
+    v-model="filePreviewVisible"
+    :title="filePreviewTitle"
+    :urls="filePreviewUrls"
+  />
+</template>
+
+<script setup lang="ts">
+import { QHSEPtwApi } from '@/api/pms/qhse/index'
+import DeptTree from '@/views/system/user/DeptTree2.vue'
+import { handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import { ElMessageBox, ElMessage } from 'element-plus'
+const deptList2 = ref<Tree[]>([]) // 树形结构
+import { formatDate } from '@/utils/formatTime'
+
+import UploadFile from '@/components/UploadFile/src/UploadFile.vue'
+import FilePreviewDialog from '@/components/FilePreview/src/FilePreviewDialog.vue'
+import { DICT_TYPE, getStrDictOptions, getDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'IotQHSEPTW' })
+
+const loading = ref(true) // 列表的加载中
+const formLoading = ref(false) // 表单加载中
+const submitLoading = ref(false) // 提交按钮加载中
+const exportLoading = ref(false) // 导出按钮加载中
+const isLeftContentCollapsed = ref(false)
+
+const { t } = useI18n()
+
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: '',
+  address: ''
+})
+const queryFormRef = ref(null) // 搜索的表单
+
+// 对话框相关
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const isEdit = ref(false)
+
+// 文件预览
+const filePreviewVisible = ref(false)
+const filePreviewTitle = ref('文件预览')
+const filePreviewUrls = ref<string[]>([])
+
+// 表单相关
+const formRef = ref()
+const formData = ref({
+  deptId: '',
+  workDuty: '',
+  workPerson: '',
+  guardian: '',
+  workLocation: '',
+  workContent: '',
+  ptwXh: '',
+  ptwGrade: '',
+  ptwType: '',
+  file: '',
+  remark: ''
+})
+
+// 表单验证规则
+const formRules = {
+  deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
+  workDuty: [{ required: true, message: '作业负责人不能为空', trigger: 'blur' }],
+  workPerson: [{ required: true, message: '作业人员不能为空', trigger: 'blur' }],
+  guardian: [{ required: true, message: '监护人不能为空', trigger: 'blur' }],
+  workLocation: [{ required: true, message: '作业地点不能为空', trigger: 'blur' }],
+  workContent: [{ required: true, message: '作业内容不能为空', trigger: 'blur' }],
+  ptwXh: [{ required: true, message: '作业票编号不能为空', trigger: 'blur' }],
+  ptwGrade: [{ required: true, message: '作业票等级不能为空', trigger: 'blur' }],
+  ptwType: [{ required: true, message: '作业票类型不能为空', trigger: 'blur' }],
+  file: [{ required: true, message: '附件不能为空', trigger: 'blur' }]
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await QHSEPtwApi.getPtwList(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+const downloadFile = (response) => {
+  // 创建 blob 对象
+  const blob = new Blob([response], {
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
+  })
+
+  // 获取文件名
+  let fileName = 'PTW.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 QHSEPtwApi.exportPtw(queryParams)
+    downloadFile(response)
+    exportLoading.value = false
+  } catch (error) {
+    ElMessage.error('导出失败,请重试')
+    console.error('导出错误:', error)
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 首页处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  await getList()
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.deptId = ''
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+// 显示新增对话框
+const handleAdd = () => {
+  isEdit.value = false
+  dialogTitle.value = '新增'
+  resetForm()
+  dialogVisible.value = true
+}
+
+// 显示编辑对话框
+const handleEdit = (row) => {
+  isEdit.value = true
+  dialogTitle.value = '修改'
+
+  formData.value = {
+    ...row,
+    // 确保日期字段正确处理
+    issueDate: row.issueDate ? ensureMillisecondTimestamp(row.issueDate) : null,
+    validityPeriod: row.validityPeriod ? ensureMillisecondTimestamp(row.validityPeriod) : null
+  }
+
+  dialogVisible.value = true
+}
+
+// 确保时间戳是毫秒级的
+const ensureMillisecondTimestamp = (timestamp) => {
+  let time = Number(timestamp)
+  if (time < 10000000000) {
+    // 秒级时间戳转为毫秒级
+    return time * 1000
+  }
+  return time
+}
+
+//删除证书
+const handleDelete = async (id: number) => {
+  ElMessageBox.confirm('确定要删除该条记录吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      try {
+        await QHSEPtwApi.deletePtw(id)
+        ElMessage.success('删除成功')
+        getList()
+      } catch (error) {
+        console.error(error)
+      }
+    })
+    .catch(() => {
+      // 取消操作
+    })
+}
+
+// 重置表单
+const resetForm = () => {
+  formData.value = {
+    deptId: '',
+    workDuty: '',
+    workPerson: '',
+    guardian: '',
+    workLocation: '',
+    workContent: '',
+    ptwXh: '',
+    ptwGrade: '',
+    ptwType: '',
+    file: '',
+    remark: ''
+  }
+  formRef.value?.clearValidate()
+}
+
+// 关闭对话框
+const closeDialog = () => {
+  dialogVisible.value = false
+  resetForm()
+}
+
+// 提交表单
+const submitForm = async () => {
+  if (!formRef.value) return
+
+  try {
+    await formRef.value.validate()
+    submitLoading.value = true
+
+    // 准备提交数据
+    const submitData = {
+      ...formData.value
+    }
+
+    if (isEdit.value) {
+      // 编辑
+      await QHSEPtwApi.updatePtw(submitData)
+      ElMessage.success('编辑成功')
+    } else {
+      // 新增
+      await QHSEPtwApi.createPtw(submitData)
+      ElMessage.success('新增成功')
+    }
+
+    dialogVisible.value = false
+    getList()
+  } catch (error) {
+    console.error(error)
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+// 下载文件函数
+const handleDownload = async (url) => {
+  try {
+    const response = await fetch(url)
+    const blob = await response.blob()
+    const downloadUrl = window.URL.createObjectURL(blob)
+
+    const link = document.createElement('a')
+    link.href = downloadUrl
+    link.download = url.split('/').pop() // 自动获取文件名‌:ml-citation{ref="3" data="citationList"}
+    link.click()
+
+    URL.revokeObjectURL(downloadUrl)
+  } catch (error) {
+    console.error('下载失败:', error)
+  }
+}
+
+let dialogFileView = ref(false)
+let fileList = ref([])
+const viewFile = (file) => {
+  fileList.value = file.split(',')
+  dialogFileView.value = true
+  // window.open(file)
+}
+
+const viewFileInfo = (file) => {
+  window.open(
+    'http://doc.deepoil.cc:8012/onlinePreview?url=' + encodeURIComponent(Base64.encode(file))
+  )
+}
+
+const extractFileName = (url: string): string => {
+  try {
+    // 移除查询参数和哈希
+    const cleanUrl = url.split('?')[0].split('#')[0]
+    // 获取最后一个斜杠后的内容
+    const parts = cleanUrl.split('/')
+    const fileName = parts[parts.length - 1]
+    // URL 解码
+    return decodeURIComponent(fileName) || url
+  } catch {
+    // 如果解析失败,返回原始 URL
+    return url
+  }
+}
+
+onMounted(async () => {
+  getList()
+  deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+</script>
+
+<style scoped>
+::deep(.el-tree--highlight-current) {
+  height: 200px !important;
+}
+::deep(.el-transfer-panel__body) {
+  height: 700px !important;
+}
+.image-preview {
+  margin-top: 10px;
+  display: flex;
+  justify-content: center;
+}
+</style>

+ 1 - 6
src/views/pms/qhse/socData/SOCClassifyForm.vue

@@ -36,12 +36,7 @@
         </el-select>
       </el-form-item>
       <el-form-item label="备注" prop="remark">
-        <el-input
-          v-model="formData.remark"
-          maxlength="11"
-          placeholder="请输入备注"
-          type="textarea"
-        />
+        <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
       </el-form-item>
     </el-form>
     <template #footer>

+ 60 - 4
src/views/pms/qhse/socSummary/IotSocSummaryForm.vue

@@ -39,6 +39,7 @@
           check-strictly
           node-key="id"
           filterable
+          multiple
           placeholder="请选择soc类型"
         />
       </el-form-item>
@@ -80,7 +81,8 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   project: undefined,
   observationDate: undefined,
-  socClass: undefined,
+
+  socClass: [],
   userName: undefined,
   post: undefined,
   deptId: undefined,
@@ -88,6 +90,7 @@ const formData = ref({
 })
 const formRules = reactive({
   observationDate: [{ required: true, message: '观察日期不能为空', trigger: 'blur' }],
+
   socClass: [{ required: true, message: 'soc类型不能为空', trigger: 'blur' }],
   className: [{ required: true, message: '类型名称不能为空', trigger: 'blur' }],
   deptId: [{ required: true, message: '队伍不能为空', trigger: 'change' }],
@@ -102,11 +105,20 @@ const open = async (type: string, id?: number) => {
   dialogTitle.value = t('action.' + type)
   formType.value = type
   resetForm()
+  if (!socList.value.length) {
+    const data = await IotSocApi.getSocList({})
+    socList.value = handleTree(data)
+  }
   // 修改时,设置数据
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await IotSocSummaryApi.getIotSocSummary(id)
+      const data = await IotSocSummaryApi.getIotSocSummary(id)
+      formData.value = {
+        ...data,
+
+        socClass: normalizeSocClasses(data.socClass, socList.value)
+      }
     } finally {
       formLoading.value = false
     }
@@ -122,7 +134,12 @@ const submitForm = async () => {
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value
+    const data = {
+      ...formData.value,
+
+      socClasses: formData.value.socClass,
+      socClass: undefined
+    }
     if (formType.value === 'create') {
       await IotSocSummaryApi.createIotSocSummary(data)
       message.success(t('common.createSuccess'))
@@ -151,7 +168,8 @@ const resetForm = () => {
     id: undefined,
     project: undefined,
     observationDate: undefined,
-    socClass: undefined,
+
+    socClass: [],
     className: undefined,
     userName: undefined,
     post: undefined,
@@ -162,6 +180,44 @@ const resetForm = () => {
   formRef.value?.resetFields()
 }
 
+const normalizeSocClasses = (value: unknown, tree: Tree[] = []): (string | number)[] => {
+  const values = Array.isArray(value)
+    ? value
+    : typeof value === 'string'
+      ? value
+          .split(',')
+          .map((item) => item.trim())
+          .filter(Boolean)
+      : value === null || value === undefined || value === ''
+        ? []
+        : [value as string | number]
+
+  const idMap = new Map<string, string | number>()
+  const stack = [...tree]
+  while (stack.length) {
+    const node = stack.shift() as any
+    if (!node) continue
+    idMap.set(String(node.id), node.id)
+    if (Array.isArray(node.children) && node.children.length) {
+      stack.push(...node.children)
+    }
+  }
+
+  return values.map((item) => idMap.get(String(item)) ?? item)
+}
+
+const serializeSocClasses = (value: unknown): string => {
+  if (Array.isArray(value)) {
+    return value
+      .map((item) => String(item).trim())
+      .filter(Boolean)
+      .join(',')
+  }
+  if (typeof value === 'string') return value
+  if (value === null || value === undefined) return ''
+  return String(value)
+}
+
 onMounted(async () => {
   deptList2.value = handleTree(await DeptApi.getSimpleDeptList())
 

+ 166 - 5
src/views/pms/qhse/socSummary/index.vue

@@ -76,10 +76,11 @@
             align="center"
             prop="observationDate"
             :formatter="dateFormatter"
+            类型名称
             width="180px"
           />
 
-          <el-table-column label="类型名称" align="center" prop="className" />
+          <el-table-column label="类型名称" align="center" prop="className" min-width="120" />
           <el-table-column label="姓名" align="center" prop="userName" />
           <el-table-column label="岗位" align="center" prop="post" />
 
@@ -92,8 +93,10 @@
             width="180px"
           />
 
-          <el-table-column label="操作" align="center" min-width="120px">
+          <el-table-column label="操作" align="center" min-width="180px" fixed="right">
             <template #default="scope">
+              <!-- <el-button link type="primary" @click="view(scope.row.id)"> 查看 </el-button> -->
+              <el-button link type="primary" @click="downloadSOC(scope.row)"> 下载 </el-button>
               <el-button
                 link
                 type="primary"
@@ -120,6 +123,29 @@
           v-model:limit="queryParams.pageSize"
           @pagination="getList"
         />
+
+        <!-- <div id="docx-viewer" class="docx-viewer-container"></div> -->
+        <teleport to="body">
+          <transition name="dialog-fade">
+            <div
+              v-if="dialogVisible"
+              class="custom-dialog-overlay"
+              @click.self="dialogVisible = false"
+            >
+              <div class="custom-dialog">
+                <div class="custom-dialog-header">
+                  <span class="custom-dialog-title">文档预览</span>
+                  <el-icon class="custom-dialog-close" @click="dialogVisible = false">
+                    <Close />
+                  </el-icon>
+                </div>
+                <div class="custom-dialog-body">
+                  <div id="docx-viewer" class="docx-viewer-container"></div>
+                </div>
+              </div>
+            </div>
+          </transition>
+        </teleport>
       </ContentWrap>
     </el-col>
   </el-row>
@@ -129,18 +155,20 @@
 </template>
 
 <script setup lang="ts">
-import { dateFormatter } from '@/utils/formatTime'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotSocSummaryApi } from '@/api/pms/qhse/index'
 import IotSocSummaryForm from './IotSocSummaryForm.vue'
 import DeptTree from '@/views/system/user/DeptTree2.vue'
+import { renderAsync } from 'docx-preview'
+import { Close } from '@element-plus/icons-vue'
 
 /** SOC卡汇总 列表 */
 defineOptions({ name: 'IotSocSummary' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-
+const dialogVisible = ref(false) // 文档预览弹框显示状态
 const loading = ref(true) // 列表的加载中
 const list = ref([]) // 列表的数据
 const total = ref(0) // 列表的总页数
@@ -219,15 +247,148 @@ const handleExport = async () => {
     // 发起导出
     exportLoading.value = true
     const data = await IotSocSummaryApi.exportIotSocSummary(queryParams)
-    download.excel(data, 'SOC卡汇总.xls')
+    download.excel(data, 'SOC卡汇总.xlsx')
   } catch {
   } finally {
     exportLoading.value = false
   }
 }
 
+const view = async (id) => {
+  dialogVisible.value = true
+  // 等待弹框渲染完成后再加载文档
+
+  // 等待弹框渲染完成后再加载文档
+  await nextTick()
+  const container = document.getElementById('docx-viewer')
+  if (container) {
+    container.innerHTML = '' // 清空之前的内容
+    const res = await IotSocSummaryApi.previewIotSocSummary(id)
+
+    await renderAsync(res, container)
+  }
+}
+
+const downloadSOC = async (row) => {
+  const res = await IotSocSummaryApi.downloadIotSocSummary(row.id)
+  download.excel(
+    res,
+    `行为安全观察与沟通卡_${formatDate(row.observationDate).substring(0, 10)}_${row.userName}.docx`
+  )
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()
 })
 </script>
+
+<style scoped lang="scss">
+.custom-dialog-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 2000;
+}
+
+.custom-dialog {
+  background: #fff;
+  border-radius: 8px;
+  width: 60%;
+  max-width: 1200px;
+  height: 90vh;
+  overflow-y: auto;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.custom-dialog-header {
+  padding: 16px 20px;
+  border-bottom: 1px solid #e4e7ed;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #f5f7fa;
+  border-radius: 8px 8px 0 0;
+}
+
+.custom-dialog-title {
+  font-size: 16px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.custom-dialog-close {
+  font-size: 20px;
+  cursor: pointer;
+  color: #909399;
+  transition: color 0.3s;
+
+  &:hover {
+    color: #409eff;
+  }
+}
+
+.custom-dialog-body {
+  flex: 1;
+  overflow: hidden;
+  padding: 20px;
+  background: transparent;
+  min-height: 400px;
+}
+
+.docx-viewer-container {
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+  background: transparent !important;
+
+  :deep(.docx-wrapper) {
+    background: transparent !important;
+    padding: 0 !important;
+  }
+
+  :deep(.docx-wrapper > section.docx) {
+    background: #fff !important;
+    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+    margin: 0 auto;
+    padding: 40px;
+  }
+}
+
+.custom-dialog-footer {
+  padding: 12px 20px;
+  border-top: 1px solid #e4e7ed;
+  text-align: right;
+  background: #f5f7fa;
+  border-radius: 0 0 8px 8px;
+}
+
+// 动画效果
+.dialog-fade-enter-active,
+.dialog-fade-leave-active {
+  transition: opacity 0.3s ease;
+}
+
+.dialog-fade-enter-from,
+.dialog-fade-leave-to {
+  opacity: 0;
+}
+
+.dialog-fade-enter-active .custom-dialog,
+.dialog-fade-leave-active .custom-dialog {
+  transition: transform 0.3s ease;
+}
+
+.dialog-fade-enter-from .custom-dialog,
+.dialog-fade-leave-to .custom-dialog {
+  transform: scale(0.9);
+}
+</style>