Quellcode durchsuchen

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

yanghao vor 1 Woche
Ursprung
Commit
3d3449909e

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

@@ -394,6 +394,13 @@ export const IotSocSummaryApi = {
   // 下载SOC卡
   downloadIotSocSummary: async (id) => {
     return await request.download({ url: `/rq/iot-soc-summary/safety-card/download/${id}` })
+  },
+  // 统计
+  // getSocSummaryStatistics: async (id) => {
+  //   return await request.get({ url: `/rq/iot-soc-summary/stat?deptId=${id}` })
+  // }
+  getSocSummaryStatistics: async (params) => {
+    return await request.get({ url: `/rq/iot-soc-summary/stat`, params })
   }
 }
 

+ 12 - 0
src/components/Echart/src/Echart.vue

@@ -15,6 +15,10 @@ import 'echarts/lib/component/markArea'
 
 defineOptions({ name: 'EChart' })
 
+const emit = defineEmits<{
+  chartClick: [params: any]
+}>()
+
 const { getPrefixCls, variables } = useDesign()
 
 const prefixCls = getPrefixCls('echart')
@@ -64,6 +68,10 @@ const initChart = () => {
   if (unref(elRef) && props.options) {
     echartRef = echarts.init(unref(elRef) as HTMLElement)
     echartRef?.setOption(unref(options))
+    echartRef?.off('click')
+    echartRef?.on('click', (params) => {
+      emit('chartClick', params)
+    })
   }
 }
 
@@ -85,6 +93,10 @@ const resizeHandler = debounce(() => {
   }
 }, 100)
 
+defineExpose({
+  resize: resizeHandler
+})
+
 const contentResizeHandler = async (e: TransitionEvent) => {
   if (e.propertyName === 'width') {
     resizeHandler()

+ 34 - 48
src/views/pms/device/monitor/index.vue

@@ -6,25 +6,17 @@
       <ContentWrap v-loading="loading" style="border: none">
         <div style="border: none" class="px-2 py-3 rounded-sm bg-white">
           <!-- 搜索工作栏 -->
-          <el-form
-            class="-mb-15px"
-            :model="queryParams"
-            ref="queryFormRef"
-            :inline="true"
-            label-width="68px"
-          >
+          <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
             <el-form-item
               :label="t('monitor.deviceName')"
               prop="deviceName"
-              style="margin-left: 20px"
-            >
+              style="margin-left: 20px">
               <el-input
                 v-model="queryParams.deviceName"
                 :placeholder="t('monitor.nameHolder')"
                 clearable
                 @keyup.enter="handleQuery"
-                class="!w-240px"
-              />
+                class="!w-240px" />
             </el-form-item>
             <el-form-item :label="t('monitor.deviceCode')" prop="deviceCode">
               <el-input
@@ -32,36 +24,42 @@
                 :placeholder="t('monitor.codeHolder')"
                 clearable
                 @keyup.enter="handleQuery"
-                class="!w-240px"
-              />
+                class="!w-240px" />
             </el-form-item>
             <el-form-item :label="t('monitor.ifInline')" prop="ifInline">
               <el-select
                 v-model="queryParams.ifInline"
                 :placeholder="t('monitor.ifInlineHolder')"
                 clearable
-                class="!w-240px"
-              >
+                class="!w-240px">
                 <el-option
                   v-for="dict in getStrDictOptions(DICT_TYPE.IOT_DEVICE_STATUS)"
                   :key="dict.value"
                   :label="dict.label"
-                  :value="dict.value"
-                />
+                  :value="dict.value" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="数采查询" prop="source">
+              <el-select
+                v-model="queryParams.source"
+                placeholder="请选择数采来源"
+                clearable
+                class="!w-240px">
+                <el-option label="全部" value="all" />
+                <el-option label="网关设备" value="gateway" />
+                <el-option label="中行北斗" value="zhbd" />
               </el-select>
             </el-form-item>
             <el-form-item class="float-right !mr-0 !mb-0">
               <el-button-group>
                 <el-button
                   :type="viewMode === 'card' ? 'primary' : 'default'"
-                  @click="viewMode = 'card'"
-                >
+                  @click="viewMode = 'card'">
                   <Icon icon="ep:grid" />
                 </el-button>
                 <el-button
                   :type="viewMode === 'list' ? 'primary' : 'default'"
-                  @click="viewMode = 'list'"
-                >
+                  @click="viewMode = 'list'">
                   <Icon icon="ep:list" />
                 </el-button>
               </el-button-group>
@@ -80,8 +78,7 @@
                 plain
                 @click="handleExport"
                 :loading="exportLoading"
-                v-hasPermi="['iot:device:export']"
-              >
+                v-hasPermi="['iot:device:export']">
                 <Icon icon="ep:download" class="mr-5px" /> 导出
               </el-button>
             </el-form-item>
@@ -99,12 +96,10 @@
                 :sm="12"
                 :md="12"
                 :lg="6"
-                class="mb-4"
-              >
+                class="mb-4">
                 <el-card
                   class="h-full transition-colors relative overflow-hidden custom-card"
-                  :body-style="{ padding: '0' }"
-                >
+                  :body-style="{ padding: '0' }">
                   <!-- 添加渐变背景层 -->
                   <div
                     class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none"
@@ -112,8 +107,7 @@
                       item.ifInline === 3
                         ? 'bg-gradient-to-b from-[#eefaff] to-transparent'
                         : 'bg-gradient-to-b from-[#fff1f1] to-transparent'
-                    ]"
-                  >
+                    ]">
                   </div>
                   <div class="p-4 relative">
                     <!-- 标题区域 -->
@@ -132,13 +126,11 @@
                             item.ifInline === 3
                               ? 'bg-[var(--el-color-success)]'
                               : 'bg-[var(--el-color-danger)]'
-                          "
-                        >
+                          ">
                         </div>
                         <el-text
                           class="!text-xs font-bold"
-                          :type="item.ifInline === 3 ? 'success' : 'danger'"
-                        >
+                          :type="item.ifInline === 3 ? 'success' : 'danger'">
                           {{ getDictLabel(DICT_TYPE.IOT_DEVICE_STATUS, item.ifInline) }}
                         </el-text>
                       </div>
