Эх сурвалжийг харах

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

yanghao 1 долоо хоног өмнө
parent
commit
d7cafefccc

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

@@ -401,6 +401,11 @@ export const IotSocSummaryApi = {
   // }
   getSocSummaryStatistics: async (params) => {
     return await request.get({ url: `/rq/iot-soc-summary/stat`, params })
+  },
+
+  // 获取统计子类
+  getSocSummaryStatisticsChild: async (params) => {
+    return await request.get({ url: `/rq/iot-soc-summary/stat/child`, params })
   }
 }
 

+ 117 - 68
src/views/pms/qhse/kanban/index.vue

@@ -45,7 +45,6 @@ type SafeDayMap = Record<string, number>
 
 type SummaryTabValue = 'home' | 'certificate'
 type QhseMetricValue = 'ltir' | 'trir' | 'pmva'
-
 const userStore = useUserStore()
 
 const type = ref('day')
@@ -342,10 +341,41 @@ function updateScale() {
 
 const staticData = ref<any>({})
 const safeDay = ref<SafeDayMap>({})
+const safeDayMonths = [
+  '1月',
+  '2月',
+  '3月',
+  '4月',
+  '5月',
+  '6月',
+  '7月',
+  '8月',
+  '9月',
+  '10月',
+  '11月',
+  '12月'
+]
+const safeDayTrendSeries = ref<Array<{ name: string; color: string; values: number[] }>>([])
 const summaryPanel = ref<any>(null)
 const total = ref(0)
 const instrumentExpired = ref(0)
 
+function buildSafeDayTrendSeries(baseData: SafeDayMap) {
+  const palette = ['#4f8dff', '#ff981f', '#52c41a', '#597ef7', '#722ed1', '#f2c94c']
+  const factors = [0.92, 0.95, 0.98, 1, 1.03, 1.05, 1.04, 1.06, 1.08, 1.07, 1.09, 1.1]
+
+  safeDayTrendSeries.value = Object.entries(baseData || {}).map(([name, value], index) => {
+    const baseValue = Number(value) || 0
+    return {
+      name,
+      color: palette[index % palette.length],
+      values: factors.map((factor, factorIndex) =>
+        Math.max(0, Number((baseValue * factor + factorIndex * 0.6).toFixed(1)))
+      )
+    }
+  })
+}
+
 async function getStatic() {
   const res = await IotDangerApi.getDangerStatistics(userStore.user.deptId)
   staticData.value = res.classify || []
@@ -504,99 +534,123 @@ function destroyHazardChart() {
   hazardChart = null
 }
 
-function getSafeDayEntries() {
-  return Object.entries(safeDay.value || {})
-    .map(([label, value]) => ({
-      label,
-      value: Number(value) || 0
-    }))
-    .sort((a, b) => a.value - b.value)
-}
-
 function getSafeDayChartOption(): echarts.EChartsOption {
-  const entries = getSafeDayEntries()
-
   return {
     ...ANIMATION,
+    legend: {
+      top: 0,
+      left: 25,
+      bottom: 10,
+
+      itemWidth: 10,
+      itemHeight: 10,
+      icon: 'circle',
+      textStyle: {
+        color: '#7f92af',
+        fontSize: 12,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    },
     grid: {
       left: 32,
-      right: 32,
-      top: 12,
-      bottom: 24,
+      right: 20,
+      top: 42,
+      bottom: 10,
       containLabel: true
     },
     tooltip: createTooltip({
       trigger: 'axis',
+      // position: 'bottom',
       confine: true,
       appendToBody: false,
       axisPointer: {
-        type: 'shadow',
-        shadowStyle: {
-          color: 'rgba(31, 91, 184, 0.08)'
+        type: 'line',
+        lineStyle: {
+          color: 'rgba(79, 141, 255, 0.35)',
+          width: 1
         }
       },
       formatter(params: any) {
         if (!params || (Array.isArray(params) && params.length === 0)) return ''
-        const item = Array.isArray(params) ? params[0] : params
-        return `${item.name}<br/>安全天数:${item.value}`
+        const items = Array.isArray(params) ? params : [params]
+        const lines = items.map(
+          (item) =>
+            `${item.marker}${item.seriesName}:<span style="font-weight:700">${item.value}</span>`
+        )
+        return `${items[0].axisValue}<br/>${lines.join('<br/>')}`
       }
     }),
     xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: safeDayMonths,
+      axisLine: {
+        lineStyle: {
+          color: 'rgba(106, 144, 221, 0.45)'
+        }
+      },
+      axisTick: { show: false },
+      axisLabel: {
+        color: '#5b6f8f',
+        fontSize: 12,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    yAxis: {
       type: 'value',
       name: '安全生产天数',
       nameLocation: 'middle',
-      nameGap: 30,
+      nameGap: 45,
+
       nameTextStyle: {
-        color: '#5b6f8f',
+        color: '#657897',
         fontSize: 12,
-        fontWeight: 600,
         fontFamily: FONT_FAMILY
       },
       axisLine: { show: false },
       axisTick: { show: false },
       axisLabel: {
-        color: '#263854',
+        color: '#8a9bb5',
         fontSize: 12,
         fontFamily: FONT_FAMILY
       },
       splitLine: {
         lineStyle: {
-          color: 'rgba(83, 114, 173, 0.6)',
+          color: 'rgba(104, 139, 205, 0.22)',
           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' }
-          ])
-        }
+    series: safeDayTrendSeries.value.map((item) => ({
+      name: item.name,
+      type: 'line',
+      smooth: true,
+      emphasis: {
+        focus: 'series'
+      },
+      // areaStyle: {
+      //   color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+      //     { offset: 0, color: item.color },
+      //     { offset: 1, color: `${item.color}99` }
+      //   ])
+      // },
+      symbolSize: 7,
+      showSymbol: true,
+      data: item.values,
+      lineStyle: {
+        width: 2,
+        color: item.color,
+        bgColor: '#000'
+      },
+      itemStyle: {
+        color: item.color,
+        borderColor: item.color,
+        bgColor: '#000',
+        borderWidth: 1
       }
-    ]
+    }))
   }
 }
 
@@ -868,6 +922,7 @@ async function loadHomeBoard() {
   hazardBars.value[2].value = summaryPanel.value.todoHazard || 0
 
   safeDay.value = (await kanbanApi.getSafeDay(userStore.getUser.deptId)) || {}
+  buildSafeDayTrendSeries(safeDay.value)
 
   await Promise.all([getStatic(), getCertStatic(), getInstrumentOverview()])
 
@@ -1025,7 +1080,7 @@ onUnmounted(() => {
           <div v-if="activeSummaryTab === 'home'" class="summary-toolbar__date">
             <span class="summary-toolbar__date-label">日期:</span>
 
-            <div class="flex items-center gap-2">
+            <div class="flex items-center gap-2 datepicker">
               <el-date-picker
                 v-model="timeVal"
                 :teleport="true"
@@ -1557,7 +1612,7 @@ onUnmounted(() => {
 }
 
 .safe-day-chart-panel {
-  height: 218px;
+  height: 230px;
 }
 
 .qhse-metric-tabs {
@@ -1841,19 +1896,13 @@ onUnmounted(() => {
   flex: 1;
 }
 
-// :deep(.el-table) {
-//   --el-table-header-bg-color: rgba(248, 251, 255, 0.95);
-//   --el-table-row-hover-bg-color: rgba(79, 141, 255, 0.06);
-//   border-radius: 14px;
+// :deep(.el-input__inner) {
+//   font-size: 14px !important;
+//   border: none !important;
+//   font-size: 14px !important;
 // }
 
-:deep(.el-input__inner) {
-  font-size: 14px !important;
-  border: none !important;
-  font-size: 14px !important;
-}
-
-:deep(.el-input__wrapper) {
+:deep(.datepicker .el-input__wrapper) {
   background: transparent !important;
   border: none !important;
   font-size: 14px !important;

+ 132 - 39
src/views/pms/qhse/socSummary/index.vue

@@ -4,7 +4,7 @@
     <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
 
     <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
-      <div class="soc-summary-panel">
+      <div class="soc-summary-panel" v-loading="staticLoading">
         <div class="soc-summary-chart">
           <div class="soc-summary-chart__header">
             <div class="soc-summary-chart__title">
@@ -18,8 +18,7 @@
             ref="socChartRef"
             :options="socChartOption"
             :height="240"
-            @chart-click="handleChartClick"
-          />
+            @chart-click="handleChartClick" />
         </div>
       </div>
 
@@ -193,8 +192,12 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
 import { useUserStore } from '@/store/modules/user'
 const userStore = useUserStore()
 
-type SummaryItem = Record<string, number>
-type ChildMap = Record<string, SummaryItem[]>
+type SummaryItem = Record<string, any>
+type ChartDataItem = {
+  id?: string | number
+  name: string
+  value: number
+}
 
 const DRILLDOWN_KEYS = ['个人防护', '规范操作', '规范指挥', '人员位置', '作业场所'] as const
 
@@ -301,30 +304,49 @@ const downloadSOC = async (row) => {
   )
 }
 
-const child = ref<ChildMap>({})
+const childData = ref<SummaryItem[]>([])
 const totalData = ref<SummaryItem[]>([])
 const currentDrilldownKey = ref('')
+const currentDrilldownId = ref<string | number | undefined>(undefined)
+
+const normalizeChartData = (source: SummaryItem[] = []): ChartDataItem[] => {
+  return source
+    .map((item) => {
+      if (!item) return null
+
+      if ('name' in item || 'className' in item || 'label' in item) {
+        return {
+          id: item.id ?? item.socClass ?? item.valueId,
+          name: item.name ?? item.className ?? item.label ?? '',
+          value: Number(item.value ?? item.count ?? item.total ?? 0) || 0
+        }
+      }
+
+      const entries = Object.entries(item)
+      const nameEntry =
+        entries.find(
+          ([key, value]) =>
+            !['id', 'socClass', 'valueId', 'value', 'count', 'total'].includes(key) &&
+            typeof value === 'number'
+        ) || entries.find(([key]) => !['id', 'socClass', 'valueId'].includes(key))
+
+      const [name, value] = nameEntry || ['', 0]
+      return {
+        id: item.id ?? item.socClass ?? item.valueId,
+        name,
+        value: Number(value) || 0
+      }
+    })
+    .filter((item): item is ChartDataItem => !!item && !!item.name)
+}
 
 const totalChartData = computed(() => {
-  return (totalData.value as SummaryItem[]).map((item) => {
-    const [name, value] = Object.entries(item || {})[0] || ['', 0]
-    return {
-      name,
-      value: Number(value) || 0
-    }
-  })
+  return normalizeChartData(totalData.value as SummaryItem[])
 })
 
 const childChartData = computed(() => {
   if (!currentDrilldownKey.value) return []
-  const source = (child.value as ChildMap)?.[currentDrilldownKey.value] || []
-  return source.map((item) => {
-    const [name, value] = Object.entries(item || {})[0] || ['', 0]
-    return {
-      name,
-      value: Number(value) || 0
-    }
-  })
+  return normalizeChartData(childData.value as SummaryItem[])
 })
 
 const chartColorMap: Record<string, string> = {
@@ -336,6 +358,17 @@ const chartColorMap: Record<string, string> = {
   作业场所: '#ff00ff'
 }
 
+const childChartColorPalette = [
+  '#4f8dff',
+  '#43c7ca',
+  '#ff981f',
+  '#8d8cff',
+  '#ff7a7a',
+  '#52c41a',
+  '#13c2c2',
+  '#faad14'
+]
+
 const hexToRgba = (hex: string, alpha: number) => {
   const normalizedHex = hex.replace('#', '')
   const safeHex =
@@ -353,16 +386,16 @@ const hexToRgba = (hex: string, alpha: number) => {
   return `rgba(${red}, ${green}, ${blue}, ${alpha})`
 }
 
-const getBarBaseColor = (name: string, isDrilldown: boolean) => {
-  if (isDrilldown && currentDrilldownKey.value) {
-    return chartColorMap[currentDrilldownKey.value] || '#67c23a'
+const getBarBaseColor = (name: string, isDrilldown: boolean, index: number) => {
+  if (isDrilldown) {
+    return childChartColorPalette[index % childChartColorPalette.length]
   }
 
   return chartColorMap[name] || '#409eff'
 }
 
-const getBarItemStyle = (name: string, isDrilldown: boolean) => {
-  const baseColor = getBarBaseColor(name, isDrilldown)
+const getBarItemStyle = (name: string, isDrilldown: boolean, index: number) => {
+  const baseColor = getBarBaseColor(name, isDrilldown, index)
 
   return {
     color: {
@@ -465,9 +498,11 @@ const socChartOption = computed<EChartsOption>(() => {
             shadowOffsetY: 8
           }
         },
-        data: sourceData.map((item) => ({
+        data: sourceData.map((item, index) => ({
+          id: item.id,
+          name: item.name,
           value: item.value,
-          itemStyle: getBarItemStyle(item.name, isDrilldown)
+          itemStyle: getBarItemStyle(item.name, isDrilldown, index)
         })),
         label: {
           show: true,
@@ -482,37 +517,71 @@ const socChartOption = computed<EChartsOption>(() => {
   }
 })
 
+let staticLoading = ref(false)
+async function loadChildChartData(drilldownKey: string, socClass: string | number) {
+  const res = await IotSocSummaryApi.getSocSummaryStatisticsChild({
+    deptId: queryParams.deptId || userStore.user.deptId,
+    observationDate: queryParams.observationDate,
+    socClass
+  })
+
+  currentDrilldownKey.value = drilldownKey
+  currentDrilldownId.value = socClass
+  childData.value = res?.total || res?.list || res || []
+}
+
 async function getStatic() {
+  const drilldownKey = currentDrilldownKey.value
+  const drilldownId = currentDrilldownId.value
   if (queryParams.deptId) {
+    staticLoading.value = true
     const res = await IotSocSummaryApi.getSocSummaryStatistics({
       deptId: queryParams.deptId,
       // 日期
       observationDate: queryParams.observationDate
     })
-    child.value = res.child
-    totalData.value = res.total
+    totalData.value = res.total || res || []
+    childData.value = []
+    if (drilldownKey && drilldownId !== undefined) {
+      await loadChildChartData(drilldownKey, drilldownId)
+    }
+    staticLoading.value = false
   } else {
+    staticLoading.value = true
     queryParams.deptId = userStore.user.deptId
     const res = await IotSocSummaryApi.getSocSummaryStatistics({
       deptId: queryParams.deptId,
       // 日期
       observationDate: queryParams.observationDate
     })
-    child.value = res.child
-    totalData.value = res.total
+    totalData.value = res.total || res || []
+    childData.value = []
+    if (drilldownKey && drilldownId !== undefined) {
+      await loadChildChartData(drilldownKey, drilldownId)
+    }
+    staticLoading.value = false
   }
 }
 
-const handleChartClick = (params: any) => {
+const handleChartClick = async (params: any) => {
   const name = params?.name
   if (!name || currentDrilldownKey.value) return
-  if (DRILLDOWN_KEYS.some((item) => item === name)) {
-    currentDrilldownKey.value = name
+  const target = totalChartData.value.find((item) => item.name === name)
+  const socClass = params?.data?.id ?? target?.id
+  if (DRILLDOWN_KEYS.some((item) => item === name) && socClass !== undefined) {
+    staticLoading.value = true
+    try {
+      await loadChildChartData(name, socClass)
+    } finally {
+      staticLoading.value = false
+    }
   }
 }
 
 const resetDrilldown = () => {
   currentDrilldownKey.value = ''
+  currentDrilldownId.value = undefined
+  childData.value = []
 }
 
 /** 初始化 **/
@@ -645,17 +714,41 @@ onMounted(() => {
   padding: 3px 16px 0px;
   border: 1px solid rgba(143, 168, 211, 0.22);
   border-radius: 10px;
-  background: radial-gradient(circle at top left, rgba(64, 158, 255, 0.18), transparent 0%),
-    radial-gradient(circle at top right, rgba(208, 156, 255, 0.16), transparent 2%),
+  background: radial-gradient(
+      circle at 102% 104%,
+      rgba(142, 164, 255, 0.12) 0,
+      rgba(142, 164, 255, 0.12) 14%,
+      transparent 14.5%
+    ),
+    radial-gradient(
+      circle at 88% 16%,
+      rgba(118, 183, 255, 0.08) 0,
+      rgba(118, 183, 255, 0.08) 7%,
+      transparent 100%
+    ),
     linear-gradient(135deg, #fdfefe 0%, #f3f8ff 68%, #eef4ff 100%);
-  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
+  box-shadow:
+    0 12px 28px rgba(64, 112, 196, 0.08),
+    inset 0 1px 0 rgba(255, 255, 255, 0.9);
 }
 
 .soc-summary-panel::before {
   content: '';
   position: absolute;
   inset: 0;
-  background: linear-gradient(115deg, rgba(255, 255, 255, 0.22), transparent 42%),
+  background: radial-gradient(
+      circle at 76% -4%,
+      rgba(129, 224, 202, 0.14) 0,
+      rgba(129, 224, 202, 0.14) 12.8%,
+      transparent 13%
+    ),
+    radial-gradient(
+      circle at 101% 101%,
+      rgba(142, 164, 255, 0.1) 0,
+      rgba(142, 164, 255, 0.1) 15.8%,
+      transparent 16%
+    ),
+    linear-gradient(115deg, rgba(255, 255, 255, 0.22), transparent 42%),
     repeating-linear-gradient(
       135deg,
       rgba(255, 255, 255, 0.08) 0,