Procházet zdrojové kódy

feat: 完成瑞都维保次数看板

- 新增瑞都近月维保次数统计接口
- 实现维保次数柱状图展示
- 支持系列名称、月份和次数格式化
- 增加看板点击跳转工单完成情况页面
Zimo před 1 dnem
rodič
revize
15ba4fa9de

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

@@ -257,6 +257,9 @@ export const IotStatApi = {
   getConstructionBriefing: async () => {
     return await request.get({ url: `/pms/iot-rd-daily-report/constructionBriefing` })
   },
+  getRdRecentWbcs: async () => {
+    return await request.get({ url: `/rq/stat/recent/wbcs/rd` })
+  },
 
   // 瑞都看板(新)
   // 获取ssoToken

+ 233 - 1
src/views/pms/stat/rdkb/rd-maintenance-times.vue

@@ -1,4 +1,236 @@
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  ChartData,
+  CHART_RENDERER,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  formatMonthLabel,
+  formatSeriesName,
+  THEME
+} from '@/utils/kb'
+import { IotStatApi } from '@/api/pms/stat'
+
+const chartData = ref<ChartData>({
+  xAxis: [],
+  series: []
+})
+
+const chartRef = ref<HTMLDivElement>()
+let chart: echarts.ECharts | null = null
+const router = useRouter()
+let chartClickBound = false
+
+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 formatCount(value: number | string) {
+  return Number(value || 0).toLocaleString('en-US')
+}
+
+function getChartOption(data: ChartData): echarts.EChartsOption {
+  const xAxisData = data.xAxis || []
+  const seriesData = data.series || []
+  const colorList = [THEME.color.green, THEME.color.orange]
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: 50,
+      bottom: 10
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number | string) {
+        return formatCount(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,
+        formatter(value: string) {
+          return formatMonthLabel(value)
+        }
+      }
+    },
+    yAxis: {
+      type: 'value',
+      name: '次数',
+      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 formatCount(value)
+        }
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    series: seriesData.map((item, index) => {
+      const color = colorList[index % colorList.length]
+
+      return {
+        name: formatSeriesName(item.name),
+        type: 'bar',
+        data: item.data || [],
+        barWidth: 22,
+        barMaxWidth: 30,
+        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) ? formatCount(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: CHART_RENDERER
+  })
+  chart.getZr().on('click', handleChartClick)
+  chartClickBound = true
+  renderChart()
+}
+
+function handleChartClick() {
+  router.push({
+    name: 'WorkOrderCompletion'
+  })
+}
+
+function renderChart() {
+  chart?.setOption(getChartOption(chartData.value), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+}
+
+function destroyChart() {
+  if (chart) {
+    if (chartClickBound) {
+      chart.getZr().off('click', handleChartClick)
+      chartClickBound = false
+    }
+    chart.dispose()
+    chart = null
+  }
+}
+
+async function loadChart() {
+  try {
+    const res = await IotStatApi.getRdRecentWbcs()
+    chartData.value = {
+      xAxis: (res?.xAxis || []).map((item) => `${item}`),
+      series: (res?.series || []).map((item) => ({
+        name: item.name,
+        data: (item.data || []).map((value) => Number(value || 0))
+      }))
+    }
+    renderChart()
+  } catch (error) {
+    console.error('获取维保次数失败:', error)
+    chartData.value = {
+      xAxis: [],
+      series: []
+    }
+    renderChart()
+  }
+}
+
+onMounted(() => {
+  initChart()
+  loadChart()
+  window.addEventListener('resize', resizeChart)
+  window.addEventListener('rdkb:resize', resizeChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  window.removeEventListener('rdkb:resize', resizeChart)
+  destroyChart()
+})
+</script>
 
 <template>
   <div class="panel flex flex-col">