فهرست منبع

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

yanghao 3 روز پیش
والد
کامیت
64bf5750cd

+ 6 - 2
src/api/pms/qhse/index.ts

@@ -454,8 +454,12 @@ export const QHSEPtwApi = {
 }
 
 export const kanbanApi = {
-  getKanban: async (params) => {
-    return await request.get({ url: `/rq/qhse-kanban/get`, params })
+  getKanban: async () => {
+    return await request.get({ url: `/rq/qhse/stat/number` })
+  },
+  // 安全生产天数
+  getSafeDay: async (id) => {
+    return await request.get({ url: `rq/qhse-safe-day/get/dept/company?deptId=${id}` })
   }
 }
 

+ 13 - 2
src/views/pms/qhse/certPerson/CertPersonForm.vue

@@ -6,6 +6,17 @@
       :rules="formRules"
       label-width="100px"
       v-loading="formLoading">
+      <el-form-item label="所属队伍" prop="deptId">
+        <el-tree-select
+          clearable
+          v-model="formData.deptId"
+          :data="deptList2"
+          :props="defaultProps"
+          :check-strictly="false"
+          node-key="id"
+          filterable
+          placeholder="请选择所在部门" />
+      </el-form-item>
       <el-form-item label="姓名" prop="nickname">
         <el-input v-model="formData.nickname" placeholder="请输入姓名" />
       </el-form-item>
@@ -30,7 +41,7 @@
 </template>
 <script setup lang="ts">
 import { CertPersonApi } from '@/api/pms/qhse/index'
-import { handleTree } from '@/utils/tree'
+import { handleTree, defaultProps } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import { useUserStore } from '@/store/modules/user'
 import { ElMessageBox } from 'element-plus'
@@ -58,7 +69,7 @@ const formData = ref({
 })
 const formRules = reactive({
   nickname: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
-
+  deptId: [{ required: true, message: '请选择所属队伍', trigger: 'blur' }],
   mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
   postName: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }]
 })

+ 1 - 1
src/views/pms/qhse/jsa/IotSocSummaryForm.vue

@@ -12,7 +12,7 @@
           v-model="formData.deptId"
           :data="deptList2"
           :props="defaultProps"
-          check-strictly
+          :check-strictly="false"
           node-key="id"
           filterable
           placeholder="请选择所在部门" />

+ 259 - 133
src/views/pms/qhse/kanban/index.vue

