|
|
@@ -0,0 +1,444 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import { QhseMonthReportApi } from '@/api/pms/qhse'
|
|
|
+import type {
|
|
|
+ QhseMonthReportItem,
|
|
|
+ ReportCompanyColumn,
|
|
|
+ ReportMetricRow,
|
|
|
+ ReportMetricValue
|
|
|
+} from './types'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { computed, ref, watch } from 'vue'
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ visible: boolean
|
|
|
+ id?: number
|
|
|
+}
|
|
|
+
|
|
|
+interface TableRow {
|
|
|
+ category: string
|
|
|
+ label: string
|
|
|
+ field: keyof QhseMonthReportItem
|
|
|
+ summary: string
|
|
|
+ [key: string]: string | keyof QhseMonthReportItem
|
|
|
+}
|
|
|
+
|
|
|
+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 metricRows: ReportMetricRow[] = [
|
|
|
+ { category: '人工时与里程', label: '员工人数', field: 'employee', unit: '人' },
|
|
|
+ { category: '人工时与里程', label: '分包商人数', field: 'subcontractors', unit: '人' },
|
|
|
+ {
|
|
|
+ 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: '/' }
|
|
|
+]
|
|
|
+
|
|
|
+const categoryRowSpanMap = computed(() => {
|
|
|
+ return 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) => {
|
|
|
+ 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 companyValues = Object.fromEntries(
|
|
|
+ companyColumns.map((company) => [company.key, getMetricCompanyValue(row.field, company.key)])
|
|
|
+ )
|
|
|
+
|
|
|
+ return {
|
|
|
+ category: row.category,
|
|
|
+ label: row.label,
|
|
|
+ field: row.field,
|
|
|
+ summary: getMetricSummaryValue(row.field),
|
|
|
+ ...companyValues
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+async function loadDetail(id: number) {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const res = await QhseMonthReportApi.getQhseMonthReport(id)
|
|
|
+ report.value = ((res as any)?.data ?? res ?? {}) as QhseMonthReportItem
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleVisibleChange(visible: boolean) {
|
|
|
+ emits('update:visible', visible)
|
|
|
+ if (!visible) report.value = undefined
|
|
|
+}
|
|
|
+
|
|
|
+function formatDisplayValue(field: keyof QhseMonthReportItem) {
|
|
|
+ const value = report.value?.[field]
|
|
|
+ if (value === undefined || value === null || value === '') return '-'
|
|
|
+ if (field === 'createTime') {
|
|
|
+ const date = dayjs(value)
|
|
|
+ return date.isValid() ? date.format('YYYY-MM-DD') : String(value)
|
|
|
+ }
|
|
|
+ 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)
|
|
|
+}
|
|
|
+
|
|
|
+function getMetricSummaryValue(field: keyof QhseMonthReportItem) {
|
|
|
+ const rowData = mockMetricValueMap.value[String(field)] || {}
|
|
|
+ const values = companyColumns
|
|
|
+ .map((company) => rowData[company.key])
|
|
|
+ .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)
|
|
|
+ return Number.isInteger(total) ? String(total) : total.toFixed(2)
|
|
|
+ }
|
|
|
+ return values.map((value) => String(value)).join(';')
|
|
|
+}
|
|
|
+
|
|
|
+function tableSpanMethod({
|
|
|
+ row,
|
|
|
+ column,
|
|
|
+ rowIndex
|
|
|
+}: {
|
|
|
+ row: TableRow
|
|
|
+ column: { property?: string }
|
|
|
+ rowIndex: number
|
|
|
+}) {
|
|
|
+ if (column.property !== 'category') return { rowspan: 1, colspan: 1 }
|
|
|
+ const firstIndex = firstRowIndexByCategory.value[row.category]
|
|
|
+ if (firstIndex !== rowIndex) return { rowspan: 0, colspan: 0 }
|
|
|
+ return { rowspan: categoryRowSpanMap.value[row.category], colspan: 1 }
|
|
|
+}
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => [props.visible, props.id] as const,
|
|
|
+ ([visible, id]) => {
|
|
|
+ if (!visible || !id) return
|
|
|
+ loadDetail(id)
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <el-drawer
|
|
|
+ :model-value="visible"
|
|
|
+ :size="'100%'"
|
|
|
+ :with-header="false"
|
|
|
+ destroy-on-close
|
|
|
+ body-class="qhse-report-preview-drawer__body"
|
|
|
+ @update:model-value="handleVisibleChange">
|
|
|
+ <div class="qhse-report-preview" v-loading="loading">
|
|
|
+ <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>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="qhse-report-preview__table-wrap">
|
|
|
+ <el-table
|
|
|
+ :data="tableRows"
|
|
|
+ :span-method="tableSpanMethod"
|
|
|
+ border
|
|
|
+ stripe
|
|
|
+ height="60vh"
|
|
|
+ class="qhse-report-preview__el-table">
|
|
|
+ <el-table-column
|
|
|
+ prop="category"
|
|
|
+ label="基本信息"
|
|
|
+ fixed="left"
|
|
|
+ width="140"
|
|
|
+ align="center" />
|
|
|
+ <el-table-column prop="label" label="单位" fixed="left" width="220" align="center" />
|
|
|
+ <el-table-column
|
|
|
+ v-for="company in companyColumns"
|
|
|
+ :key="company.key"
|
|
|
+ :prop="company.key"
|
|
|
+ :label="company.label"
|
|
|
+ min-width="140"
|
|
|
+ align="center" />
|
|
|
+ <el-table-column
|
|
|
+ prop="summary"
|
|
|
+ label="汇总"
|
|
|
+ min-width="180"
|
|
|
+ align="center"
|
|
|
+ fixed="right" />
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="qhse-report-preview__footer">
|
|
|
+ <el-button size="large" type="primary" @click="handleVisibleChange(false)">关闭</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.qhse-report-preview {
|
|
|
+ display: flex;
|
|
|
+ min-height: 100%;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+ background: linear-gradient(180deg, #eef4ff 0%, #f7f9fc 220px, #eef2f7 100%);
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__sheet {
|
|
|
+ display: flex;
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #d8e0ef;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 18px 50px rgb(35 51 84 / 10%);
|
|
|
+ padding: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__sheet-title {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: 700;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ color: #203354;
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__meta {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 12px 24px;
|
|
|
+ color: #52627f;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__table-wrap {
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__footer {
|
|
|
+ position: sticky;
|
|
|
+ bottom: 0;
|
|
|
+ z-index: 20;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: auto;
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 10px;
|
|
|
+ background: rgb(255 255 255 / 90%);
|
|
|
+ backdrop-filter: blur(8px);
|
|
|
+}
|
|
|
+
|
|
|
+@media (width < 768px) {
|
|
|
+ .qhse-report-preview {
|
|
|
+ padding: 12px;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .qhse-report-preview__sheet {
|
|
|
+ padding: 14px;
|
|
|
+ border-radius: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .qhse-report-preview__sheet-title {
|
|
|
+ font-size: 22px;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview-drawer__body) {
|
|
|
+ height: 100%;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview__el-table) {
|
|
|
+ --el-table-header-bg-color: #dbe8ff;
|
|
|
+ --el-table-header-text-color: #183153;
|
|
|
+ --el-table-row-hover-bg-color: #f8fbff;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview__el-table .el-table__cell) {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview__el-table th.el-table__cell) {
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview__el-table .cell) {
|
|
|
+ padding: 12px 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview__el-table .el-table__body-wrapper td:last-child) {
|
|
|
+ background: #eef4ff;
|
|
|
+ color: #14346b;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview__el-table .el-table__body-wrapper td:nth-child(2)) {
|
|
|
+ background: #f8fbff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.qhse-report-preview__el-table .el-table__body-wrapper td:nth-child(n + 3):not(:last-child)) {
|
|
|
+ color: #0f3f8f;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+</style>
|