@@ -172,8 +164,7 @@
                         <img
                           v-if="!item.carId"
                           src="@/assets/imgs/iot/device.png"
-                          class="w-full h-full"
-                        />
+                          class="w-full h-full" />
                         <img v-if="item.carId" src="@/assets/imgs/iot/car.png" class="mt-4 ml-4" />
                       </div>
                     </div>
@@ -198,8 +189,7 @@
                             item.vehicleName,
                             item.carOnline ?? ''
                           )
-                        "
-                      >
+                        ">
                         <Icon icon="ep:view" class="mr-1" />
                         {{ t('monitor.details') }}
                       </el-button>
@@ -218,8 +208,7 @@
             :data="list"
             :stripe="true"
             height="60vh"
-            :show-overflow-tooltip="true"
-          >
+            :show-overflow-tooltip="true">
             <zm-table-column :label="t('monitor.serial')" width="60" align="center">
               <template #default="scope">
                 {{ scope.$index + 1 }}
@@ -248,14 +237,12 @@
               align="center"
               prop="lastInlineTime"
               :formatter="dateFormatter"
-              width="180px"
-            />
+              width="180px" />
             <zm-table-column
               :label="t('monitor.operation')"
               align="center"
               min-width="120px"
-              action
-            >
+              action>
               <template #default="scope">
                 <el-button
                   link
@@ -271,8 +258,7 @@
                       scope.row.vehicleName,
                       scope.row.carOnline ?? ''
                     )
-                  "
-                >
+                  ">
                   {{ t('monitor.check') }}
                 </el-button>
               </template>
@@ -284,8 +270,7 @@
             :total="total"
             v-model:page="queryParams.pageNo"
             v-model:limit="queryParams.pageSize"
-            @pagination="getList"
-          />
+            @pagination="getList" />
         </ContentWrap>
       </ContentWrap>
     </el-col>
@@ -340,7 +325,8 @@ const queryParams = reactive({
   infoUrl: undefined,
   templateJson: undefined,
   creator: undefined,
-  ifInline: undefined
+  ifInline: undefined,
+  source: undefined
 })
 const queryFormRef = ref() // 搜索的表单
 const exportLoading = ref(false) // 导出加载状态

+ 421 - 56
src/views/pms/qhse/kanban/index.vue

@@ -1,8 +1,6 @@
-<script lang="ts" setup>
+<script lang="ts" setup>
 import * as echarts from 'echarts'
-import { VueDatePicker } from '@vuepic/vue-datepicker'
 import '@vuepic/vue-datepicker/dist/main.css'