@@ -1,5 +1,10 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
+import { kanbanApi } from '@/api/pms/qhse/index'
+import { IotDangerApi } from '@/api/pms/qhse/index'
+import { useUserStore } from '@/store/modules/user'
+
+const userStore = useUserStore()
 import {
   AlarmClock,
   Checked,
@@ -27,21 +32,6 @@ defineOptions({
   name: 'PmsQhseKanban'
 })
 
-type SummaryCard = {
-  title: string
-  value: string
-  note: string
-  accent: string
-  glow: string
-  icon: any
-}
-
-type MetricBar = {
-  label: string
-  value: number
-  color: string
-}
-
 type RiskZone = {
   title: string
   desc: string
@@ -62,8 +52,11 @@ type BottomCard = {
   lines: string[]
 }
 
+type SafeDayMap = Record<string, number>
+
 const wrapperRef = ref<HTMLDivElement>()
 const hazardChartRef = ref<HTMLDivElement>()
+const safeDayChartRef = ref<HTMLDivElement>()
 const socChartRef = ref<HTMLDivElement>()
 const scale = ref(1)
 const supportsZoom = ref(false)
@@ -71,30 +64,31 @@ const supportsZoom = ref(false)
 let resizeObserver: ResizeObserver | null = null
 let resizeRaf = 0
 let hazardChart: echarts.ECharts | null = null
+let safeDayChart: echarts.ECharts | null = null
 let socChart: echarts.ECharts | null = null
 
 const pageTitle = 'QHSE管理看板'
 
-const summaryCards: SummaryCard[] = [
+const summaryCards = ref([
   {
     title: '风险总数(处)',
-    value: '126',
-    note: 'R:12',
+    value: 0,
+    note: '',
     accent: '#ff5a62',
     glow: 'rgba(255, 90, 98, 0.26)',
     icon: Warning
   },
   {
     title: '本月隐患(条)',
-    value: '29',
-    note: 'Exp:2',
+    value: 0,
+    note: '',
     accent: '#ff9f2f',
     glow: 'rgba(255, 159, 47, 0.24)',
     icon: Opportunity
   },
   {
     title: '隐患整改率',
-    value: '92.3%',
+    value: '0%',
     note: '',
     accent: '#2ac7c9',
     glow: 'rgba(42, 199, 201, 0.26)',
@@ -102,7 +96,7 @@ const summaryCards: SummaryCard[] = [
   },
   {
     title: '本月作业许可',
-    value: '11',
+    value: 0,
     note: '',
     accent: '#4f8dff',
     glow: 'rgba(79, 141, 255, 0.22)',
@@ -110,31 +104,26 @@ const summaryCards: SummaryCard[] = [
   },
   {
     title: '人员持证率',
-    value: '97.8%',
+    value: 0,
     note: 'Warn:3',
     accent: '#f2b800',
     glow: 'rgba(242, 184, 0, 0.22)',
     icon: Postcard
   }
-]
+])
 
-const hazardBars: MetricBar[] = [
-  { label: '总数', value: 657, color: '#4f8dff' },
-  { label: '已整改', value: 628, color: '#43c7ca' },
-  { label: '未整改', value: 29, color: '#ff981f' }
-]
+const hazardBars = ref([
+  { label: '总数', value: 0, color: '#4f8dff' },
+  { label: '已整改', value: 0, color: '#43c7ca' },
+  { label: '未整改', value: 0, color: '#ff981f' }
+])
 
-const incidentStats = [
-  { label: '安全事故', value: '0起', accent: '#2ac7c9' },
-  { label: '安全生产天数', value: '3起', accent: '#f2c11a' }
-]
-
-const riskZones: RiskZone[] = [
-  { title: '高危风险区', desc: '危化库 / 试压区 / 配电房', color: '#ff4c49' },
-  { title: '中风险区', desc: '焊接 / 机加 / 吊装区', color: '#ff981f' },
-  { title: '低风险区', desc: '物料库 / 维修 / 装卸区', color: '#f2c11a' },
-  { title: '安全控制区', desc: '办公区 / 展厅 / 主通道', color: '#5794ff' }
-]
+const riskZones = ref([
+  { title: '', desc: '办公区 / 展厅 / 主通道', color: '#25b36a', value: 0 },
+  { title: '', desc: '物料库 / 维修 / 装卸区', color: '#3d7cff', value: 0 },
+  { title: '', desc: '焊接 / 机加 / 吊装区', color: '#ff9827', value: 0 },
+  { title: '', desc: '危化库 / 试压区 / 配电房', color: '#ff5b61', value: 0 }
+])
 
 const permitStats: PermitStat[] = [
   { label: '个人防护', value: 18, color: '#4f8dff' },
@@ -222,6 +211,17 @@ function updateScale() {
   })
 }
 
+const staticData = ref({})
+async function getStatic() {
+  const res = await IotDangerApi.getDangerStatistics(userStore.user.deptId)
+  staticData.value = res.classify
+
+  riskZones.value.forEach((zone, index: number) => {
+    zone.value = staticData.value[index].count
+    zone.title = `${staticData.value[index].classify}区`
+  })
+}
+
 onMounted(() => {
   supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
   nextTick(updateScale)
@@ -230,9 +230,11 @@ onMounted(() => {
     resizeObserver.observe(wrapperRef.value)
   }
   initHazardChart()
+  initSafeDayChart()
   initSocChart()
   window.addEventListener('resize', updateScale)
   window.addEventListener('resize', resizeHazardChart)
+  window.addEventListener('resize', resizeSafeDayChart)
   window.addEventListener('resize', resizeSocChart)
 })
 
@@ -240,9 +242,11 @@ onUnmounted(() => {
   resizeObserver?.disconnect()
   window.removeEventListener('resize', updateScale)
   window.removeEventListener('resize', resizeHazardChart)
+  window.removeEventListener('resize', resizeSafeDayChart)
   window.removeEventListener('resize', resizeSocChart)
   cancelAnimationFrame(resizeRaf)
   destroyHazardChart()
+  destroySafeDayChart()
   destroySocChart()
 })
 
@@ -271,7 +275,7 @@ function getHazardChartOption(): echarts.EChartsOption {
     }),
     xAxis: {
       type: 'category',
-      data: hazardBars.map((item) => item.label),
+      data: hazardBars.value.map((item) => item.label),
       axisLine: {
         show: false
       },
@@ -309,7 +313,7 @@ function getHazardChartOption(): echarts.EChartsOption {
     series: [
       {
         type: 'bar',
-        data: hazardBars.map((item) => ({
+        data: hazardBars.value.map((item) => ({
           value: item.value,
           itemStyle: {
             color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@@ -318,7 +322,7 @@ function getHazardChartOption(): echarts.EChartsOption {
             ]),
             shadowBlur: 14,
             shadowColor: item.color,
-            borderRadius: [0, 0, 0, 0]
+            borderRadius: [5, 5, 0, 0]
           }
         })),
         barWidth: 70,
@@ -359,6 +363,11 @@ function initHazardChart() {
   hazardChart.setOption(getHazardChartOption(), true)
 }
 
+function updateHazardChart() {
+  if (!hazardChart) return
+  hazardChart.setOption(getHazardChartOption(), true)
+}
+
 function resizeHazardChart() {
   hazardChart?.resize()
 }
@@ -370,6 +379,133 @@ function destroyHazardChart() {
   }
 }
 
+function getSafeDayEntries() {
+  const safeDayMap = (safeDay.value ?? {}) as SafeDayMap
+
+  return Object.entries(safeDayMap)
+    .map(([label, value]) => ({
+      label,
+      value: Number(value) || 0
+    }))
+    .sort((a, b) => a.value - b.value)
+}
+
+function getSafeDayChartOption(): echarts.EChartsOption {
+  const entries = getSafeDayEntries()
+
+  return {
+    ...ANIMATION,
+    grid: {
+      left: 52,
+      right: 32,
+      top: 12,
+      bottom: 24,
+      containLabel: true
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: 'rgba(31, 91, 184, 0.08)'
+        }
+      },
+      formatter(params: any) {
+        const item = Array.isArray(params) ? params[0] : params
+        return `${item.name}<br/>安全天数:${item.value}`
+      }
+    }),
+    xAxis: {
+      type: 'value',
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#263854',
+        fontSize: 12,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: 'rgba(83, 114, 173, 0.6)',
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: entries.map((item) => item.label),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#16263d',
+        fontSize: 14,
+        fontWeight: 700,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    series: [
+      {
+        type: 'bar',
+        data: entries.map((item) => item.value),
+        barWidth: 16,
+        showBackground: true,
+        backgroundStyle: {
+          color: 'rgba(108, 149, 228, 0.08)',
+          borderRadius: 6
+        },
+        itemStyle: {
+          borderRadius: 6,
+          color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
+            { offset: 0, color: '#78a0ec' },
+            { offset: 1, color: '#6a90dd' }
+          ])
+        },
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 16,
+            shadowColor: 'rgba(106, 144, 221, 0.34)'
+          }
+        }
+      }
+    ]
+  }
+}
+
+function initSafeDayChart() {
+  if (!safeDayChartRef.value) return
+  if (safeDayChart) {
+    safeDayChart.dispose()
+  }
+  safeDayChart = echarts.init(safeDayChartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  safeDayChart.setOption(getSafeDayChartOption(), true)
+}
+
+function updateSafeDayChart() {
+  if (!safeDayChart) return
+  safeDayChart.setOption(getSafeDayChartOption(), true)
+}
+
+function resizeSafeDayChart() {
+  safeDayChart?.resize()
+}
+
+function destroySafeDayChart() {
+  if (safeDayChart) {
+    safeDayChart.dispose()
+    safeDayChart = null
+  }
+}
+
 function getSocChartOption(): echarts.EChartsOption {
   return {
     ...ANIMATION,
@@ -486,6 +622,60 @@ function destroySocChart() {
     socChart = null
   }
 }
+
+const getKanban = async () => {
+  const data = await kanbanApi.getKanban()
+  return data
+}
+
+function formatPercent(numerator: number, denominator: number) {
+  if (!denominator || Number.isNaN(denominator)) {
+    return '0%'
+  }
+
+  return `${((numerator / denominator) * 100).toFixed(1)}%`
+}
+
+const summaryPanel = ref<any>(null)
+const safeDay = ref<SafeDayMap>({})
+onMounted(async () => {
+  summaryPanel.value = await getKanban()
+
+  summaryCards.value[0].value = summaryPanel.value.danger
+  summaryCards.value[1].value = summaryPanel.value.monthHazard
+  summaryCards.value[2].value = formatPercent(
+    summaryPanel.value.totalHazard - summaryPanel.value.todoHazard,
+    summaryPanel.value.totalHazard
+  )
+  summaryCards.value[3].value = summaryPanel.value.ptwCount
+  summaryCards.value[4].value = formatPercent(
+    summaryPanel.value.totdalCert - summaryPanel.value.expiredCert,
+    summaryPanel.value.totdalCert
+  )
+
+  summaryCards.value[4].note = `Warn: ${summaryPanel.value.warnCert}`
+
+  hazardBars.value[0].value = summaryPanel.value.totalHazard
+  hazardBars.value[1].value = summaryPanel.value.totalHazard - summaryPanel.value.todoHazard
+  hazardBars.value[2].value = summaryPanel.value.todoHazard
+
+  try {
+    safeDay.value = (await kanbanApi.getSafeDay(userStore.getUser.deptId)) || {}
+  } catch (error) {
+    console.log(error)
+  }
+
+  try {
+    getStatic()
+  } catch (error) {
+    console.log(error)
+  }
+
+  nextTick(() => {
+    updateHazardChart()
+    updateSafeDayChart()
+  })
+})
 </script>
 
 <template>
@@ -531,38 +721,6 @@ function destroySocChart() {
           </section>
 
           <div class="board-main">
-            <div class="left-column">
-              <section class="panel board-panel kb-stage-card kb-stage-card--2">
-                <div class="panel-title">
-                  <span class="icon-decorator"><span></span><span></span></span>
-                  隐患排查治理统计
-                </div>
-                <div ref="hazardChartRef" class="chart-panel chart-panel--echart"></div>
-              </section>
-
-              <section class="panel board-panel kb-stage-card kb-stage-card--3">
-                <div class="panel-title">
-                  <span class="icon-decorator"><span></span><span></span></span>
-                  事故事件趋势(近12月)
-                </div>
-                <div class="incident-panel">
-                  <div class="incident-graphic">
-                    <div class="incident-graphic__axis"></div>
-                    <div class="incident-graphic__area"></div>
-                    <div class="incident-graphic__line"></div>
-                  </div>
-                  <div class="incident-metrics">
-                    <div v-for="item in incidentStats" :key="item.label" class="incident-metric">
-                      <span :style="{ color: item.label === '安全生产天数' ? '#259745' : '' }"
-                        >{{ item.label }}:</span
-                      >
-                      <strong :style="{ color: item.accent }">{{ item.value }}</strong>
-                    </div>
-                  </div>
-                </div>
-              </section>
-            </div>
-
             <div class="center-column">
               <section class="panel board-panel board-panel--center kb-stage-card kb-stage-card--4">
                 <div class="panel-title panel-title--center">
@@ -574,12 +732,35 @@ function destroySocChart() {
                     <div class="risk-card__title">
                       <span class="risk-card__dot" :style="{ background: zone.color }"></span>
                       <span :style="{ color: zone.color }">{{ zone.title }}</span>
+                      <!-- <span class="risk-card__count">{{ zone.value }}</span> -->
+                      <CountTo
+                        :duration="2600"
+                        :end-val="zone.value"
+                        :start-val="0"
+                        :style="{ color: zone.color, fontSize: '28px' }" />
                     </div>
                     <div class="risk-card__desc">{{ zone.desc }}</div>
                   </article>
                 </div>
               </section>
             </div>
+            <div class="left-column">
+              <section class="panel board-panel kb-stage-card kb-stage-card--2">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  隐患排查治理统计
+                </div>
+                <div ref="hazardChartRef" class="chart-panel chart-panel--echart"></div>
+              </section>
+
+              <section class="panel board-panel kb-stage-card kb-stage-card--3">
+                <div class="panel-title">
+                  <span class="icon-decorator"><span></span><span></span></span>
+                  安全生产天数
+                </div>
+                <div ref="safeDayChartRef" class="safe-day-chart-panel"></div>
+              </section>
+            </div>
 
             <div class="right-column">
               <section class="panel board-panel kb-stage-card kb-stage-card--5">
@@ -745,64 +926,9 @@ function destroySocChart() {
   height: 188px;
 }
 
-.incident-panel {
-  display: grid;
-  margin-top: 50px;
-  grid-template-columns: 180px 1fr;
-  align-items: center;
-  gap: 28px;
-}
-
-.incident-graphic {
-  position: relative;
-  width: 170px;
-  height: 132px;
-}
-
-.incident-graphic__axis {
-  position: absolute;
-  inset: 24px 16px 16px 16px;
-  border-bottom: 6px solid #4f8dff;
-  border-left: 6px solid #4f8dff;
-  border-radius: 2px;
-  opacity: 0.92;
-}
-
-.incident-graphic__area {
-  position: absolute;
-  right: 22px;
-  bottom: 24px;
-  width: 102px;
-  height: 68px;
-  background: linear-gradient(180deg, rgb(79 141 255 / 88%) 0%, rgb(79 141 255 / 28%) 100%);
-  clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 100%);
-}
-
-.incident-graphic__line {
-  position: absolute;
-  right: 22px;
-  bottom: 24px;
-  width: 102px;
-  height: 68px;
-  border-top: 5px solid #4f8dff;
-  clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 5%, 54% 77%, 22% 55%, 0 100%);
-}
-
-.incident-metrics {
-  display: grid;
-  gap: 20px;
-}
-
-.incident-metric {
-  font-size: 24px;
-  font-weight: 700;
-  color: #556b89;
-}
-
-.incident-metric strong {
-  margin-left: 4px;
-  font-size: 34px;
-  line-height: 1;
+.safe-day-chart-panel {
+  height: 218px;
+  // margin-top: 18px;
 }
 
 .panel-title--center {

+ 1 - 1
src/views/pms/qhse/ptw/index.vue

@@ -176,7 +176,7 @@
           v-model="formData.deptId"
           :data="deptList2"
           :props="defaultProps"
-          check-strictly
+          :check-strictly="false"
           node-key="id"
           filterable
           placeholder="请选择所在部门" />

+ 10 - 15
src/views/pms/qhse/socSummary/IotSocSummaryForm.vue

@@ -5,8 +5,7 @@
       :model="formData"
       :rules="formRules"
       label-width="100px"
-      v-loading="formLoading"
-    >
+      v-loading="formLoading">
       <el-form-item label="队伍" prop="deptId">
         <el-tree-select
           clearable
@@ -16,9 +15,7 @@
           :check-strictly="false"
           node-key="id"
           filterable
-          placeholder="请选择所在队伍"
-          @node-click="handleNodeClick"
-        />
+          placeholder="请选择所在队伍" />
       </el-form-item>
 
       <el-form-item label="观察日期" prop="observationDate">
@@ -27,8 +24,7 @@
           type="date"
           value-format="x"
           placeholder="选择观察日期"
-          style="width: 100%"
-        />
+          style="width: 100%" />
       </el-form-item>
       <el-form-item label="soc类型" prop="socClass">
         <el-tree-select
@@ -40,8 +36,7 @@
           node-key="id"
           filterable
           multiple
-          placeholder="请选择soc类型"
-        />
+          placeholder="请选择soc类型" />
       </el-form-item>
 
       <el-form-item label="姓名" prop="userName">
@@ -155,12 +150,12 @@ const submitForm = async () => {
   }
 }
 
-const handleNodeClick = (data: Tree) => {
-  if (data.type !== '3') {
-    ElMessage.warning('只能选择队伍')
-    return
-  }
-}
+// const handleNodeClick = (data: Tree) => {
+//   if (data.type !== '3') {
+//     ElMessage.warning('只能选择队伍')
+//     return
+//   }
+// }
 
 /** 重置表单 */
 const resetForm = () => {