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

feat(pms-stat): 完善瑞恒/瑞鹰看板图表与跳转联动

- 瑞恒看板新增资产价值卡片
  - 新增 assetValue 独立组件
  - 使用静态资产价值数据展示原值、本期计提、累计折旧、净值
  - 统一使用 kb.ts 中的 THEME、ANIMATION、tooltip 和图表风格
  - 在 y 轴展示单位“资产价值(万元)”

- 瑞恒看板新增存货分布/积压趋势卡片
  - 新增 inventorySituation 独立组件
  - 根据静态项目数据展示 5 月底余额、5 月底积压、年初积压
  - 支持“分布/趋势”切换
  - 分布视图使用双环图展示余额和积压占比
  - 趋势视图使用横向柱状图对比年初积压与 5 月底积压
  - 接入 rhkb:resize 保证看板缩放后图表正常重绘

- 调整瑞恒看板首页布局
  - 将首页网格扩展为设备状态、设备类别、设备利用率、安全天数、注气量、资产价值、运维成本、库存情况等卡片组合
  - 新增 equipment-rate 卡片占位
  - 调整注气量图表 y 轴单位为“万方”
  - 优化设备状态图例尺寸,适配看板小卡片空间

- 增加瑞恒运维成本图表跳转
  - 点击运维成本图表跳转至 /report-statistics/operational-costs
  - 跳转时携带当前图表首尾日期作为 createTime
  - 运维成本报表页支持从路由 query 初始化 createTime
  - 保留直接进入报表页时默认按本年筛选的行为

- 修复瑞恒日报统计看板首次渲染空白
  - 在切换到看板视图时等待图表容器挂载并具备有效宽高后再初始化 ECharts
  - 补充 requestAnimationFrame 调度和 resize,避免从看板跳转后首屏图表不显示

- 优化瑞鹰看板图表展示
  - 调整首页部分卡片顺序
  - 优化汇总卡片排序
  - 同步存货分布饼图外侧标签展示效果
  - 缩小通用 legend 默认字号,提升小卡片可读性
Zimo 1 долоо хоног өмнө
parent
commit
fd0dec17a9

+ 1 - 1
src/utils/kb.ts