-import { zhCN } from 'date-fns/locale'
 import {
   AlarmClock,
   Checked,
@@ -46,6 +44,7 @@ type PermitStat = {
 type SafeDayMap = Record<string, number>
 
 type SummaryTabValue = 'home' | 'certificate'
+type QhseMetricValue = 'ltir' | 'trir' | 'pmva'
 
 const userStore = useUserStore()
 
@@ -83,6 +82,7 @@ const timeVal = ref(getCurrentPickerValue(type.value))
 const wrapperRef = ref<HTMLDivElement>()
 const hazardChartRef = ref<HTMLDivElement>()
 const safeDayChartRef = ref<HTMLDivElement>()
+const qhseTrendChartRef = ref<HTMLDivElement>()
 const socChartRef = ref<HTMLDivElement>()
 const scale = ref(1)
 const supportsZoom = ref(false)
@@ -91,6 +91,7 @@ let resizeObserver: ResizeObserver | null = null
 let resizeRaf = 0
 let hazardChart: echarts.ECharts | null = null
 let safeDayChart: echarts.ECharts | null = null
+let qhseTrendChart: echarts.ECharts | null = null
 let socChart: echarts.ECharts | null = null
 
 const summaryTabs: Array<{ label: string; value: SummaryTabValue }> = [
@@ -109,9 +110,11 @@ watch(
       nextTick(() => {
         initHazardChart()
         initSafeDayChart()
+        initQhseTrendChart()
         initSocChart()
         resizeHazardChart()
         resizeSafeDayChart()
+        resizeQhseTrendChart()
         resizeSocChart()
       })
     }
@@ -199,6 +202,64 @@ const qualificationWarnings = ref([
   { label: '即将到期', value: 0, accent: '#e6ab00' }
 ])
 
+const qhseMetricTabs = [
+  { label: 'LTIR(百万工时)', value: 'ltir', accent: '#3d7cff', label2: '百万工时损失工时事故率' },
+  { label: 'TRIR(百万工时)', value: 'trir', accent: '#17b6c5', label2: '百万工时总可记录事件率' },
+  {
+    label: 'PMVA(百万公里)',
+    value: 'pmva',
+    accent: '#f2a93b',
+    label2: '百万公里可预防性交通事故率'
+  }
+] as const
+
+const activeQhseMetric = ref<QhseMetricValue>('ltir')
+
+const qhseTrendSeries = ref<Record<QhseMetricValue, Array<{ year: string; value: number }>>>({
+  ltir: [
+    { year: '2026年1月', value: 0.42 },
+    { year: '2026年2月', value: 0.39 },
+    { year: '2026年3月', value: 0.36 },
+    { year: '2026年4月', value: 0.34 },
+    { year: '2026年5月', value: 0.31 },
+    { year: '2026年6月', value: 0.29 },
+    { year: '2026年7月', value: 0.27 },
+    { year: '2026年8月', value: 0.25 },
+    { year: '2026年9月', value: 0.24 },
+    { year: '2026年10月', value: 0.22 },
+    { year: '2026年11月', value: 0.21 },
+    { year: '2026年12月', value: 0.2 }
+  ],
+  trir: [
+    { year: '1月', value: 0.78 },
+    { year: '2月', value: 0.75 },
+    { year: '3月', value: 0.73 },
+    { year: '4月', value: 0.7 },
+    { year: '5月', value: 0.68 },
+    { year: '6月', value: 0.66 },
+    { year: '7月', value: 0.63 },
+    { year: '8月', value: 0.61 },
+    { year: '9月', value: 0.59 },
+    { year: '10月', value: 0.57 },
+    { year: '11月', value: 0.55 },
+    { year: '12月', value: 0.54 }
+  ],
+  pmva: [
+    { year: '1月', value: 0.22 },
+    { year: '2月', value: 0.21 },
+    { year: '3月', value: 0.2 },
+    { year: '4月', value: 0.18 },
+    { year: '5月', value: 0.17 },
+    { year: '6月', value: 0.16 },
+    { year: '7月', value: 0.15 },
+    { year: '8月', value: 0.14 },
+    { year: '9月', value: 0.14 },
+    { year: '10月', value: 0.13 },
+    { year: '11月', value: 0.12 },
+    { year: '12月', value: 0.12 }
+  ]
+})
+
 const bottomCards = ref([
   {
     title: '体系合规',
@@ -222,11 +283,11 @@ const bottomCards = ref([
     lines: ['年度应急演练:80次']
   },
   {
-    title: '质量检验',
+    title: '职业健康',
     icon: Checked,
     accent: '#f2c11a',
     glow: 'rgba(242, 193, 26, 0.2)',
-    lines: ['产品合格率', '98.7%(达标)']
+    lines: ['职业健康体检率', '98.7%(达标)']
   },
   {
     title: '环境危废',
@@ -273,6 +334,7 @@ function updateScale() {
     nextTick(() => {
       resizeHazardChart()
       resizeSafeDayChart()
+      resizeQhseTrendChart()
       resizeSocChart()
     })
   })
@@ -331,6 +393,8 @@ function getHazardChartOption(): echarts.EChartsOption {
     },
     tooltip: createTooltip({
       trigger: 'axis',
+      confine: true,
+      appendToBody: false,
       axisPointer: {
         type: 'shadow',
         shadowStyle: {
@@ -338,6 +402,7 @@ function getHazardChartOption(): echarts.EChartsOption {
         }
       },
       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}`
       }
@@ -357,6 +422,15 @@ function getHazardChartOption(): echarts.EChartsOption {
     },
     yAxis: {
       type: 'value',
+      name: '隐患数量',
+      nameLocation: 'end',
+      nameGap: 13,
+      nameTextStyle: {
+        color: '#657897',
+        fontSize: 12,
+        // fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      },
       axisLine: { show: false },
       axisTick: { show: false },
       axisLabel: {
@@ -415,11 +489,14 @@ function initHazardChart() {
 }
 
 function updateHazardChart() {
-  hazardChart?.setOption(getHazardChartOption(), true)
+  if (!hazardChart) return
+  hazardChart.clear()
+  hazardChart.setOption(getHazardChartOption(), { notMerge: true, lazyUpdate: false })
 }
 
 function resizeHazardChart() {
-  hazardChart?.resize()
+  if (!hazardChart) return
+  hazardChart.resize({ animation: { duration: 300 } })
 }
 
 function destroyHazardChart() {
@@ -442,7 +519,7 @@ function getSafeDayChartOption(): echarts.EChartsOption {
   return {
     ...ANIMATION,
     grid: {
-      left: 52,
+      left: 32,
       right: 32,
       top: 12,
       bottom: 24,
@@ -450,6 +527,8 @@ function getSafeDayChartOption(): echarts.EChartsOption {
     },
     tooltip: createTooltip({
       trigger: 'axis',
+      confine: true,
+      appendToBody: false,
       axisPointer: {
         type: 'shadow',
         shadowStyle: {
@@ -457,12 +536,22 @@ function getSafeDayChartOption(): echarts.EChartsOption {
         }
       },
       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}`
       }
     }),
     xAxis: {
       type: 'value',
+      name: '安全生产天数',
+      nameLocation: 'middle',
+      nameGap: 30,
+      nameTextStyle: {
+        color: '#5b6f8f',
+        fontSize: 12,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      },
       axisLine: { show: false },
       axisTick: { show: false },
       axisLabel: {
@@ -521,11 +610,14 @@ function initSafeDayChart() {
 }
 
 function updateSafeDayChart() {
-  safeDayChart?.setOption(getSafeDayChartOption(), true)
+  if (!safeDayChart) return
+  safeDayChart.clear()
+  safeDayChart.setOption(getSafeDayChartOption(), { notMerge: true, lazyUpdate: false })
 }
 
 function resizeSafeDayChart() {
-  safeDayChart?.resize()
+  if (!safeDayChart) return
+  safeDayChart.resize({ animation: { duration: 300 } })
 }
 
 function destroySafeDayChart() {
@@ -533,18 +625,152 @@ function destroySafeDayChart() {
   safeDayChart = null
 }
 
+function getQhseTrendChartOption(): echarts.EChartsOption {
+  const activeTab =
+    qhseMetricTabs.find((item) => item.value === activeQhseMetric.value) || qhseMetricTabs[0]
+  const seriesData = qhseTrendSeries.value[activeQhseMetric.value] || []
+
+  return {
+    ...ANIMATION,
+    grid: {
+      left: 32,
+      right: 18,
+      top: 28,
+      bottom: 28,
+      containLabel: true
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      confine: true,
+      appendToBody: false,
+      axisPointer: {
+        type: 'line',
+        lineStyle: {
+          color: `${activeTab.accent}99`,
+          width: 1.5
+        }
+      },
+      formatter(params: any) {
+        if (!params || (Array.isArray(params) && params.length === 0)) return ''
+        const item = Array.isArray(params) ? params[0] : params
+        return `${item.axisValue}<br/>${activeTab.label}:${item.data}`
+      }
+    }),
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: seriesData.map((item) => item.year),
+      axisLine: {
+        lineStyle: {
+          color: 'rgba(106, 144, 221, 0.45)'
+        }
+      },
+      axisTick: { show: false },
+      axisLabel: {
+        color: '#5b6f8f',
+        rotate: 28,
+        fontSize: 13,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    yAxis: {
+      type: 'value',
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: {
+        color: '#8a9bb5',
+        fontSize: 12,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: 'rgba(104, 139, 205, 0.22)',
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        type: 'line',
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 9,
+        showSymbol: true,
+        data: seriesData.map((item) => item.value),
+        lineStyle: {
+          width: 3,
+          color: activeTab.accent
+        },
+        itemStyle: {
+          color: activeTab.accent,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: `${activeTab.accent}55` },
+            { offset: 1, color: `${activeTab.accent}08` }
+          ])
+        }
+      }
+    ]
+  }
+}
+
+function initQhseTrendChart() {
+  if (!qhseTrendChartRef.value) return
+  qhseTrendChart?.dispose()
+  qhseTrendChart = echarts.init(qhseTrendChartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  qhseTrendChart.setOption(getQhseTrendChartOption(), { notMerge: true, lazyUpdate: false })
+}
+
+function updateQhseTrendChart() {
+  if (!qhseTrendChart) return
+  qhseTrendChart.clear()
+  qhseTrendChart.setOption(getQhseTrendChartOption(), { notMerge: true, lazyUpdate: false })
+}
+
+function resizeQhseTrendChart() {
+  if (!qhseTrendChart) return
+  qhseTrendChart.resize({ animation: { duration: 300 } })
+}
+
+function destroyQhseTrendChart() {
+  qhseTrendChart?.dispose()
+  qhseTrendChart = null
+}
+
 function getSocChartOption(): echarts.EChartsOption {
   return {
     ...ANIMATION,
+    title: {
+      text: 'SOC',
+      left: '34%',
+      top: '54%',
+      textAlign: 'center',
+      textVerticalAlign: 'middle',
+      textStyle: {
+        color: '#3d5f94',
+        fontSize: 24,
+        fontWeight: 800,
+        fontFamily: FONT_FAMILY
+      }
+    },
     tooltip: createTooltip({
       trigger: 'item',
+      confine: true,
+      appendToBody: false,
       formatter(params: any) {
+        if (!params) return ''
         return `${params.name}<br/>数量:${params.value}<br/>占比:${params.percent}%`
       }
     }),
     legend: {
       orient: 'vertical',
-      right: 4,
+      right: 100,
       top: 'center',
       itemWidth: 12,
       itemHeight: 12,
@@ -601,11 +827,18 @@ function initSocChart() {
   socChart = echarts.init(socChartRef.value, undefined, {
     renderer: CHART_RENDERER
   })
-  socChart.setOption(getSocChartOption(), true)
+  socChart.setOption(getSocChartOption(), { notMerge: true, lazyUpdate: false })
+}
+
+function updateSocChart() {
+  if (!socChart) return
+  socChart.clear()
+  socChart.setOption(getSocChartOption(), { notMerge: true, lazyUpdate: false })
 }
 
 function resizeSocChart() {
-  socChart?.resize()
+  if (!socChart) return
+  socChart.resize({ animation: { duration: 300 } })
 }
 
 function destroySocChart() {
@@ -641,8 +874,11 @@ async function loadHomeBoard() {
   nextTick(() => {
     updateHazardChart()
     updateSafeDayChart()
+    updateQhseTrendChart()
+    updateSocChart()
     resizeHazardChart()
     resizeSafeDayChart()
+    resizeQhseTrendChart()
     resizeSocChart()
   })
 }
@@ -718,6 +954,7 @@ watch(
       nextTick(() => {
         resizeHazardChart()
         resizeSafeDayChart()
+        resizeQhseTrendChart()
         resizeSocChart()
       })
     }
@@ -732,7 +969,7 @@ watch(
 )
 
 onMounted(async () => {
-  supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
+  // supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
   nextTick(updateScale)
   resizeObserver = new ResizeObserver(updateScale)
   if (wrapperRef.value) {
@@ -740,10 +977,12 @@ onMounted(async () => {
   }
   initHazardChart()
   initSafeDayChart()
+  initQhseTrendChart()
   initSocChart()
   window.addEventListener('resize', updateScale)
   window.addEventListener('resize', resizeHazardChart)
   window.addEventListener('resize', resizeSafeDayChart)
+  window.addEventListener('resize', resizeQhseTrendChart)
   window.addEventListener('resize', resizeSocChart)
 
   await loadHomeBoard()
@@ -754,10 +993,12 @@ onUnmounted(() => {
   window.removeEventListener('resize', updateScale)
   window.removeEventListener('resize', resizeHazardChart)
   window.removeEventListener('resize', resizeSafeDayChart)
+  window.removeEventListener('resize', resizeQhseTrendChart)
   window.removeEventListener('resize', resizeSocChart)
   cancelAnimationFrame(resizeRaf)
   destroyHazardChart()
   destroySafeDayChart()
+  destroyQhseTrendChart()
   destroySocChart()
 })
 </script>
@@ -784,37 +1025,29 @@ onUnmounted(() => {
           <div v-if="activeSummaryTab === 'home'" class="summary-toolbar__date">
             <span class="summary-toolbar__date-label">日期:</span>
 
-            <div class="flex items-center">
-              <!-- 维度切换下拉 -->
-              <el-select v-model="type" placeholder="筛选维度" style="width: 120px">
-                <el-option label="按日" value="day" />
-                <el-option label="按月" value="month" />
-                <el-option label="按季度" value="quarter" />
-                <el-option label="按年" value="year" />
-              </el-select>
-
-              <!-- 日期组件 -->
-              <VueDatePicker
-                v-model="timeVal"
-                vertical
-                v-if="type === 'quarter'"
-                quarter-picker
-                :teleport="true"
-                :locale="zhCN"
-                @update:model-value="handelChange"
-                style="margin-left: 10px; width: 320px" />
-
+            <div class="flex items-center gap-2">
               <el-date-picker
-                v-else
                 v-model="timeVal"
                 :teleport="true"
                 :type="type === 'day' ? 'date' : type === 'month' ? 'month' : 'year'"
+                class="summary-toolbar__picker el-date-picker"
+                popper-class="summary-toolbar__picker-popper"
                 value-format="YYYY-MM-DD"
                 range-separator="~"
                 start-placeholder="开始"
                 end-placeholder="结束"
                 @change="handelChange"
-                style="width: 320px; height: 40px; margin-left: 10px" />
+                style="
+                  width: 150px;
+                  height: 40px;
+                  margin-left: 10px;
+                  background-color: transparent;
+                " />
+
+              <span class="bg-[#f5f9ff] rounded-2xl px-2 py-0 cursor-pointer">月度</span>
+              <span class="bg-[#f5f9ff] rounded-2xl px-2 py-0 cursor-pointer">季度</span>
+              <span class="bg-[#f5f9ff] rounded-2xl px-2 py-0 cursor-pointer">年度</span>
+              <span class="bg-[#f5f9ff] rounded-2xl px-2 py-0 cursor-pointer">初始化</span>
             </div>
           </div>
         </div>
@@ -823,7 +1056,7 @@ onUnmounted(() => {
           <section class="panel summary-panel kb-stage-card kb-stage-card--1">
             <div class="panel-title">
               <span class="icon-decorator"><span></span><span></span></span>
-              风险总览
+              QHSE关键数据
             </div>
             <div class="summary-grid">
               <article
@@ -861,7 +1094,7 @@ onUnmounted(() => {
               <section class="panel board-panel board-panel--center kb-stage-card kb-stage-card--4">
                 <div class="panel-title panel-title--center">
                   <span class="icon-decorator"><span></span><span></span></span>
-                  风险管控及隐患排查
+                  风险管控及隐患治理
                 </div>
                 <div class="risk-grid">
                   <article v-for="zone in riskZones" :key="zone.title" class="risk-card">
@@ -878,11 +1111,7 @@ onUnmounted(() => {
                   </article>
                 </div>
 
-                <div class="risk-hazard-block">
-                  <div class="panel-title risk-hazard-block__title">
-                    <span class="icon-decorator"><span></span><span></span></span>
-                    隐患排查治理统计
-                  </div>
+                <div class="risk-hazard-block mt-2!">
                   <div
                     ref="hazardChartRef"
                     class="chart-panel chart-panel--echart risk-hazard-block__chart"></div>
@@ -894,9 +1123,36 @@ onUnmounted(() => {
               <section class="panel board-panel kb-stage-card kb-stage-card--3">
                 <div class="panel-title panel-title--center">
                   <span class="icon-decorator"><span></span><span></span></span>
-                  结果指标
+                  QHSE指标
                 </div>
-                <div ref="safeDayChartRef" class="safe-day-chart-panel"></div>
+
+                <section class="board-panel kb-stage-card kb-stage-card--5">
+                  <div ref="safeDayChartRef" class="safe-day-chart-panel"></div>
+                </section>
+
+                <section class="board-panel kb-stage-card kb-stage-card--5">
+                  <div class="qhse-metric-tabs">
+                    <button
+                      v-for="item in qhseMetricTabs"
+                      :key="item.value"
+                      :title="item.label2"
+                      type="button"
+                      class="qhse-metric-tab"
+                      :class="{ 'is-active': activeQhseMetric === item.value }"
+                      @click="
+                        () => {
+                          activeQhseMetric = item.value
+                          nextTick(() => {
+                            updateQhseTrendChart()
+                            resizeQhseTrendChart()
+                          })
+                        }
+                      ">
+                      {{ item.label }}
+                    </button>
+                  </div>
+                  <div ref="qhseTrendChartRef" class="qhse-trend-chart-panel"></div>
+                </section>
               </section>
             </div>
 
@@ -904,21 +1160,13 @@ onUnmounted(() => {
               <section class="panel board-panel board-panel--center kb-stage-card kb-stage-card--4">
                 <div class="panel-title panel-title--center">
                   <span class="icon-decorator"><span></span><span></span></span>
-                  行为安全与风险预警
+                  行为安全与系统预警
                 </div>
                 <section class="board-panel kb-stage-card kb-stage-card--5 pt-2">
-                  <div class="panel-title">
-                    <!-- <span class="icon-decorator"><span></span><span></span></span> -->
-                    SOC卡类型
-                  </div>
                   <div ref="socChartRef" class="soc-chart-panel"></div>
                 </section>
 
                 <section class="board-panel kb-stage-card kb-stage-card--6 pl-4">
-                  <div class="panel-title">
-                    <span class="icon-decorator"><span></span><span></span></span>
-                    人员资质风险预警
-                  </div>
                   <div class="qualification-panel">
                     <div class="qualification-icon">
                       <el-icon>
@@ -1191,11 +1439,38 @@ onUnmounted(() => {
   background: rgb(255 255 255 / 80%);
   border: 1px solid rgb(137 176 235 / 38%);
   border-radius: 10px;
+  font-size: 14px !important;
   box-shadow:
     inset 0 1px 0 rgb(255 255 255 / 92%),
     0 8px 14px rgb(63 103 171 / 6%);
 }
 
+.summary-toolbar__picker :deep(.el-input__inner),
+.summary-toolbar__picker :deep(.el-range-input),
+.summary-toolbar__picker :deep(.el-input__prefix),
+.summary-toolbar__picker :deep(.el-input__suffix) {
+  font-size: 14px !important;
+}
+
+.summary-toolbar__picker :deep(.el-input__inner::placeholder),
+.summary-toolbar__picker :deep(.el-range-input::placeholder) {
+  font-size: 14px !important;
+}
+
+:deep(.summary-toolbar__picker-popper) {
+  font-size: 14px;
+}
+
+:deep(.summary-toolbar__picker-popper .el-date-table),
+:deep(.summary-toolbar__picker-popper .el-date-table th),
+:deep(.summary-toolbar__picker-popper .el-date-table td),
+:deep(.summary-toolbar__picker-popper .el-month-table),
+:deep(.summary-toolbar__picker-popper .el-year-table),
+:deep(.summary-toolbar__picker-popper .el-picker-panel__icon-btn),
+:deep(.summary-toolbar__picker-popper .el-date-picker__header-label) {
+  font-size: 14px !important;
+}
+
 .summary-panel {
   padding: 0 10px 10px;
   height: 200px;
@@ -1255,8 +1530,11 @@ onUnmounted(() => {
 .right-column {
   display: grid;
   gap: 24px;
+  align-content: start;
+}
+.left-column {
+  height: 540px;
 }
-
 .board-panel {
   min-height: 258px;
 }
@@ -1267,6 +1545,7 @@ onUnmounted(() => {
 
 .board-panel--center {
   min-height: 540px;
+  height: 540px;
 }
 
 .chart-panel {
@@ -1281,8 +1560,59 @@ onUnmounted(() => {
   height: 218px;
 }
 
-.soc-chart-panel {
+.qhse-metric-tabs {
+  display: flex;
+  justify-content: left;
+  gap: 5px;
+  padding-left: 30px;
+}
+
+.qhse-metric-tab {
+  min-height: 24px;
+  padding: 5px 12px;
+  font-size: 13px;
+  font-weight: 700;
+  line-height: 1.35;
+  color: #4d6487;
+  text-align: center;
+  background: linear-gradient(180deg, rgb(255 255 255 / 86%) 0%, rgb(228 239 255 / 82%) 100%);
+  border: 1px solid rgb(118 167 238 / 28%);
+  border-radius: 14px;
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 92%),
+    0 10px 16px rgb(52 94 164 / 8%);
+  cursor: pointer;
+  transition:
+    transform 0.2s ease,
+    box-shadow 0.2s ease,
+    color 0.2s ease,
+    background 0.2s ease,
+    border-color 0.2s ease;
+}
+
+.qhse-metric-tab:hover {
+  transform: translateY(-1px);
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 94%),
+    0 12px 18px rgb(52 94 164 / 10%);
+}
+
+.qhse-metric-tab.is-active {
+  color: #fff;
+  background: linear-gradient(180deg, #63adff 0%, #347eea 100%);
+  border-color: rgb(62 122 223 / 76%);
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 18%),
+    0 12px 20px rgb(45 120 234 / 22%);
+}
+
+.qhse-trend-chart-panel {
   height: 220px;
+  // margin-top: 14px;
+}
+
+.soc-chart-panel {
+  height: 280px;
 }
 
 .panel-title--center {
@@ -1305,6 +1635,7 @@ onUnmounted(() => {
 
 .risk-hazard-block {
   padding: 0 24px 5px;
+  margin-top: 18px;
 }
 
 .risk-hazard-block__title {
@@ -1518,6 +1849,29 @@ onUnmounted(() => {
 
 :deep(.el-input__inner) {
   font-size: 14px !important;
+  border: none !important;
+  font-size: 14px !important;
+}
+
+:deep(.el-input__wrapper) {
+  background: transparent !important;
+  border: none !important;
+  font-size: 14px !important;
+}
+
+:deep(.el-input) {
+  border: none !important;
+  outline: none !important;
+  font-size: 14px !important;
+}
+
+:deep(.el-date-editor.el-input, .el-date-editor.el-input__inner) {
+  border: none !important;
+  font-size: 14px !important;
+}
+
+:deep(.el-date-editor.is-active) {
+  border-color: #ff6a00 !important;
 }
 
 .certificate-pagination {
@@ -1537,4 +1891,15 @@ onUnmounted(() => {
     font-size: calc(14px * var(--kb-scale, 1));
   }
 }
+
+.summary-toolbar__picker {
+  :deep(*) {
+    font-size: 14px !important;
+  }
+
+  :deep(input) {
+    font-size: 14px !important;
+    line-height: 14px !important;
+  }
+}
 </style>

+ 348 - 41
src/views/pms/qhse/socSummary/index.vue

@@ -4,23 +4,40 @@
     <DeptTree @node-click="handleDeptNodeClick" v-model:collapsed="isLeftContentCollapsed" />
 
     <el-col :span="isLeftContentCollapsed ? 24 : 20" :xs="24">
-      <ContentWrap style="border: none">
+      <div class="soc-summary-panel">
+        <div class="soc-summary-chart">
+          <div class="soc-summary-chart__header">
+            <div class="soc-summary-chart__title">
+              {{ currentDrilldownKey ? `${currentDrilldownKey}分类统计` : 'SOC卡分类统计' }}
+            </div>
+            <el-button v-if="currentDrilldownKey" link type="primary" @click="resetDrilldown">
+              返回总览
+            </el-button>
+          </div>
+          <Echart
+            ref="socChartRef"
+            :options="socChartOption"
+            :height="240"
+            @chart-click="handleChartClick"
+          />
+        </div>
+      </div>
+
+      <ContentWrap style="border: none; margin-top: 5px">
         <!-- 搜索工作栏 -->
         <el-form
           class="-mb-15px"
           :model="queryParams"
           ref="queryFormRef"
           :inline="true"
-          label-width="68px"
-        >
+          label-width="auto">
           <el-form-item label="姓名" prop="userName">
             <el-input
               v-model="queryParams.userName"
               placeholder="请输入姓名"
               clearable
               @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
+              class="!w-120px" />
           </el-form-item>
 
           <el-form-item label="队伍名称" prop="deptName">
@@ -29,8 +46,18 @@
               placeholder="请输入队伍名称"
               clearable
               @keyup.enter="handleQuery"
-              class="!w-240px"
-            />
+              class="!w-120px" />
+          </el-form-item>
+
+          <el-form-item label="观察日期" prop="observationDate">
+            <el-date-picker
+              v-model="queryParams.observationDate"
+              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-160px" />
           </el-form-item>
           <el-form-item>
             <el-button @click="handleQuery"
@@ -43,8 +70,7 @@
               type="primary"
               plain
               @click="openForm('create')"
-              v-hasPermi="['rq:iot-soc-summary:create']"
-            >
+              v-hasPermi="['rq:iot-soc-summary:create']">
               <Icon icon="ep:plus" class="mr-5px" /> 新增
             </el-button>
             <el-button
@@ -52,8 +78,7 @@
               plain
               @click="handleExport"
               :loading="exportLoading"
-              v-hasPermi="['rq:iot-soc-summary:export']"
-            >
+              v-hasPermi="['rq:iot-soc-summary:export']">
               <Icon icon="ep:download" class="mr-5px" /> 导出
             </el-button>
           </el-form-item>
@@ -61,8 +86,13 @@
       </ContentWrap>
 
       <!-- 列表 -->
-      <ContentWrap style="border: none">
-        <zm-table :loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <ContentWrap style="border: none; margin-top: -10px">
+        <zm-table
+          :loading="loading"
+          :data="list"
+          height="35vh"
+          :stripe="true"
+          :show-overflow-tooltip="true">
           <zm-table-column :label="t('monitor.serial')" width="70" align="center">
             <template #default="scope">
               {{ scope.$index + 1 }}
@@ -77,8 +107,7 @@
             prop="observationDate"
             :formatter="dateFormatter"
             类型名称
-            width="180px"
-          />
+            width="180px" />
 
           <zm-table-column label="类型名称" align="center" prop="className" min-width="120" />
           <zm-table-column label="姓名" align="center" prop="userName" />
@@ -90,8 +119,7 @@
             align="center"
             prop="createTime"
             :formatter="dateFormatter"
-            width="180px"
-          />
+            width="180px" />
 
           <zm-table-column label="操作" align="center" min-width="180px" fixed="right" action>
             <template #default="scope">
@@ -101,16 +129,14 @@
                 link
                 type="primary"
                 @click="openForm('update', scope.row.id)"
-                v-hasPermi="['rq:iot-soc-summary:update']"
-              >
+                v-hasPermi="['rq:iot-soc-summary:update']">
                 编辑
               </el-button>
               <el-button
                 link
                 type="danger"
                 @click="handleDelete(scope.row.id)"
-                v-hasPermi="['rq:iot-soc-summary:delete']"
-              >
+                v-hasPermi="['rq:iot-soc-summary:delete']">
                 删除
               </el-button>
             </template>
@@ -121,8 +147,7 @@
           :total="total"
           v-model:page="queryParams.pageNo"
           v-model:limit="queryParams.pageSize"
-          @pagination="getList"
-        />
+          @pagination="getList" />
 
         <!-- <div id="docx-viewer" class="docx-viewer-container"></div> -->
         <teleport to="body">
@@ -130,8 +155,7 @@
             <div
               v-if="dialogVisible"
               class="custom-dialog-overlay"
-              @click.self="dialogVisible = false"
-            >
+              @click.self="dialogVisible = false">
               <div class="custom-dialog">
                 <div class="custom-dialog-header">
                   <span class="custom-dialog-title">文档预览</span>
@@ -160,11 +184,19 @@ import download from '@/utils/download'
 import { IotSocSummaryApi } from '@/api/pms/qhse/index'
 import IotSocSummaryForm from './IotSocSummaryForm.vue'
 import DeptTree from '@/views/system/user/DeptTree2.vue'
-import { renderAsync } from 'docx-preview'
 import { Close } from '@element-plus/icons-vue'
+import type { EChartsOption } from 'echarts'
+import { Echart } from '@/components/Echart'
 
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 const { ZmTable, ZmTableColumn } = useTableComponents()
+import { useUserStore } from '@/store/modules/user'
+const userStore = useUserStore()
+
+type SummaryItem = Record<string, number>
+type ChildMap = Record<string, SummaryItem[]>
+
+const DRILLDOWN_KEYS = ['个人防护', '规范操作', '规范指挥', '人员位置', '作业场所'] as const
 
 /** SOC卡汇总 列表 */
 defineOptions({ name: 'IotSocSummary' })
@@ -176,6 +208,7 @@ const loading = ref(true) // 列表的加载中
 const list = ref([]) // 列表的数据
 const total = ref(0) // 列表的总页数
 const isLeftContentCollapsed = ref(false)
+const socChartRef = ref<{ resize: () => void } | null>(null)
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
@@ -208,6 +241,8 @@ const getList = async () => {
 const handleDeptNodeClick = async (row) => {
   queryParams.deptId = row.id
   queryParams.pageNo = 1
+  resetDrilldown()
+  await getStatic()
   getList()
 }
 
@@ -215,6 +250,7 @@ const handleDeptNodeClick = async (row) => {
 const handleQuery = () => {
   queryParams.pageNo = 1
   getList()
+  getStatic()
 }
 
 /** 重置按钮操作 */
@@ -257,21 +293,6 @@ const handleExport = async () => {
   }
 }
 
-const view = async (id) => {
-  dialogVisible.value = true
-  // 等待弹框渲染完成后再加载文档
-
-  // 等待弹框渲染完成后再加载文档
-  await nextTick()
-  const container = document.getElementById('docx-viewer')
-  if (container) {
-    container.innerHTML = '' // 清空之前的内容
-    const res = await IotSocSummaryApi.previewIotSocSummary(id)
-
-    await renderAsync(res, container)
-  }
-}
-
 const downloadSOC = async (row) => {
   const res = await IotSocSummaryApi.downloadIotSocSummary(row.id)
   download.excel(
@@ -280,9 +301,231 @@ const downloadSOC = async (row) => {
   )
 }
 
+const child = ref<ChildMap>({})
+const totalData = ref<SummaryItem[]>([])
+const currentDrilldownKey = ref('')
+
+const totalChartData = computed(() => {
+  return (totalData.value as SummaryItem[]).map((item) => {
+    const [name, value] = Object.entries(item || {})[0] || ['', 0]
+    return {
+      name,
+      value: Number(value) || 0
+    }
+  })
+})
+
+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
+    }
+  })
+})
+
+const chartColorMap: Record<string, string> = {
+  不安全: '#f56c6c',
+  个人防护: '#0000ff',
+  规范操作: '#810081',
+  规范指挥: '#d09cff',
+  人员位置: '#00ffff',
+  作业场所: '#ff00ff'
+}
+
+const hexToRgba = (hex: string, alpha: number) => {
+  const normalizedHex = hex.replace('#', '')
+  const safeHex =
+    normalizedHex.length === 3
+      ? normalizedHex
+          .split('')
+          .map((char) => char + char)
+          .join('')
+      : normalizedHex
+
+  const red = Number.parseInt(safeHex.slice(0, 2), 16)
+  const green = Number.parseInt(safeHex.slice(2, 4), 16)
+  const blue = Number.parseInt(safeHex.slice(4, 6), 16)
+
+  return `rgba(${red}, ${green}, ${blue}, ${alpha})`
+}
+
+const getBarBaseColor = (name: string, isDrilldown: boolean) => {
+  if (isDrilldown && currentDrilldownKey.value) {
+    return chartColorMap[currentDrilldownKey.value] || '#67c23a'
+  }
+
+  return chartColorMap[name] || '#409eff'
+}
+
+const getBarItemStyle = (name: string, isDrilldown: boolean) => {
+  const baseColor = getBarBaseColor(name, isDrilldown)
+
+  return {
+    color: {
+      type: 'linear',
+      x: 0,
+      y: 0,
+      x2: 0,
+      y2: 1,
+      colorStops: [
+        { offset: 0, color: hexToRgba(baseColor, 0.98) },
+        { offset: 0.55, color: baseColor },
+        { offset: 1, color: hexToRgba(baseColor, 0.72) }
+      ]
+    },
+    borderRadius: [10, 10, 3, 3],
+    shadowBlur: 14,
+    shadowColor: hexToRgba(baseColor, 0.28),
+    shadowOffsetY: 6,
+    borderColor: hexToRgba(baseColor, 0.9),
+    borderWidth: 1
+  }
+}
+
+const socChartOption = computed<EChartsOption>(() => {
+  const isDrilldown = !!currentDrilldownKey.value
+  const sourceData = isDrilldown ? childChartData.value : totalChartData.value
+
+  return {
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: 'rgba(64, 158, 255, 0.08)'
+        }
+      },
+      backgroundColor: 'rgba(18, 34, 61, 0.9)',
+      borderWidth: 0,
+      padding: [10, 14],
+      textStyle: {
+        color: '#f5f7fa'
+      }
+    },
+    grid: {
+      left: 24,
+      right: 24,
+      top: 36,
+      bottom: 40,
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      data: sourceData.map((item) => item.name),
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        interval: 0,
+        rotate: isDrilldown ? 18 : 0,
+        color: '#5b6475',
+        fontSize: 12
+      },
+      axisLine: {
+        lineStyle: {
+          color: '#d7deea'
+        }
+      }
+    },
+    yAxis: {
+      type: 'value',
+      minInterval: 1,
+      axisLabel: {
+        color: '#7a8599'
+      },
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      splitLine: {
+        lineStyle: {
+          color: 'rgba(148, 163, 184, 0.2)',
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        type: 'bar',
+        barMaxWidth: isDrilldown ? 42 : 56,
+        showBackground: true,
+        backgroundStyle: {
+          color: 'rgba(22, 136, 245, 0.055)'
+        },
+        emphasis: {
+          focus: 'series',
+          itemStyle: {
+            shadowBlur: 20,
+            shadowOffsetY: 8
+          }
+        },
+        data: sourceData.map((item) => ({
+          value: item.value,
+          itemStyle: getBarItemStyle(item.name, isDrilldown)
+        })),
+        label: {
+          show: true,
+          position: 'top',
+          distance: 10,
+          color: '#303133',
+          fontSize: 12,
+          fontWeight: 600
+        }
+      }
+    ]
+  }
+})
+
+async function getStatic() {
+  if (queryParams.deptId) {
+    const res = await IotSocSummaryApi.getSocSummaryStatistics({
+      deptId: queryParams.deptId,
+      // 日期
+      observationDate: queryParams.observationDate
+    })
+    child.value = res.child
+    totalData.value = res.total
+  } else {
+    queryParams.deptId = userStore.user.deptId
+    const res = await IotSocSummaryApi.getSocSummaryStatistics({
+      deptId: queryParams.deptId,
+      // 日期
+      observationDate: queryParams.observationDate
+    })
+    child.value = res.child
+    totalData.value = res.total
+  }
+}
+
+const handleChartClick = (params: any) => {
+  const name = params?.name
+  if (!name || currentDrilldownKey.value) return
+  if (DRILLDOWN_KEYS.some((item) => item === name)) {
+    currentDrilldownKey.value = name
+  }
+}
+
+const resetDrilldown = () => {
+  currentDrilldownKey.value = ''
+}
+
 /** 初始化 **/
+watch(isLeftContentCollapsed, async () => {
+  await nextTick()
+  setTimeout(() => {
+    socChartRef.value?.resize()
+  }, 320)
+})
+
 onMounted(() => {
   getList()
+  getStatic()
 })
 </script>
 
@@ -394,4 +637,68 @@ onMounted(() => {
 .dialog-fade-leave-to .custom-dialog {
   transform: scale(0.9);
 }
+
+.soc-summary-panel {
+  position: relative;
+  overflow: hidden;
+  margin-bottom: 5px;
+  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%),
+    linear-gradient(135deg, #fdfefe 0%, #f3f8ff 68%, #eef4ff 100%);
+  box-shadow: 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%),
+    repeating-linear-gradient(
+      135deg,
+      rgba(255, 255, 255, 0.08) 0,
+      rgba(255, 255, 255, 0.08) 1px,
+      transparent 1px,
+      transparent 14px
+    );
+  pointer-events: none;
+}
+
+.soc-summary-chart {
+  position: relative;
+  z-index: 1;
+  width: 100%;
+  padding: 4px;
+}
+
+.soc-summary-chart__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 6px;
+}
+
+.soc-summary-chart__title {
+  position: relative;
+  padding-left: 14px;
+  font-size: 16px;
+  font-weight: 700;
+  color: #1f2a44;
+  letter-spacing: 0.5px;
+}
+
+.soc-summary-chart__title::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 50%;
+  width: 6px;
+  height: 18px;
+  border-radius: 999px;
+  background: linear-gradient(180deg, #409eff 0%, #9b8cff 100%);
+  transform: translateY(-50%);
+  box-shadow: 0 4px 10px rgba(64, 158, 255, 0.35);
+}
 </style>