Przeglądaj źródła

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

yanghao 2 tygodni temu
rodzic
commit
9e33d70367

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

@@ -568,6 +568,11 @@ export const QhseMonthReportApi = {
   // 导出QHSE月报 Excel
   exportQhseMonthReport: async (params) => {
     return await request.download({ url: `/rq/qhse-month-report/export-excel`, params })
+  },
+
+  // 提交审批
+  submitForApproval: async (id) => {
+    return await request.post({ url: `/rq/qhse-month-report/approving/${id}` })
   }
 }
 
@@ -584,6 +589,29 @@ export const QhseMonthReportSummaryApi = {
   }
 }
 
+// 月报审核
+export const QhseMonthReportAuditApi = {
+  // 获得QHSE月报审核分页
+  getQhseMonthReportAuditList: async (params) => {
+    return await request.get({ url: `/rq/qhse-month-report/page`, params })
+  },
+
+  // 获得QHSE月报审核详情
+  getQhseMonthReportAudit: async (id) => {
+    return await request.get({ url: `/rq/qhse-month-report/get?id=` + id })
+  },
+
+  // 审核通过
+  auditPass: async (id) => {
+    return await request.post({ url: `/rq/qhse-month-report/approval/${id}` })
+  },
+
+  // 审核不通过
+  auditNotPass: async (data) => {
+    return await request.post({ url: `/rq/qhse-month-report/reject`, data })
+  }
+}
+
 // 应检设备证书
 export const InspectDeviceCertApi = {
   // 获得设备证书分页 rq/qhse-device-cert/create

+ 4 - 1
src/utils/dict.ts

@@ -341,7 +341,10 @@ export enum DICT_TYPE {
   ACCIDENT_CASE_GRADE = 'qhse_case_grade',
 
   // 行业标准
-  QHSE_LAW_TYPE = 'qhse_law_type'
+  QHSE_LAW_TYPE = 'qhse_law_type',
+
+  // 审批状态
+  QHSE_AUDIT_STATUS = 'qhse_month_report_status'
 }
 
 export function realValue(type: any, value: string) {

+ 4 - 4
src/views/pms/qhse/credential/index.vue

@@ -174,9 +174,9 @@
                 </template>
               </zm-table-column>
 
-              <zm-table-column label="到期提醒" align="center">
+              <!-- <zm-table-column label="到期提醒" align="center">
                 <template #default="scope"> {{ scope.row.noticeBefore }}天前提醒 </template>
-              </zm-table-column>
+              </zm-table-column> -->
 
               <zm-table-column label="备注" align="center" prop="remark" />
               <zm-table-column
@@ -294,14 +294,14 @@
           style="width: 100%" />
       </el-form-item>
 
-      <el-form-item label="到期前提醒" prop="noticeBefore">
+      <!-- <el-form-item label="到期前提醒" prop="noticeBefore">
         <el-input-number
           v-model="formData.noticeBefore"
           :min="0"
           :max="365"
           placeholder="请输入提前多少天提醒"
           style="width: 100%" />
-      </el-form-item>
+      </el-form-item> -->
 
       <el-form-item label="备注" prop="remark">
         <el-input

+ 3 - 3
src/views/pms/qhse/monthlyReport/MonthlyReportAdd.vue

@@ -449,7 +449,7 @@
       <!-- 操作按钮 -->
       <div class="form-actions">
         <el-button @click="handleCancel">取消</el-button>
-        <el-button type="primary" @click="handleSubmit" :loading="submitLoading"> 提交 </el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitLoading"> 保存 </el-button>
       </div>
     </el-form>
   </div>
@@ -571,12 +571,12 @@ const handleSubmit = async () => {
   submitLoading.value = true
   try {
     await QhseMonthReportApi.updateQhseMonthReport(reportInfo)
-    message.success('提交成功')
+    message.success('保存成功')
     // 返回列表页或上一页
     delView(unref(router.currentRoute))
     router.push({ name: 'QhseMonthlyReport', params: {} })
   } catch (error) {
-    console.error('提交失败:', error)
+    console.error('保存失败:', error)
   } finally {
     submitLoading.value = false
   }

+ 660 - 0
src/views/pms/qhse/reportReview/index.vue

@@ -0,0 +1,660 @@
+<template>
+  <el-row :gutter="20">
+    <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
+    <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
+      <ContentWrap style="border: 0">
+        <!-- 搜索工作栏 -->
+        <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
+          <el-form-item label="月报标题" prop="title">
+            <el-input v-model="queryParams.title" placeholder="请输入月报标题" />
+          </el-form-item>
+
+          <el-form-item label="创建日期" prop="createTime">
+            <el-date-picker
+              v-model="queryParams.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-200px" />
+          </el-form-item>
+
+          <el-form-item>
+            <el-button @click="handleQuery" v-hasPermi="['rq:qhse-month-report:query']"
+              ><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button
+            >
+            <el-button @click="resetQuery"
+              ><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button
+            >
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+
+      <ContentWrap style="border: 0">
+        <zm-table
+          :data="tableData"
+          border
+          style="width: 100%"
+          :header-cell-style="{ background: '#f5f7fa', color: '#333' }"
+          :cell-style="{ padding: '12px 8px' }"
+          height="70.8vh">
+          <zm-table-column
+            prop="title"
+            label="月报标题"
+            align="center"
+            fixed="left"
+            show-overflow-tooltip />
+          <zm-table-column prop="personName" label="工单填报人" align="center" />
+          <zm-table-column prop="auditStatus" label="审批状态" align="center">
+            <template #default="{ row }">
+              <dict-tag :type="DICT_TYPE.QHSE_AUDIT_STATUS" :value="row.status" />
+            </template>
+          </zm-table-column>
+          <zm-table-column prop="rejectDesc" label="拒绝原因" align="center" />
+          <zm-table-column label="创建日期" prop="createTime" align="center">
+            <template #default="{ row }">
+              {{ formatDate(row.createTime) }}
+            </template>
+          </zm-table-column>
+
+          <zm-table-column label="操作" width="150" align="center" fixed="right" action>
+            <template #default="{ row }">
+              <el-button v-if="canReview(row.status)" text type="primary" @click="openReview(row)">
+                <Icon icon="ep:view" class="mr-5px" />审批
+              </el-button>
+            </template>
+          </zm-table-column>
+        </zm-table>
+
+        <!-- 分页 -->
+        <div class="mt-2 flex justify-right">
+          <el-pagination
+            v-model:current-page="pagination.pageNo"
+            v-model:page-size="pagination.pageSize"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="handleSizeChange"
+            @current-change="handleCurrentChange"
+            background />
+        </div>
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+  <el-drawer v-model="reviewVisible" title="月报审核" direction="rtl" size="760px" destroy-on-close>
+    <div v-loading="detailLoading" class="review-drawer">
+      <template v-if="reviewReport">
+        <div class="report-banner">
+          <div class="report-banner__icon"><Icon icon="ep:document-checked" /></div>
+          <div class="report-banner__content">
+            <h2>{{ displayValue(reviewReport.title) }}</h2>
+            <div class="report-banner__meta">
+              <span><Icon icon="ep:calendar" /> {{ displayValue(reviewReport.yearMonths) }}</span>
+            </div>
+          </div>
+          <dict-tag :type="DICT_TYPE.QHSE_AUDIT_STATUS" :value="reviewReport.status" />
+        </div>
+        <div class="report-meta-grid">
+          <div class="meta-item"
+            ><span>所属部门</span
+            ><strong>{{ displayValue(reviewReport.deptName || reviewReport.deptId) }}</strong></div
+          >
+
+          <div class="meta-item"
+            ><span>填报人</span
+            ><strong>{{
+              displayValue(
+                reviewReport.personName || reviewReport.dutyPersonName || reviewReport.dutyPerson
+              )
+            }}</strong></div
+          >
+        </div>
+        <section v-for="section in detailSections" :key="section.title" class="detail-section">
+          <div class="detail-section__heading">
+            <span class="detail-section__mark"><Icon :icon="section.icon" /></span>
+            <div
+              ><h3>{{ section.title }}</h3
+              ><p>{{ section.description }}</p></div
+            >
+          </div>
+          <div class="metric-grid">
+            <div v-for="item in section.items" :key="item.prop" class="metric-item">
+              <span>{{ item.label }}</span
+              ><strong>{{ displayValue(reviewReport[item.prop]) }}</strong
+              ><small>{{ item.unit }}</small>
+            </div>
+          </div>
+        </section>
+        <section class="detail-section remark-section">
+          <div class="detail-section__heading"
+            ><span class="detail-section__mark"><Icon icon="ep:chat-line-square" /></span
+            ><div><h3>备注信息</h3><p>填报人补充说明</p></div></div
+          >
+          <p class="remark-content">{{ displayValue(reviewReport.remark) }}</p>
+        </section>
+      </template>
+    </div>
+    <template #footer>
+      <div class="review-footer">
+        <el-button @click="reviewVisible = false">关闭</el-button>
+        <el-button
+          v-if="canReview(reviewReport?.status)"
+          type="danger"
+          :loading="auditLoading"
+          @click="handleAudit(false)">
+          审批不通过
+        </el-button>
+        <el-button
+          v-if="canReview(reviewReport?.status)"
+          type="success"
+          :loading="auditLoading"
+          @click="handleAudit(true)">
+          审批通过
+        </el-button>
+      </div>
+    </template>
+  </el-drawer>
+  <el-dialog v-model="rejectDialogVisible" title="填写驳回原因" width="520px">
+    <el-form ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="auto">
+      <el-form-item label="驳回原因" prop="rejectDesc">
+        <el-input
+          v-model="rejectForm.rejectDesc"
+          type="textarea"
+          :rows="4"
+          maxlength="200"
+          show-word-limit
+          placeholder="请输入驳回原因" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="rejectDialogVisible = false">取消</el-button>
+      <el-button type="primary" :loading="auditLoading" @click="submitReject">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { QhseMonthReportAuditApi } from '@/api/pms/qhse'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import DeptTree from '@/views/system/user/DeptTree2.vue'
+import { formatDate } from '@/utils/formatTime'
+import MonthlyReport from '@/views/pms/qhse/monthlyReport/MonthlyReport.vue'
+
+defineOptions({ name: 'QhseMonthlyReportReiview' })
+
+// 查询参数
+const queryParams = reactive({
+  createTime: null,
+  title: '',
+  deptId: ''
+})
+
+let queryFormRef = ref(null)
+
+// 表格数据
+const tableData = ref([])
+const isLeftContentCollapsed = ref(false)
+
+const reviewVisible = ref(false)
+const detailLoading = ref(false)
+const auditLoading = ref(false)
+const reviewReport = ref(null)
+const reviewId = ref(null)
+const rejectDialogVisible = ref(false)
+const rejectFormRef = ref(null)
+const rejectForm = reactive({
+  rejectDesc: ''
+})
+const rejectRules = {
+  rejectDesc: [{ required: true, message: '请输入驳回原因', trigger: 'blur' }]
+}
+
+const canReview = (status) => status === 'approving'
+
+const detailSections = [
+  {
+    title: '工时与安全行驶',
+    description: '人员投入及安全行驶数据',
+    icon: 'ep:user',
+    items: [
+      { label: '员工人数', prop: 'employee', unit: '人' },
+      { label: '分包商人数', prop: 'subcontractors', unit: '人' },
+      { label: '安全行驶里程', prop: 'drivingMileage', unit: '公里' },
+      { label: '总人工时数', prop: 'totalManHours', unit: '小时' }
+    ]
+  },
+  {
+    title: 'QHSE 被动性指标',
+    description: '事故、事件及保命规则统计',
+    icon: 'ep:warning',
+    items: [
+      { label: '无事故累计天数', prop: 'withoutAccident', unit: '天' },
+      { label: '死亡事故', prop: 'fatality', unit: '起' },
+      { label: '损失工时事故', prop: 'injury', unit: '起' },
+      { label: '工作受限事件', prop: 'restrictedCase', unit: '起' },
+      { label: '医疗处理事件', prop: 'medicalCase', unit: '起' },
+      { label: '急救箱事件', prop: 'firstAidCase', unit: '起' },
+      { label: '交通事故', prop: 'vehicleAccident', unit: '起' },
+      { label: '未遂事件', prop: 'nearMiss', unit: '起' },
+      { label: '泄漏事件', prop: 'spill', unit: '起' },
+      { label: '违反保命规则', prop: 'lifeSavingRules', unit: '次' }
+    ]
+  },
+  {
+    title: 'QHSE 主动性指标',
+    description: '会议、检查、培训及现场活动',
+    icon: 'ep:checked',
+    items: [
+      { label: '班前会', prop: 'toolboxTalk', unit: '次' },
+      { label: '管理委员会会议', prop: 'committeeMeeting', unit: '次' },
+      { label: 'QHSE 月度例会', prop: 'monthlyMeeting', unit: '次' },
+      { label: '公司级隐患排查', prop: 'companyHazard', unit: '次' },
+      { label: 'QHSE 检查', prop: 'qhseInspection', unit: '次' },
+      { label: '安全观察卡', prop: 'socCards', unit: '张' },
+      { label: '工作许可审核', prop: 'ptwAudit', unit: '份' },
+      { label: '工作安全分析', prop: 'jsa', unit: '次' },
+      { label: '演练次数', prop: 'drills', unit: '次' },
+      { label: 'QHSE 培训次数', prop: 'training', unit: '次' },
+      { label: 'QHSE 培训人次', prop: 'participantsTraining', unit: '人次' },
+      { label: 'QHSE 培训学时', prop: 'trainingsHours', unit: '小时' }
+    ]
+  },
+  {
+    title: '环境数据',
+    description: '月度资源消耗统计',
+    icon: 'ep:sunrise',
+    items: [
+      { label: '水消耗', prop: 'waterConsumption', unit: '吨' },
+      { label: '柴油消耗', prop: 'dieselConsumption', unit: '升' },
+      { label: '用电量', prop: 'electricityConsumption', unit: '千瓦·时' },
+      { label: '天然气消耗', prop: 'naturalGasConsumption', unit: '立方米' }
+    ]
+  }
+]
+
+// 搜索
+const handleQuery = () => {
+  pagination.pageNo = 1 // 搜索后回到第一页
+  loadTableData()
+}
+
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  pagination.pageNo = 1
+  loadTableData()
+}
+
+const downloadFile = (response) => {
+  // 创建 blob 对象
+  const blob = new Blob([response], {
+    type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8'
+  })
+
+  // 获取文件名
+  let fileName = '月报.xlsx'
+  const disposition = response.headers ? response.headers['content-disposition'] : ''
+  if (disposition) {
+    const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/
+    const matches = filenameRegex.exec(disposition)
+    if (matches != null && matches[1]) {
+      fileName = matches[1].replace(/['"]/g, '')
+    }
+  }
+
+  // 创建下载链接
+  const url = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = url
+  link.setAttribute('download', fileName)
+
+  // 触发下载
+  document.body.appendChild(link)
+  link.click()
+
+  // 清理
+  document.body.removeChild(link)
+  window.URL.revokeObjectURL(url)
+}
+
+// 重置查询
+const resetQuery = () => {
+  pagination.pageNo = 1 // 重置为第一页
+  queryFormRef.value.resetFields()
+  loadTableData()
+}
+
+// 每页数量变化
+const handleSizeChange = (val) => {
+  pagination.pageSize = val
+  pagination.pageNo = 1 // 重置为第一页
+  loadTableData()
+}
+
+// 当前页变化
+const handleCurrentChange = (val) => {
+  pagination.pageNo = val
+  loadTableData()
+}
+
+const openReview = async (row) => {
+  reviewVisible.value = true
+  reviewId.value = row.id
+  reviewReport.value = null
+  detailLoading.value = true
+  try {
+    const res = await QhseMonthReportAuditApi.getQhseMonthReportAudit(row.id)
+    reviewReport.value = res?.data ?? res ?? {}
+  } catch (error) {
+    ElMessage.error('获取月报详情失败')
+    reviewVisible.value = false
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+const handleAudit = async (passed) => {
+  if (!reviewId.value) return
+  if (!passed) {
+    rejectForm.rejectDesc = ''
+    rejectDialogVisible.value = true
+    return
+  }
+  auditLoading.value = true
+  try {
+    await QhseMonthReportAuditApi.auditPass(reviewId.value)
+    ElMessage.success(passed ? '审批通过' : '审批不通过')
+    reviewVisible.value = false
+    await loadTableData()
+  } catch (error) {
+    ElMessage.error(passed ? '审批通过失败' : '审批不通过失败')
+  } finally {
+    auditLoading.value = false
+  }
+}
+
+const submitReject = async () => {
+  if (!reviewId.value) return
+  await rejectFormRef.value?.validate()
+  auditLoading.value = true
+  try {
+    await QhseMonthReportAuditApi.auditNotPass({
+      id: reviewId.value,
+      rejectDesc: rejectForm.rejectDesc
+    })
+    ElMessage.success('审批不通过')
+    rejectDialogVisible.value = false
+    reviewVisible.value = false
+    await loadTableData()
+  } catch (error) {
+    ElMessage.error('审批不通过失败')
+  } finally {
+    auditLoading.value = false
+  }
+}
+
+const displayValue = (value) =>
+  value === undefined || value === null || value === '' ? '--' : value
+
+// 加载数据
+const exportLoading = ref(false)
+let total = ref(0)
+const pagination = reactive({
+  pageNo: 1,
+  pageSize: 10
+})
+const loadTableData = async () => {
+  try {
+    const params = {
+      pageNo: pagination.pageNo,
+      pageSize: pagination.pageSize,
+      createTime: queryParams.createTime,
+      title: queryParams.title,
+      deptId: queryParams.deptId
+    }
+    const res = await QhseMonthReportAuditApi.getQhseMonthReportAuditList(params)
+    tableData.value = res.list || []
+    total.value = res.total || 0
+  } catch (error) {
+    console.error('加载失败:', error)
+  }
+}
+
+// 页面挂载后加载数据
+onMounted(() => {
+  loadTableData()
+})
+</script>
+
+<style scoped lang="scss">
+::v-deep .el-button {
+  border-radius: 0;
+}
+
+::v-deep .el-select__wrapper {
+  border-radius: 0 !important;
+  height: 26px;
+}
+.hazard-table-container {
+  margin: 20px;
+  margin-top: 10px;
+}
+
+.area-header {
+  font-weight: bold;
+  text-align: center;
+  padding: 12px 0;
+  background-color: #f5f7fa;
+  border-bottom: 1px solid #ddd;
+}
+
+.review-drawer {
+  min-height: calc(100vh - 150px);
+  padding: 2px 4px 24px;
+  background: #f6f8fb;
+}
+
+.report-banner {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 22px;
+  color: #fff;
+  background: linear-gradient(135deg, #183b56, #287271);
+  border-radius: 10px;
+  box-shadow: 0 8px 20px rgba(24, 59, 86, 0.16);
+}
+
+.report-banner__icon {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 48px;
+  height: 48px;
+  font-size: 25px;
+  background: rgba(255, 255, 255, 0.16);
+  border-radius: 8px;
+}
+
+.report-banner__content {
+  flex: 1;
+  min-width: 0;
+}
+
+.report-banner__eyebrow {
+  margin-bottom: 5px;
+  font-size: 10px;
+  letter-spacing: 1px;
+  opacity: 0.72;
+}
+
+.report-banner h2 {
+  margin: 0 0 10px;
+  overflow: hidden;
+  font-size: 20px;
+  font-weight: 600;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.report-banner__meta {
+  display: flex;
+  gap: 18px;
+  font-size: 12px;
+  opacity: 0.88;
+}
+
+.report-banner__meta span {
+  display: inline-flex;
+  align-items: center;
+  gap: 5px;
+}
+
+.report-meta-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 1px;
+  margin: 14px 0;
+  overflow: hidden;
+  background: #e7ebf0;
+  border: 1px solid #e7ebf0;
+  border-radius: 8px;
+}
+
+.meta-item {
+  padding: 15px 16px;
+  background: #fff;
+}
+
+.meta-item span,
+.meta-item strong {
+  display: block;
+}
+
+.meta-item span {
+  margin-bottom: 7px;
+  color: #8a96a3;
+  font-size: 12px;
+}
+
+.meta-item strong {
+  overflow: hidden;
+  color: #263746;
+  font-size: 14px;
+  font-weight: 600;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.detail-section {
+  padding: 18px;
+  margin-bottom: 14px;
+  background: #fff;
+  border: 1px solid #e8edf2;
+  border-radius: 8px;
+}
+
+.detail-section__heading {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin-bottom: 16px;
+}
+
+.detail-section__mark {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  color: #287271;
+  background: #e7f4f1;
+  border-radius: 7px;
+}
+
+.detail-section h3 {
+  margin: 0 0 3px;
+  color: #263746;
+  font-size: 15px;
+}
+
+.detail-section__heading p {
+  margin: 0;
+  color: #98a3ae;
+  font-size: 12px;
+}
+
+.metric-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 10px;
+}
+
+.metric-item {
+  position: relative;
+  padding: 12px 13px;
+  background: #f7f9fb;
+  border-radius: 6px;
+}
+
+.metric-item span,
+.metric-item strong,
+.metric-item small {
+  display: block;
+}
+
+.metric-item span {
+  margin-bottom: 7px;
+  color: #687887;
+  font-size: 12px;
+}
+
+.metric-item strong {
+  color: #213547;
+  font-size: 19px;
+  line-height: 1.2;
+}
+
+.metric-item small {
+  position: absolute;
+  right: 12px;
+  bottom: 12px;
+  color: #98a3ae;
+  font-size: 11px;
+}
+
+.remark-content {
+  min-height: 64px;
+  padding: 13px;
+  margin: 0;
+  color: #5d6b78;
+  font-size: 13px;
+  line-height: 1.7;
+  white-space: pre-wrap;
+  background: #f7f9fb;
+  border-radius: 6px;
+}
+
+.review-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+
+@media screen and (max-width: 600px) {
+  .report-banner {
+    align-items: flex-start;
+    flex-wrap: wrap;
+  }
+
+  .report-banner .el-tag {
+    margin-left: 64px;
+  }
+
+  .report-meta-grid,
+  .metric-grid {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+</style>