yanghao 2 недель назад
Родитель
Сommit
fc11e474be
4 измененных файлов с 668 добавлено и 2 удалено
  1. 1 1
      .env.local
  2. 23 0
      src/api/pms/qhse/index.ts
  3. 4 1
      src/utils/dict.ts
  4. 640 0
      src/views/pms/qhse/reportReview/index.vue

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.86:48080  https://iot.deepoil.cc:5443  http://172.26.0.56:48080  http://192.168.188.198:48080
-VITE_BASE_URL='https://iot.deepoil.cc:5443'
+VITE_BASE_URL='https://aims.deepoil.cc'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

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

@@ -584,6 +584,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.put({ url: `/rq/qhse-month-report/approval/${id}` })
+  },
+
+  // 审核不通过
+  auditNotPass: async (id) => {
+    return await request.put({ url: `/rq/qhse-month-report/reject/${id}` })
+  }
+}
+
 // 应检设备证书
 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) {

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

@@ -0,0 +1,640 @@
+<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-button type="success" plain @click="handleExport" :loading="exportLoading">
+              <Icon icon="ep:download" class="mr-5px" /> 导出
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </ContentWrap>
+
+      <ContentWrap style="border: 0">
+        <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.auditStatus" />
+            </template>
+          </zm-table-column>
+          <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 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.auditStatus" />
+        </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 type="danger" :loading="auditLoading" @click="handleAudit(false)">
+          审批不通过
+        </el-button>
+        <el-button type="success" :loading="auditLoading" @click="handleAudit(true)">
+          审批通过
+        </el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { QhseMonthReportApi, 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 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 handleExport = async () => {
+  try {
+    exportLoading.value = true
+    // 调用导出接口
+    const response = await QhseMonthReportApi.exportQhseMonthReport(queryParams)
+
+    // 下载文件
+    downloadFile(response)
+    exportLoading.value = false
+  } catch (error) {
+    ElMessage.error('导出失败,请重试')
+    console.error('导出错误:', error)
+  } finally {
+  }
+}
+
+// 重置查询
+const resetQuery = () => {
+  pagination.pageNo = 1 // 重置为第一页
+  queryFormRef.value.resetFields()
+  loadTableData()
+}
+
+// 删除确认
+const deleteRow = async (row) => {
+  try {
+    await ElMessageBox.confirm(`确认删除 ${row.elementDescription} 吗?`, '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    await QhseMonthReportApi.deleteQhseMonthReport(row.id)
+    ElMessage.success('删除成功')
+    loadTableData() // 重新加载数据
+  } catch (error) {
+    // 用户取消或删除失败
+    if (error !== 'cancel') {
+      ElMessage.error('删除失败')
+    }
+  }
+}
+
+// 每页数量变化
+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
+  auditLoading.value = true
+  try {
+    if (passed) await QhseMonthReportAuditApi.auditPass(reviewId.value)
+    else await QhseMonthReportAuditApi.auditNotPass(reviewId.value)
+    ElMessage.success(passed ? '审批通过' : '审批不通过')
+    reviewVisible.value = false
+    await loadTableData()
+  } catch (error) {
+    ElMessage.error(passed ? '审批通过失败' : '审批不通过失败')
+  } 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>