|
|
@@ -0,0 +1,306 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import { QhseMonthReportApi } from '@/api/pms/qhse'
|
|
|
+import type { QhseMonthReportItem, ReportMetricRow } from './types'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+
|
|
|
+interface Props {
|
|
|
+ visible: boolean
|
|
|
+ id?: number
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps<Props>()
|
|
|
+const emits = defineEmits(['update:visible'])
|
|
|
+
|
|
|
+const loading = ref(false)
|
|
|
+const report = ref<QhseMonthReportItem>()
|
|
|
+
|
|
|
+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
|
|
|
+ }, {})
|
|
|
+})
|
|
|
+
|
|
|
+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)
|
|
|
+}
|
|
|
+
|
|
|
+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__toolbar">
|
|
|
+ <div class="qhse-report-preview__heading">
|
|
|
+ <h2>{{ report?.title || 'QHSE月报汇总' }}</h2>
|
|
|
+ <p>按月报模板预览,包含横向列头与纵向分类行头</p>
|
|
|
+ </div>
|
|
|
+ <el-button @click="handleVisibleChange(false)">关闭</el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <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">
|
|
|
+ <table class="qhse-report-preview__table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th class="is-sticky-col">基本信息</th>
|
|
|
+ <th>单位</th>
|
|
|
+ <th>瑞恒兴域</th>
|
|
|
+ <th>四川瑞都</th>
|
|
|
+ <th>陕西瑞鹰</th>
|
|
|
+ <th>俄油服</th>
|
|
|
+ <th>瑞气能源</th>
|
|
|
+ <th>瑞霖技术</th>
|
|
|
+ <th>北京总部</th>
|
|
|
+ <th>汇总</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr v-for="(row, index) in metricRows" :key="`${row.category}-${row.field}`">
|
|
|
+ <th
|
|
|
+ v-if="firstRowIndexByCategory[row.category] === index"
|
|
|
+ class="is-sticky-col is-category"
|
|
|
+ :rowspan="categoryRowSpanMap[row.category]">
|
|
|
+ {{ row.category }}
|
|
|
+ </th>
|
|
|
+ <th class="is-label">{{ row.label }}</th>
|
|
|
+ <td>{{ row.unit }}</td>
|
|
|
+ <td class="is-value">{{ formatDisplayValue(row.field) }}</td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </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__toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__heading {
|
|
|
+ h2 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 26px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: #1f2a44;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 8px 0 0;
|
|
|
+ color: #5f6b85;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__sheet {
|
|
|
+ display: flex;
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 16px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #d8e0ef;
|
|
|
+ border-radius: 20px;
|
|
|
+ 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 {
|
|
|
+ overflow: auto;
|
|
|
+ border: 1px solid #cfd8e6;
|
|
|
+}
|
|
|
+
|
|
|
+.qhse-report-preview__table {
|
|
|
+ width: 100%;
|
|
|
+ min-width: 820px;
|
|
|
+ border-collapse: collapse;
|
|
|
+ table-layout: fixed;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #26334d;
|
|
|
+
|
|
|
+ th,
|
|
|
+ td {
|
|
|
+ border: 1px solid #cfd8e6;
|
|
|
+ padding: 12px 14px;
|
|
|
+ text-align: center;
|
|
|
+ vertical-align: middle;
|
|
|
+ background: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ thead th {
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ z-index: 3;
|
|
|
+ background: #dbe8ff;
|
|
|
+ color: #183153;
|
|
|
+ font-weight: 700;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.is-sticky-col {
|
|
|
+ position: sticky;
|
|
|
+ left: 0;
|
|
|
+ z-index: 2;
|
|
|
+}
|
|
|
+
|
|
|
+.is-category {
|
|
|
+ background: #edf3ff !important;
|
|
|
+ font-weight: 700;
|
|
|
+ min-width: 140px;
|
|
|
+}
|
|
|
+
|
|
|
+.is-label {
|
|
|
+ background: #f8fbff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.is-value {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #0f3f8f;
|
|
|
+}
|
|
|
+
|
|
|
+@media (width < 768px) {
|
|
|
+ .qhse-report-preview {
|
|
|
+ padding: 12px;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .qhse-report-preview__toolbar {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ }
|
|
|
+
|
|
|
+ .qhse-report-preview__sheet {
|
|
|
+ padding: 14px;
|
|
|
+ border-radius: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .qhse-report-preview__sheet-title {
|
|
|
+ font-size: 22px;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|