Ver Fonte

feat(pms): 实现瑞鹰看板历史工作量图表

- 新增瑞鹰历史钻井、修井日报统计接口封装
  - /rq/stat/ry/hiZjDailyReports
  - /rq/stat/ry/hiXjDailyReports
- 实现历史工作量看板组件
  - 支持钻井、修井分段切换
  - 按接口返回的 xAxis 和 series 渲染柱状图
  - 复用瑞鹰看板现有 panel、segmented、ECharts 渐变柱样式
  - 支持 resize 和 rykb:resize 重绘
- 优化钻井历史工作量展示
  - 自动识别累计进尺和累计完井数两类指标
  - 进尺使用左侧 Y 轴,完井数使用右侧 Y 轴
  - 解决进尺数值过大导致完井数柱体不可见的问题
- 保持修井单指标图表使用单 Y 轴展示
Zimo há 5 dias atrás
pai
commit
842ca9d40c
2 ficheiros alterados com 341 adições e 6 exclusões
  1. 6 0
      src/api/pms/stat/index.ts
  2. 335 6
      src/views/pms/stat/rykb/historical-workload.vue

+ 6 - 0
src/api/pms/stat/index.ts

@@ -21,6 +21,12 @@ export const IotStatApi = {
   getRepairRigWork: async (params: any) => {
     return await request.get({ url: `/rq/stat/ry/dailyReport/` + params })
   },
+  getRyHiZjDailyReports: async () => {
+    return await request.get({ url: `/rq/stat/ry/hiZjDailyReports` })
+  },
+  getRyHiXjDailyReports: async () => {
+    return await request.get({ url: `/rq/stat/ry/hiXjDailyReports` })
+  },
   getOrderYwcb: async (params: any) => {
     return await request.get({ url: `/rq/stat/rh/ywcb/` + params })
   },

+ 335 - 6
src/views/pms/stat/rykb/historical-workload.vue

@@ -1,13 +1,322 @@
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  ChartData,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  formatSeriesName,
+  THEME
+} from '@/utils/kb'
+import { IotStatApi } from '@/api/pms/stat'
+
+type ActivePanel = 'zj' | 'xj'
+
+const activePanel = ref<ActivePanel>('zj')
+const chartData = ref<ChartData>({
+  xAxis: [],
+  series: []
+})
+const chartRef = ref<HTMLDivElement>()
+
+let chart: echarts.ECharts | null = null
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  {
+    label: '钻井',
+    value: 'zj'
+  },
+  {
+    label: '修井',
+    value: 'xj'
+  }
+]
+
+const activeTitle = computed(() =>
+  activePanel.value === 'zj' ? '钻井历史工作量' : '修井历史工作量'
+)
+
+function getBarStyle(color: (typeof THEME.color)[keyof typeof THEME.color]) {
+  return {
+    borderRadius: [12, 12, 0, 0],
+    shadowBlur: 12,
+    shadowColor: color.bg,
+    color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+      { offset: 0, color: color.light },
+      { offset: 0.55, color: color.mid },
+      { offset: 1, color: color.line }
+    ])
+  }
+}
+
+function formatValue(value: number | string) {
+  return Number(value || 0).toLocaleString('en-US')
+}
+
+function isFootageSeries(name: string) {
+  const displayName = formatSeriesName(name)
+
+  return name.includes('footage') || displayName.includes('进尺')
+}
+
+function isFinishedWellsSeries(name: string) {
+  const displayName = formatSeriesName(name)
+
+  return name.includes('finishedWells') || displayName.includes('完井')
+}
+
+function hasMixedUnits(data: ChartData) {
+  return (
+    data.series.some((item) => isFootageSeries(item.name)) &&
+    data.series.some((item) => isFinishedWellsSeries(item.name))
+  )
+}
+
+function getYAxisOption(data: ChartData): echarts.EChartsOption['yAxis'] {
+  const baseAxis = {
+    type: 'value' as const,
+    splitNumber: 4,
+    axisLine: {
+      show: false
+    },
+    axisTick: {
+      show: false
+    },
+    axisLabel: {
+      color: THEME.text.regular,
+      fontSize: 12,
+      fontFamily: FONT_FAMILY,
+      formatter(value: number) {
+        return formatValue(value)
+      }
+    },
+    splitLine: {
+      lineStyle: {
+        color: THEME.split,
+        type: 'dashed' as const
+      }
+    }
+  }
+
+  if (!hasMixedUnits(data)) {
+    return {
+      ...baseAxis,
+      name: data.series.some((item) => isFootageSeries(item.name))
+        ? '累计进尺(m)'
+        : '累计完井数(口)',
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY,
+        align: 'left'
+      }
+    }
+  }
+
+  return [
+    {
+      ...baseAxis,
+      name: '累计进尺(m)',
+      position: 'left',
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY,
+        align: 'left'
+      }
+    },
+    {
+      ...baseAxis,
+      name: '累计完井数(口)',
+      position: 'right',
+      nameTextStyle: {
+        color: THEME.text.regular,
+        fontSize: 13,
+        fontFamily: FONT_FAMILY,
+        align: 'right'
+      },
+      splitLine: {
+        show: false
+      }
+    }
+  ]
+}
+
+function getChartOption(data: ChartData): echarts.EChartsOption {
+  const xAxisData = data.xAxis || []
+  const seriesData = data.series || []
+  const colorList = [THEME.color.orange, THEME.color.blue, THEME.color.green]
+  const mixedUnits = hasMixedUnits(data)
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: 50,
+      right: mixedUnits ? 38 : THEME.grid.right,
+      bottom: 10
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number | string) {
+        return formatValue(value)
+      }
+    }),
+    legend: createLegend(
+      {
+        top: 4,
+        itemWidth: 12,
+        itemHeight: 12
+      },
+      seriesData.map((item) => formatSeriesName(item.name))
+    ),
+    xAxis: {
+      type: 'category',
+      data: xAxisData,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        interval: 0,
+        color: THEME.text.regular,
+        fontSize: 14,
+        fontFamily: FONT_FAMILY
+      }
+    },
+    yAxis: getYAxisOption(data),
+    series: seriesData.map((item, index) => {
+      const color = colorList[index % colorList.length]
+      const useRightAxis = mixedUnits && isFinishedWellsSeries(item.name)
+
+      return {
+        name: formatSeriesName(item.name),
+        type: 'bar',
+        yAxisIndex: useRightAxis ? 1 : 0,
+        data: item.data || [],
+        barWidth: 24,
+        barMaxWidth: 32,
+        showBackground: true,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: [12, 12, 0, 0]
+        },
+        itemStyle: getBarStyle(color),
+        label: {
+          show: true,
+          position: 'top',
+          distance: 10,
+          color: color.strong,
+          fontSize: 14,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          formatter(params: any) {
+            return Number(params.value) ? formatValue(params.value) : ''
+          }
+        },
+        emphasis: {
+          focus: 'series',
+          itemStyle: {
+            ...getBarStyle(color),
+            shadowColor: color.shadow,
+            shadowBlur: 18
+          }
+        }
+      }
+    })
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: 'svg'
+  })
+  renderChart()
+}
+
+function renderChart() {
+  if (!chart) return
+  chart.setOption(getChartOption(chartData.value), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+}
+
+function destroyChart() {
+  chart?.dispose()
+  chart = null
+}
+
+async function loadChart() {
+  try {
+    const res =
+      activePanel.value === 'zj'
+        ? await IotStatApi.getRyHiZjDailyReports()
+        : await IotStatApi.getRyHiXjDailyReports()
+
+    chartData.value = {
+      xAxis: (res?.xAxis || []).map((item) => `${item}`),
+      series: (res?.series || []).map((item) => ({
+        name: item.name,
+        data: item.data || []
+      }))
+    }
+    renderChart()
+  } catch (error) {
+    console.error(`获取${activeTitle.value}失败:`, error)
+    chartData.value = {
+      xAxis: [],
+      series: []
+    }
+    renderChart()
+  }
+}
+
+watch(activePanel, () => {
+  loadChart()
+})
+
+onMounted(() => {
+  initChart()
+  loadChart()
+  window.addEventListener('resize', resizeChart)
+  window.addEventListener('rykb:resize', resizeChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('rykb:resize', resizeChart)
+  destroyChart()
+})
+</script>
 
 <template>
   <div class="panel flex flex-col">
-    <div class="panel-title">
-      <div class="icon-decorator">
-        <span></span>
-        <span></span>
+    <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="historical-workload-switch" />
     </div>
     <div ref="chartRef" class="flex-1 min-h-0"></div>
   </div>
@@ -15,4 +324,24 @@
 
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
+
+.historical-workload-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;
+  }
+}
 </style>