@@ -122,7 +122,7 @@ export function createLegend(extra: Partial<LegendOption> = {}, data: string[] =
     data,
     textStyle: {
       color: THEME.text.regular,
-      fontSize: 13,
+      fontSize: 10,
       fontFamily: FONT_FAMILY
     },
     ...extra

+ 28 - 2
src/views/pms/iotrhdailyreport/components/DailyStatistics.vue

@@ -275,7 +275,7 @@ const handleSelectTab = (val: '表格' | '看板') => {
   nextTick(() => {
     currentTab.value = val
     setTimeout(() => {
-      render()
+      if (val === '看板') scheduleChartRender()
     })
   })
 }
@@ -291,6 +291,7 @@ watch(
 const chartRef = ref<HTMLDivElement | null>(null)
 let chart: ECharts | null = null
 let chartContainerEl: HTMLDivElement | null = null
+let chartRenderRaf = 0
 
 const xAxisData = ref<string[]>([])
 
@@ -636,7 +637,30 @@ const render = () => {
   chart?.setOption(getChartOption(), { notMerge: true, lazyUpdate: true })
 }
 
+const scheduleChartRender = (attempt = 0) => {
+  cancelAnimationFrame(chartRenderRaf)
+
+  chartRenderRaf = requestAnimationFrame(() => {
+    chartRenderRaf = 0
+
+    if (currentTab.value !== '看板') return
+
+    const chartEl = chartRef.value
+
+    if (!chartEl || !chartEl.clientWidth || !chartEl.clientHeight) {
+      if (attempt < 20) scheduleChartRender(attempt + 1)
+      return
+    }
+
+    render()
+    requestAnimationFrame(() => {
+      chart?.resize()
+    })
+  })
+}
+
 onUnmounted(() => {
+  cancelAnimationFrame(chartRenderRaf)
   window.removeEventListener('resize', resizer)
   chart?.dispose()
   chart = null
@@ -645,7 +669,9 @@ onUnmounted(() => {
 
 const handleQuery = () => {
   getChart().then(() => {
-    render()
+    if (currentTab.value === '看板') {
+      scheduleChartRender()
+    }
   })
   getList()
   getTotal()

+ 9 - 4
src/views/pms/stat/rhkb.vue

@@ -9,6 +9,9 @@ import todayGas from './rhkb/todayGas.vue'
 import historyGas from './rhkb/historyGas.vue'
 import deviceList from './rhkb/deviceList.vue'
 import rhsafeday from './rhkb/rhsafeday.vue'
+import assetValue from './rhkb/assetValue.vue'
+import equipmentRate from './rhkb/equipment-rate.vue'
+import inventorySituation from './rhkb/inventorySituation.vue'
 
 defineOptions({
   name: 'IotRhStatt'
@@ -105,11 +108,13 @@ onUnmounted(() => {
             <div class="kb-chart-grid">
               <deviceStatus class="kb-stage-card kb-stage-card--1" />
               <deviceType class="kb-stage-card kb-stage-card--2" />
-              <operation class="kb-stage-card kb-stage-card--3" />
+              <equipmentRate class="kb-stage-card kb-stage-card--3" />
               <rhsafeday class="kb-stage-card kb-stage-card--4" />
-              <!-- <orderTrend class="kb-stage-card kb-stage-card--5" /> -->
-              <todayGas class="kb-stage-card kb-stage-card--6" />
-              <historyGas class="kb-stage-card kb-stage-card--7" />
+              <todayGas class="kb-stage-card kb-stage-card--5" />
+              <historyGas class="kb-stage-card kb-stage-card--6" />
+              <assetValue class="kb-stage-card kb-stage-card--7" />
+              <operation class="kb-stage-card kb-stage-card--8" />
+              <inventorySituation class="kb-stage-card kb-stage-card--9" />
             </div>
           </div>
 

+ 190 - 0
src/views/pms/stat/rhkb/assetValue.vue

@@ -0,0 +1,190 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import { ANIMATION, CHART_RENDERER, createTooltip, FONT_FAMILY, THEME } from '@/utils/kb'
+
+const chartRef = ref<HTMLDivElement>()
+let chart: echarts.ECharts | null = null
+
+const assetValueData = [
+  { name: '原值', value: 85000.77 },
+  { name: '本期计提', value: 512.61 },
+  { name: '累计折旧', value: 57722.06 },
+  { name: '净值', value: 27152.6 }
+]
+
+function getChartOption(): echarts.EChartsOption {
+  return {
+    ...ANIMATION,
+    color: [THEME.color.blue.line],
+    grid: {
+      ...THEME.grid,
+      top: 32,
+      right: 18,
+      bottom: 12,
+      left: 18
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number) {
+        return `${Number(value || 0).toLocaleString()} 万元`
+      }
+    }),
+    xAxis: {
+      type: 'category',
+      data: assetValueData.map((item) => item.name),
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: 14,
+        fontWeight: 500,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: '资产价值(万元)',
+      min: 0,
+      max: 90000,
+      splitNumber: 4,
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY
+      },
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: 12,
+        fontFamily: FONT_FAMILY,
+        formatter(value: number) {
+          return value.toLocaleString()
+        }
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        name: '瑞恒',
+        type: 'bar',
+        data: assetValueData.map((item) => item.value),
+        barWidth: 34,
+        showBackground: true,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: 999
+        },
+        itemStyle: {
+          borderRadius: [12, 12, 0, 0],
+          shadowBlur: 12,
+          shadowColor: THEME.color.blue.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: THEME.color.blue.light },
+            { offset: 0.5, color: THEME.color.blue.mid },
+            { offset: 1, color: THEME.color.blue.line }
+          ])
+        },
+        label: {
+          show: true,
+          position: 'top',
+          distance: 6,
+          color: THEME.color.blue.strong,
+          fontSize: 13,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          formatter(params: any) {
+            return `${assetValueData[params.dataIndex]?.value ?? params.value}`
+          }
+        }
+      }
+    ]
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+
+  if (chart) {
+    chart.dispose()
+  }
+
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  renderChart()
+}
+
+function renderChart() {
+  chart?.setOption(getChartOption(), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+}
+
+function destroyChart() {
+  chart?.dispose()
+  chart = null
+}
+
+onMounted(() => {
+  initChart()
+  window.addEventListener('resize', resizeChart)
+  window.addEventListener('rhkb:resize', resizeChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('rhkb:resize', resizeChart)
+  destroyChart()
+})
+</script>
+
+<template>
+  <div class="panel asset-value-panel">
+    <div class="panel-title asset-value-title">
+      <div class="icon-decorator">
+        <span></span>
+        <span></span>
+      </div>
+      资产价值
+    </div>
+    <div ref="chartRef" class="asset-value-chart"></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.asset-value-panel {
+  display: flex;
+  min-height: 0;
+  flex-direction: column;
+}
+
+.asset-value-chart {
+  width: 100%;
+  min-height: 0;
+  flex: 1;
+}
+</style>

+ 1 - 1
src/views/pms/stat/rhkb/deviceStatus.vue

@@ -27,7 +27,7 @@ function getChartOption(data: ChartItem[]): echarts.EChartsOption {
       }
     }),
     legend: createLegend(
-      { bottom: 10, itemWidth: 12, itemHeight: 12 },
+      { bottom: 4, itemWidth: 8, itemHeight: 8 },
       data.map((item) => item.name)
     ),
     series: [

+ 18 - 0
src/views/pms/stat/rhkb/equipment-rate.vue

@@ -0,0 +1,18 @@
+<script lang="ts" setup></script>
+
+<template>
+  <div class="panel flex flex-col">
+    <div class="panel-title">
+      <div class="icon-decorator">
+        <span></span>
+        <span></span>
+      </div>
+      设备利用率
+    </div>
+    <div ref="chartRef" class="flex-1 min-h-0"></div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+</style>

+ 1 - 1
src/views/pms/stat/rhkb/historyGas.vue

@@ -56,7 +56,7 @@ function getChartOption(data: ChartItem[]): echarts.EChartsOption {
     },
     yAxis: {
       type: 'value',
-      name: '累计注气量()',
+      name: '累计注气量(万方)',
       nameTextStyle: {
         color: THEME.text.regular,
         fontSize: 13,

+ 507 - 0
src/views/pms/stat/rhkb/inventorySituation.vue

@@ -0,0 +1,507 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  CHART_RENDERER,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  THEME
+} from '@/utils/kb'
+
+type ActivePanel = 'distribution' | 'trend'
+
+type InventoryItem = {
+  project: string
+  mayInventoryAmount: number
+  yearBeginningBacklog: number
+  mayBacklogAmount: number
+}
+
+const activePanel = ref<ActivePanel>('distribution')
+const distributionChartRef = ref<HTMLDivElement>()
+const trendChartRef = ref<HTMLDivElement>()
+
+let distributionChart: echarts.ECharts | null = null
+let trendChart: echarts.ECharts | null = null
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  { label: '分布', value: 'distribution' },
+  { label: '趋势', value: 'trend' }
+]
+
+const yuanToWan = (value: number) => Number((value / 10000).toFixed(2))
+
+const inventoryData: InventoryItem[] = [
+  {
+    project: '东营库',
+    mayInventoryAmount: yuanToWan(2921827.56),
+    yearBeginningBacklog: yuanToWan(1644059.52),
+    mayBacklogAmount: yuanToWan(1436126.52)
+  },
+  {
+    project: '川庆',
+    mayInventoryAmount: yuanToWan(491050.94),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: yuanToWan(308801.56)
+  },
+  {
+    project: '南美',
+    mayInventoryAmount: yuanToWan(2491993.31),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0
+  },
+  {
+    project: '非洲',
+    mayInventoryAmount: yuanToWan(2276836.24),
+    yearBeginningBacklog: yuanToWan(175144.46),
+    mayBacklogAmount: yuanToWan(391697.89)
+  },
+  {
+    project: '中东',
+    mayInventoryAmount: yuanToWan(127300.64),
+    yearBeginningBacklog: yuanToWan(14784.43),
+    mayBacklogAmount: yuanToWan(103461.07)
+  },
+  {
+    project: '中亚',
+    mayInventoryAmount: yuanToWan(188389.42),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0
+  },
+  {
+    project: '塔河库',
+    mayInventoryAmount: yuanToWan(17175342.26),
+    yearBeginningBacklog: yuanToWan(9890389.23),
+    mayBacklogAmount: yuanToWan(9261631.2)
+  },
+  {
+    project: '塔里木',
+    mayInventoryAmount: yuanToWan(4599437.38),
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: yuanToWan(890890.45)
+  },
+  {
+    project: '吐哈库',
+    mayInventoryAmount: yuanToWan(2342089.06),
+    yearBeginningBacklog: yuanToWan(1097925.36),
+    mayBacklogAmount: yuanToWan(1190879.22)
+  }
+]
+
+const activeTitle = computed(() => (activePanel.value === 'distribution' ? '存货分布' : '积压趋势'))
+
+function formatAmount(value: number) {
+  return Number(value || 0).toFixed(2)
+}
+
+function getChartLayout(chartRef: Ref<HTMLDivElement | undefined>) {
+  const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    compact,
+    distributionTitleTop: compact ? 6 : 18,
+    distributionTitleFontSize: compact ? 12 : 14,
+    distributionTitleLineHeight: compact ? 14 : 16,
+    distributionPieRadius: compact ? ['36%', '54%'] : ['48%', '68%'],
+    distributionPieCenterY: compact ? '62%' : '57%',
+    distributionLegendBottom: compact ? 0 : 10,
+    distributionLegendItemSize: compact ? 9 : 13,
+    distributionLegendGap: compact ? 8 : 14,
+    distributionLegendFontSize: compact ? 10 : 14,
+    trendGridTop: compact ? 10 : 32,
+    trendGridRight: compact ? 12 : THEME.grid.right,
+    trendGridBottom: compact ? 2 : THEME.grid.bottom,
+    legendTop: compact ? 0 : 4,
+    legendRight: compact ? 2 : 6,
+    legendItemSize: compact ? 9 : 12,
+    legendGap: compact ? 10 : 16,
+    legendFontSize: compact ? 11 : 14,
+    axisFontSize: compact ? 10 : 12,
+    yAxisLabelWidth: compact ? 72 : 96,
+    yAxisLabelMargin: compact ? 8 : 12,
+    barWidth: compact ? 8 : 12,
+    barGap: compact ? '12%' : '5%',
+    barCategoryGap: compact ? '46%' : '36%',
+    labelDistance: compact ? 4 : 7,
+    labelFontSize: compact ? 10 : 12
+  }
+}
+
+function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
+  const layout = getChartLayout(distributionChartRef)
+  const inventoryTotal = data.reduce((total, item) => total + item.mayInventoryAmount, 0)
+  const backlogTotal = data.reduce((total, item) => total + item.mayBacklogAmount, 0)
+
+  return {
+    ...ANIMATION,
+    color: [
+      THEME.color.blue.line,
+      THEME.color.orange.line,
+      THEME.color.green.line,
+      THEME.color.red.line,
+      THEME.color.blue.mid,
+      THEME.color.orange.mid,
+      THEME.color.green.mid,
+      THEME.color.red.mid,
+      THEME.color.blue.light
+    ],
+    tooltip: createTooltip({
+      trigger: 'item',
+      formatter(params: any) {
+        return `${params.seriesName}<br/>${params.marker}${params.name}:${formatAmount(
+          params.value
+        )}万元<br/>占比:${params.percent}%`
+      }
+    }),
+    title: [
+      {
+        text: `5月底余额\n${formatAmount(inventoryTotal)} 万元`,
+        left: '26.5%',
+        top: layout.distributionTitleTop,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: layout.distributionTitleFontSize,
+          fontWeight: 700,
+          lineHeight: layout.distributionTitleLineHeight,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      {
+        text: `5月底积压\n${formatAmount(backlogTotal)} 万元`,
+        left: '72.5%',
+        top: layout.distributionTitleTop,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: layout.distributionTitleFontSize,
+          fontWeight: 700,
+          lineHeight: layout.distributionTitleLineHeight,
+          fontFamily: FONT_FAMILY
+        }
+      }
+    ],
+    legend: createLegend({
+      type: 'scroll',
+      bottom: layout.distributionLegendBottom,
+      left: 10,
+      right: 10,
+      itemWidth: layout.distributionLegendItemSize,
+      itemHeight: layout.distributionLegendItemSize,
+      itemGap: layout.distributionLegendGap,
+      textStyle: {
+        color: THEME.text.regular,
+        fontSize: layout.distributionLegendFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    }),
+    series: [
+      {
+        name: '5月底余额',
+        type: 'pie',
+        radius: layout.distributionPieRadius,
+        center: ['27%', layout.distributionPieCenterY],
+        minAngle: 5,
+        data: data.map((item) => ({
+          name: item.project,
+          value: item.mayInventoryAmount
+        }))
+      },
+      {
+        name: '5月底积压',
+        type: 'pie',
+        radius: layout.distributionPieRadius,
+        center: ['73%', layout.distributionPieCenterY],
+        minAngle: 5,
+        data: data
+          .filter((item) => item.mayBacklogAmount > 0)
+          .map((item) => ({
+            name: item.project,
+            value: item.mayBacklogAmount
+          }))
+      }
+    ]
+  }
+}
+
+function getTrendOption(data: InventoryItem[]): echarts.EChartsOption {
+  const layout = getChartLayout(trendChartRef)
+  const maxBacklog = Math.max(
+    ...data.map((item) => Math.max(item.yearBeginningBacklog, item.mayBacklogAmount)),
+    1
+  )
+  const backlogAxisMax = Math.ceil((maxBacklog * 1.15) / 100) * 100
+  const barLabel = {
+    show: true,
+    position: 'right' as any,
+    distance: layout.labelDistance,
+    color: THEME.text.strong,
+    fontSize: layout.labelFontSize,
+    fontWeight: 700,
+    fontFamily: FONT_FAMILY,
+    formatter(params: any) {
+      return formatAmount(Number(params.value))
+    }
+  }
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: layout.trendGridTop,
+      right: layout.trendGridRight,
+      bottom: layout.trendGridBottom
+    },
+    color: [THEME.color.blue.line, THEME.color.orange.line],
+    legend: createLegend(
+      {
+        top: layout.legendTop,
+        right: layout.legendRight,
+        itemWidth: layout.legendItemSize,
+        itemHeight: layout.legendItemSize,
+        itemGap: layout.legendGap,
+        textStyle: {
+          color: THEME.text.regular,
+          fontSize: layout.legendFontSize,
+          fontWeight: 600,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      ['年初积压', '5月底积压']
+    ),
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number) {
+        return `${formatAmount(value)}万元`
+      }
+    }),
+    xAxis: {
+      type: 'value',
+      max: backlogAxisMax,
+      splitNumber: 4,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: data.map((item) => item.project),
+      inverse: true,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY,
+        margin: layout.yAxisLabelMargin,
+        width: layout.yAxisLabelWidth,
+        overflow: 'break',
+        align: 'right'
+      }
+    },
+    series: [
+      {
+        name: '年初积压',
+        type: 'bar',
+        data: data.map((item) => item.yearBeginningBacklog),
+        barWidth: layout.barWidth,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: false
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.blue.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.blue.light },
+            { offset: 0.55, color: THEME.color.blue.mid },
+            { offset: 1, color: THEME.color.blue.line }
+          ])
+        }
+      },
+      {
+        name: '5月底积压',
+        type: 'bar',
+        data: data.map((item) => item.mayBacklogAmount),
+        barWidth: layout.barWidth,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: false
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.orange.bg,
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.orange.light },
+            { offset: 0.55, color: THEME.color.orange.mid },
+            { offset: 1, color: THEME.color.orange.line }
+          ])
+        }
+      }
+    ]
+  }
+}
+
+function initChart(
+  chartRef: Ref<HTMLDivElement | undefined>,
+  chart: echarts.ECharts | null,
+  option: echarts.EChartsOption
+) {
+  if (!chartRef.value) return chart
+
+  chart?.dispose()
+  const nextChart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  nextChart.setOption(option, true)
+
+  return nextChart
+}
+
+function renderDistributionChart() {
+  distributionChart?.setOption(getDistributionOption(inventoryData), true)
+}
+
+function renderTrendChart() {
+  trendChart?.setOption(getTrendOption(inventoryData), true)
+}
+
+function initDistributionChart() {
+  distributionChart = initChart(
+    distributionChartRef,
+    distributionChart,
+    getDistributionOption(inventoryData)
+  )
+}
+
+function initTrendChart() {
+  trendChart = initChart(trendChartRef, trendChart, getTrendOption(inventoryData))
+}
+
+function resizeCharts() {
+  distributionChart?.resize()
+  trendChart?.resize()
+  renderDistributionChart()
+  renderTrendChart()
+}
+
+function destroyCharts() {
+  distributionChart?.dispose()
+  trendChart?.dispose()
+  distributionChart = null
+  trendChart = null
+}
+
+watch(activePanel, (value) => {
+  nextTick(() => {
+    if (value === 'distribution') {
+      if (!distributionChart) initDistributionChart()
+      renderDistributionChart()
+    } else {
+      if (!trendChart) initTrendChart()
+      renderTrendChart()
+    }
+    resizeCharts()
+  })
+})
+
+onMounted(() => {
+  initDistributionChart()
+  window.addEventListener('resize', resizeCharts)
+  window.addEventListener('rhkb:resize', resizeCharts)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeCharts)
+  window.removeEventListener('rhkb:resize', resizeCharts)
+  destroyCharts()
+})
+</script>
+
+<template>
+  <div class="panel flex flex-col">
+    <div class="panel-title flex items-center justify-between">
+      <div class="kb-panel-title-text flex items-center">
+        <div class="icon-decorator">
+          <span></span>
+          <span></span>
+        </div>
+        {{ activeTitle }}
+      </div>
+      <el-segmented
+        v-model="activePanel"
+        :options="panelOptions"
+        size="small"
+        class="inventory-switch" />
+    </div>
+    <div class="flex-1 min-h-0">
+      <div
+        v-show="activePanel === 'distribution'"
+        ref="distributionChartRef"
+        class="inventory-chart"></div>
+      <div v-show="activePanel === 'trend'" ref="trendChartRef" class="inventory-chart"></div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.inventory-switch {
+  --el-segmented-item-selected-color: #03409b;
+  --el-segmented-item-selected-bg-color: rgb(255 255 255 / 86%);
+  --el-segmented-bg-color: rgb(31 91 184 / 10%);
+  --el-segmented-item-hover-bg-color: rgb(255 255 255 / 56%);
+
+  min-height: calc(26px * var(--kb-scale, 1));
+  padding: calc(2px * var(--kb-scale, 1));
+  border: 1px solid rgb(31 91 184 / 12%);
+  transform: translateY(calc(-2px * var(--kb-scale, 1)));
+
+  :deep(.el-segmented__item) {
+    min-height: calc(22px * var(--kb-scale, 1));
+    padding: 0 calc(8px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.inventory-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 0;
+}
+</style>

+ 29 - 0
src/views/pms/stat/rhkb/operation.vue

@@ -1,5 +1,6 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
+import dayjs from 'dayjs'
 import {
   ANIMATION,
   ChartItem,
@@ -12,8 +13,10 @@ import { IotStatApi } from '@/api/pms/stat'
 
 const chartData = ref<ChartItem[]>([])
 
+const router = useRouter()
 const chartRef = ref<HTMLDivElement>()
 let chart: echarts.ECharts | null = null
+let chartClickBound = false
 
 function getChartOption(data: ChartItem[]): echarts.EChartsOption {
   const names = data.map((item) => item.name)
@@ -129,6 +132,26 @@ function initChart() {
   renderChart()
 }
 
+function getChartDateRange() {
+  const startDate = dayjs(chartData.value[0]?.name)
+  const endDate = dayjs(chartData.value[chartData.value.length - 1]?.name)
+  if (!startDate.isValid() || !endDate.isValid()) return undefined
+
+  return [
+    startDate.startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+    endDate.endOf('day').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
+function handleChartClick() {
+  router.push({
+    path: '/report-statistics/operational-costs',
+    query: {
+      createTime: getChartDateRange()
+    }
+  })
+}
+
 function renderChart() {
   if (!chart) return
 
@@ -143,6 +166,10 @@ function resizeChart() {
 
 function destroyChart() {
   if (chart) {
+    if (chartClickBound) {
+      chart.getZr().off('click', handleChartClick)
+      chartClickBound = false
+    }
     chart.dispose()
     chart = null
   }
@@ -165,6 +192,8 @@ async function loadChart() {
 
 onMounted(() => {
   initChart()
+  chart?.getZr().on('click', handleChartClick)
+  chartClickBound = true
   loadChart()
   window.addEventListener('resize', resizeChart)
   window.addEventListener('rhkb:resize', resizeChart)

+ 1 - 1
src/views/pms/stat/rhkb/todayGas.vue

@@ -86,7 +86,7 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
     },
     yAxis: {
       type: 'value',
-      name: '当日注气量()',
+      name: '当日注气量(万方)',
       nameTextStyle: {
         color: THEME.text.regular,
         fontSize: 13,

+ 2 - 2
src/views/pms/stat/rykb.vue

@@ -105,10 +105,10 @@ onUnmounted(() => {
           <div v-if="activePage === 'home'" class="kb-home-page">
             <rysummary class="kb-stage-card kb-stage-card--1" />
             <div class="kb-chart-grid">
-              <rydeviceStatus class="kb-stage-card kb-stage-card--2" />
+              <equipmentCategory class="kb-stage-card kb-stage-card--5" />
               <zjStatsSwitch class="kb-stage-card kb-stage-card--3" />
               <safeday class="kb-stage-card kb-stage-card--4" />
-              <equipmentCategory class="kb-stage-card kb-stage-card--5" />
+              <rydeviceStatus class="kb-stage-card kb-stage-card--2" />
               <xjwork class="kb-stage-card kb-stage-card--6" />
               <exceptionPrompt class="kb-stage-card kb-stage-card--7" />
               <rydeviceList class="kb-stage-card kb-stage-card--8 kb-stage-card--list" />

+ 11 - 58
src/views/pms/stat/rykb/inventory-situation.vue

@@ -166,39 +166,6 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
   const layout = getInventoryChartLayout(distributionChartRef)
   const inventoryTotal = data.reduce((total, item) => total + item.mayInventoryAmount, 0)
   const backlogTotal = data.reduce((total, item) => total + item.mayBacklogAmount, 0)
-  const inventoryPieData = data.map((item) => ({
-    name: formatProjectName(item.project),
-    value: item.mayInventoryAmount
-  }))
-  const backlogPieData = data
-    .filter((item) => item.mayBacklogAmount > 0)
-    .map((item) => ({
-      name: formatProjectName(item.project),
-      value: item.mayBacklogAmount
-    }))
-  const pieLabel = {
-    show: false,
-    position: 'center' as any,
-    formatter(params: any) {
-      return `{name|${params.name}}\n{value|${formatAmount(params.value)} 万元}`
-    },
-    rich: {
-      name: {
-        color: THEME.text.regular,
-        fontSize: layout.pieLabelNameFontSize,
-        fontWeight: 500,
-        lineHeight: layout.pieLabelNameLineHeight,
-        fontFamily: FONT_FAMILY
-      },
-      value: {
-        color: THEME.text.strong,
-        fontSize: layout.pieLabelValueFontSize,
-        fontWeight: 700,
-        lineHeight: layout.pieLabelValueLineHeight,
-        fontFamily: FONT_FAMILY
-      }
-    }
-  }
 
   return {
     ...ANIMATION,
@@ -270,37 +237,23 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['27%', layout.distributionPieCenterY],
         minAngle: 5,
-        avoidLabelOverlap: true,
-        label: pieLabel,
-        emphasis: {
-          label: {
-            show: true
-          },
-          itemStyle: {
-            shadowBlur: 14,
-            shadowColor: THEME.color.blue.shadow
-          }
-        },
-        data: inventoryPieData
+        data: data.map((item) => ({
+          name: item.project,
+          value: item.mayInventoryAmount
+        }))
       },
       {
-        name: '5月底积压库存金额',
+        name: '5月底积压',
         type: 'pie',
         radius: layout.distributionPieRadius,
         center: ['73%', layout.distributionPieCenterY],
         minAngle: 5,
-        avoidLabelOverlap: true,
-        label: pieLabel,
-        emphasis: {
-          label: {
-            show: true
-          },
-          itemStyle: {
-            shadowBlur: 14,
-            shadowColor: THEME.color.blue.shadow
-          }
-        },
-        data: backlogPieData
+        data: data
+          .filter((item) => item.mayBacklogAmount > 0)
+          .map((item) => ({
+            name: item.project,
+            value: item.mayBacklogAmount
+          }))
       }
     ]
   }

+ 22 - 22
src/views/pms/stat/rykb/rysummary.vue

@@ -19,6 +19,27 @@ type CardKey =
 type CardConfig = SummaryCardConfig<CardKey>
 
 const cardConfigs: CardConfig[] = [
+  {
+    key: 'zj',
+    title: '钻井总进尺(m)',
+    icon: 'i-solar:ruler-bold',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
+  {
+    key: 'xj',
+    title: '修井总完成井数',
+    icon: 'i-mdi:wrench-check-outline',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
+  {
+    key: 'utilizationRate',
+    title: '累计设备利用率(%)',
+    icon: 'i-material-symbols:device-hub-rounded',
+    accent: THEME.color.green.strong,
+    glow: THEME.color.green.glow
+  },
   {
     key: 'device',
     title: '设备数',
@@ -60,7 +81,7 @@ const cardConfigs: CardConfig[] = [
     icon: 'i-solar:shield-warning-linear',
     accent: THEME.color.orange.strong,
     glow: THEME.color.orange.glow
-  },
+  }
   // {
   //   key: 'byfinished',
   //   title: '已保养',
@@ -75,27 +96,6 @@ const cardConfigs: CardConfig[] = [
   //   accent: THEME.color.green.strong,
   //   glow: THEME.color.green.glow
   // },
-  {
-    key: 'zj',
-    title: '钻井总进尺(m)',
-    icon: 'i-solar:ruler-bold',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
-  {
-    key: 'xj',
-    title: '修井总完成井数',
-    icon: 'i-mdi:wrench-check-outline',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
-  {
-    key: 'utilizationRate',
-    title: '累计设备利用率(%)',
-    icon: 'i-material-symbols:device-hub-rounded',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  }
 ]
 
 function createDefaultCardState(): Record<CardKey, CardStateItem> {

+ 50 - 31
src/views/report-statistics/costs.vue

@@ -26,10 +26,30 @@ const timeOptions: { label: string; value: TimeType }[] = [
   { label: '日', value: 'day' }
 ]
 
-const activeTimeType = ref<TimeType | undefined>('year')
+const route = useRoute()
+
+const getRouteCreateTime = (): [string, string] | undefined => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    const values = createTime.filter((item): item is string => typeof item === 'string')
+    return values.length === 2 ? [values[0], values[1]] : undefined
+  }
+
+  if (typeof createTime === 'string') {
+    const values = createTime.split(',').filter(Boolean)
+    return values.length === 2 ? [values[0], values[1]] : undefined
+  }
+
+  return undefined
+}
+
+const routeCreateTime = getRouteCreateTime()
+const activeTimeType = ref<TimeType | undefined>(routeCreateTime ? undefined : 'year')
 const query = ref<Query>({
   pageNo: 1,
-  pageSize: 10
+  pageSize: 10,
+  createTime: routeCreateTime
 })
 
 const handleTimeChange = (type: TimeType, init = false) => {
@@ -183,7 +203,9 @@ const loadList = useDebounceFn(async function () {
 }, 500)
 
 onMounted(() => {
-  handleTimeChange('year', true)
+  if (!query.value.createTime) {
+    handleTimeChange('year', true)
+  }
 })
 
 watch(
@@ -194,6 +216,17 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => route.query.createTime,
+  () => {
+    const createTime = getRouteCreateTime()
+    if (!createTime) return
+
+    activeTimeType.value = undefined
+    query.value.createTime = createTime
+  }
+)
+
 function selectType(type: string | undefined) {
   query.value.type = type
   query.value.pageNo = 1
@@ -229,16 +262,14 @@ const handleChange = () => {
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
-  >
+    class="grid grid-cols-[auto_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
     <DeptTreeSelect
       :top-id="156"
       :deptId="156"
       v-model="query.deptId"
       :init-select="false"
       :show-title="false"
-      class="row-span-2"
-    />
+      class="row-span-2" />
     <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
     <div class="grid grid-rows-[1fr_32px] gap-4">
       <div class="grid grid-cols-3 gap-4" v-loading="dataLoading">
@@ -248,8 +279,7 @@ const handleChange = () => {
           :key="item.key"
           class="flex flex-col items-center gap-y-4 rounded-lg shadow p-4 transition-transform hover:scale-105 duration-500"
           :class="{ [item.class.bg1]: true, 'scale-105': item.type === query.type }"
-          @click="selectType(item.type)"
-        >
+          @click="selectType(item.type)">
           <!-- 头部:图标 + 标题 -->
           <div class="flex items-center gap-x-3">
             <div class="rounded-2 p-2" :class="[item.class.text, item.class.bg2]">
@@ -267,8 +297,7 @@ const handleChange = () => {
             :end-val="item.value"
             :decimals="2"
             suffix="元"
-            :duration="1000"
-          >
+            :duration="1000">
             <!-- 插槽内容:当数据为空或0时的显示 (根据 count-to 组件的具体实现决定是否显示) -->
             <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
           </count-to>
@@ -279,16 +308,14 @@ const handleChange = () => {
             <div
               v-show="item.hb"
               class="flex items-center gap-x-1 text-xs font-medium"
-              :class="item.class.text"
-            >
+              :class="item.class.text">
               <!-- 动态图标:大于等于0向上,小于0向下 -->
               <div
                 :class="
                   item.hb >= 0
                     ? 'i-material-symbols:arrow-warm-up-rounded'
                     : 'i-material-symbols:arrow-cool-down-rounded'
-                "
-              ></div>
+                "></div>
 
               <span class="vertical-middle"> {{ item.hb > 0 ? '+' + item.hb : item.hb }}% </span>
               <span>环比</span>
@@ -296,16 +323,14 @@ const handleChange = () => {
             <div
               v-show="item.tb"
               class="flex items-center gap-x-1 text-xs font-medium"
-              :class="item.class.text"
-            >
+              :class="item.class.text">
               <!-- 动态图标:大于等于0向上,小于0向下 -->
               <div
                 :class="
                   item.tb >= 0
                     ? 'i-material-symbols:arrow-warm-up-rounded'
                     : 'i-material-symbols:arrow-cool-down-rounded'
-                "
-              ></div>
+                "></div>
 
               <span class="vertical-middle"> {{ item.tb > 0 ? '+' + item.tb : item.tb }}% </span>
               <span>同比</span>
@@ -327,15 +352,13 @@ const handleChange = () => {
             @clear="handleClear"
             @change="handleChange"
             :clearable="false"
-            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          />
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" />
           <el-button-group size="default">
             <el-button
               v-for="item in timeOptions"
               :key="item.value"
               :type="activeTimeType === item.value ? 'primary' : ''"
-              @click="handleTimeChange(item.value)"
-            >
+              @click="handleTimeChange(item.value)">
               {{ item.label }}
             </el-button>
           </el-button-group>
@@ -347,8 +370,7 @@ const handleChange = () => {
             plain
             type="success"
             @click="handleExport"
-            :loading="exportLoading"
-          >
+            :loading="exportLoading">
             <Icon icon="ep:download" class="mr-5px" /> 导出
           </el-button>
         </div>
@@ -364,15 +386,13 @@ const handleChange = () => {
               :width="width"
               :height="height"
               :max-height="height"
-              show-border
-            >
+              show-border>
               <ZmTableColumn
                 label="序号"
                 type="index"
                 :width="70"
                 fixed="left"
-                hide-in-column-settings
-              />
+                hide-in-column-settings />
               <ZmTableColumn label="日期" prop="date" min-width="140" />
               <ZmTableColumn label="类别" prop="type" min-width="120" />
               <ZmTableColumn label="设备编号" prop="deviceCode" min-width="150" />
@@ -395,8 +415,7 @@ const handleChange = () => {
           :total="total"
           layout="total, sizes, prev, pager, next, jumper"
           @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
-        />
+          @current-change="handleCurrentChange" />
       </div>
     </div>
   </div>