Просмотр исходного кода

feat: 完善瑞都看板设备状态与分类图表

- 新增设备状态环形饼图,展示正常、带病运行、故障数量及占比
- 新增设备分类横向柱状图,按设备类型展示分类数量
- 新增设备使用状态环形饼图,基于截图数据展示施工、封存、闲置等状态
- 统一图表 resize 逻辑,适配 rdkb 看板缩放事件
- 优化图表 legend、tooltip、布局间距和看板面板展示效果
Zimo 1 день назад
Родитель
Сommit
9fcaaca136

+ 1 - 1
.env.local

@@ -4,7 +4,7 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径  http://192.168.188.200:48080  https://iot.deepoil.cc  http://172.26.0.56:48080
-VITE_BASE_URL='https://iot.deepoil.cc'
+VITE_BASE_URL='https://172.21.0.198'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 2 - 0
src/views/pms/stat/rdkb/rd-Inventory-safety.vue

@@ -229,6 +229,7 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['27%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: { show: false },
         data: data.map((item) => ({
           name: item.project,
           value: item.mayInventoryAmount
@@ -240,6 +241,7 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['73%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: { show: false },
         data: data
           .filter((item) => item.yearBeginningBacklog > 0)
           .map((item) => ({

+ 213 - 2
src/views/pms/stat/rdkb/rd-device-category.vue

@@ -1,4 +1,209 @@
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import { ANIMATION, CHART_RENDERER, createTooltip, FONT_FAMILY, THEME } from '@/utils/kb'
+
+interface DeviceCategoryItem {
+  name: string
+  count: number
+}
+
+const chartRef = ref<HTMLDivElement>()
+let chart: echarts.ECharts | null = null
+
+const categoryData: DeviceCategoryItem[] = [
+  { name: '压裂泵车', count: 75 },
+  { name: '连油主车', count: 36 },
+  { name: '其他', count: 35 },
+  { name: '小车', count: 32 },
+  { name: '井控装备', count: 22 },
+  { name: '连油辅助设备', count: 19 },
+  { name: '货车', count: 17 },
+  { name: '四川瑞都设备', count: 16 },
+  { name: '卡车', count: 16 },
+  { name: '供液橇', count: 12 }
+]
+
+function getChartLayout() {
+  const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    gridTop: compact ? 20 : 26,
+    gridRight: compact ? 32 : 40,
+    gridBottom: compact ? 22 : 28,
+    gridLeft: compact ? 12 : 12,
+    barWidth: compact ? 10 : 14,
+    axisFontSize: compact ? 10 : 12,
+    labelFontSize: compact ? 10 : 12
+  }
+}
+
+function formatValue(value: number) {
+  return Number(value || 0).toLocaleString('en-US')
+}
+
+function getChartOption(): echarts.EChartsOption {
+  const layout = getChartLayout()
+  const values = categoryData.map((item) => item.count)
+  const maxValue = Math.max(...values, 1)
+  const xAxisMax = Math.ceil((maxValue * 1.12) / 10) * 10
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: layout.gridTop,
+      right: layout.gridRight,
+      bottom: layout.gridBottom,
+      left: layout.gridLeft
+    },
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      formatter(params: any[]) {
+        const first = params[0]
+        const row = categoryData[first?.dataIndex ?? 0]
+        if (!row) return ''
+
+        return [
+          `<div style="min-width:132px;font-weight:700;color:#24364f;margin-bottom:6px;">${row.name}</div>`,
+          `<div style="display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>设备数量</span><b style="color:#1f5bb8;">${row.count} 台</b></div>`
+        ].join('')
+      }
+    }),
+    xAxis: {
+      type: 'value',
+      name: '台',
+      max: xAxisMax,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontFamily: FONT_FAMILY,
+        formatter(value: number) {
+          return formatValue(value)
+        }
+      },
+      nameTextStyle: {
+        color: '#4c6c9b',
+        fontSize: layout.axisFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: 'rgba(31, 91, 184, 0.24)',
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: categoryData.map((item) => item.name),
+      inverse: true,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: '#102a43',
+        fontSize: layout.axisFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY,
+        width: 80,
+        overflow: 'truncate'
+      }
+    },
+    series: [
+      {
+        name: '设备数量',
+        type: 'bar',
+        data: values,
+        barWidth: layout.barWidth,
+        showBackground: true,
+        backgroundStyle: {
+          color: 'rgba(31, 91, 184, 0.08)',
+          borderRadius: 999
+        },
+        itemStyle: {
+          borderRadius: 999,
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.blue.light },
+            { offset: 0.58, color: THEME.color.blue.mid },
+            { offset: 1, color: THEME.color.blue.line }
+          ])
+        },
+        label: {
+          show: true,
+          position: 'right',
+          distance: 8,
+          color: THEME.text.strong,
+          fontSize: layout.labelFontSize,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          formatter(params: any) {
+            return `${formatValue(params.value)}`
+          }
+        },
+        emphasis: {
+          itemStyle: {
+            shadowBlur: 16,
+            shadowColor: THEME.color.blue.shadow
+          }
+        }
+      }
+    ]
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  renderChart()
+}
+
+function renderChart() {
+  chart?.setOption(getChartOption(), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+  renderChart()
+}
+
+function destroyChart() {
+  chart?.dispose()
+  chart = null
+}
+
+onMounted(() => {
+  initChart()
+  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">
@@ -9,10 +214,16 @@
       </div>
       设备分类
     </div>
-    <div ref="chartRef" class="flex-1 min-h-0"></div>
+    <div ref="chartRef" class="device-category-chart"></div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
+
+.device-category-chart {
+  width: 100%;
+  min-height: 0;
+  flex: 1;
+}
 </style>

+ 142 - 3
src/views/pms/stat/rdkb/rd-device-status.vue

@@ -1,7 +1,127 @@
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import { ANIMATION, CHART_RENDERER, createTooltip, FONT_FAMILY, THEME } from '@/utils/kb'
+
+interface DeviceStatusItem {
+  name: string
+  value: number
+}
+
+const chartData: DeviceStatusItem[] = [
+  {
+    name: '正常',
+    value: 269
+  },
+  {
+    name: '带病运行',
+    value: 1
+  },
+  {
+    name: '故障',
+    value: 6
+  }
+]
+
+const chartRef = ref<HTMLDivElement>()
+let chart: echarts.ECharts | null = null
+
+function getChartLayout() {
+  const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    pieRadius: compact ? ['42%', '62%'] : ['48%', '68%'],
+    pieCenterY: compact ? '50%' : '50%',
+    legendBottom: compact ? 0 : 8,
+    legendItemSize: compact ? 9 : 13,
+    legendGap: compact ? 10 : 18,
+    legendFontSize: compact ? 10 : 14
+  }
+}
+
+function getChartOption(): echarts.EChartsOption {
+  const layout = getChartLayout()
+
+  return {
+    ...ANIMATION,
+    color: [THEME.color.blue.line, THEME.color.orange.line, THEME.color.red.line],
+    tooltip: createTooltip({
+      trigger: 'item',
+      formatter(params: any) {
+        return `${params.marker}${params.name}<br/>数量:${params.value} 台<br/>占比:${params.percent}%`
+      }
+    }),
+    legend: {
+      type: 'scroll',
+      bottom: layout.legendBottom,
+      left: 'center',
+      itemWidth: layout.legendItemSize,
+      itemHeight: layout.legendItemSize,
+      itemGap: layout.legendGap,
+      textStyle: {
+        color: THEME.text.regular,
+        fontSize: layout.legendFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      },
+      formatter(name: string) {
+        const item = chartData.find((status) => status.name === name)
+        return item ? `${name}  ${item.value}台` : name
+      }
+    },
+    series: [
+      {
+        name: '设备状态',
+        type: 'pie',
+        radius: layout.pieRadius,
+        center: ['50%', layout.pieCenterY],
+        minAngle: 5,
+        label: { show: false },
+        data: chartData
+      }
+    ]
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  renderChart()
+}
+
+function renderChart() {
+  chart?.setOption(getChartOption(), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+  renderChart()
+}
+
+function destroyChart() {
+  chart?.dispose()
+  chart = null
+}
+
+onMounted(() => {
+  initChart()
+  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">
+  <div class="panel device-status-panel flex flex-col">
     <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
@@ -9,10 +129,29 @@
       </div>
       设备状态
     </div>
-    <div ref="chartRef" class="flex-1 min-h-0"></div>
+    <div class="device-status-body">
+      <div ref="chartRef" class="device-status-chart"></div>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
+
+.device-status-panel {
+  overflow: hidden;
+}
+
+.device-status-body {
+  display: flex;
+  min-height: 0;
+  flex: 1;
+  padding: calc(10px * var(--kb-scale, 1)) calc(16px * var(--kb-scale, 1))
+    calc(14px * var(--kb-scale, 1));
+}
+
+.device-status-chart {
+  min-height: 0;
+  flex: 1;
+}
 </style>

+ 161 - 3
src/views/pms/stat/rdkb/rd-use-status.vue

@@ -1,7 +1,146 @@
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import { ANIMATION, CHART_RENDERER, createTooltip, FONT_FAMILY, THEME } from '@/utils/kb'
+
+interface UseStatusItem {
+  name: string
+  value: number
+}
+
+const statusData: UseStatusItem[] = [
+  { name: '动迁', value: 0 },
+  { name: '施工', value: 162 },
+  { name: '维修中', value: 4 },
+  { name: '完工', value: 4 },
+  { name: '待维修', value: 2 },
+  { name: '封存', value: 61 },
+  { name: '驻地待命', value: 6 },
+  { name: '待命', value: 16 },
+  { name: '准备', value: 0 },
+  { name: '现场待命', value: 1 },
+  { name: '待保养', value: 0 },
+  { name: '闲置', value: 19 },
+  { name: '观察使用', value: 1 },
+  { name: '报废', value: 0 }
+]
+
+const chartRef = ref<HTMLDivElement>()
+let chart: echarts.ECharts | null = null
+
+const chartData = computed(() => statusData.filter((item) => item.value > 0))
+
+function getChartLayout() {
+  const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    pieRadius: compact ? ['38%', '58%'] : ['44%', '64%'],
+    pieCenterX: compact ? '30%' : '31%',
+    pieCenterY: compact ? '50%' : '50%',
+    legendRight: compact ? 8 : 18,
+    legendTop: compact ? 34 : 40,
+    legendItemSize: compact ? 9 : 13,
+    legendGap: compact ? 8 : 12,
+    legendFontSize: compact ? 10 : 14
+  }
+}
+
+function getChartOption(): echarts.EChartsOption {
+  const layout = getChartLayout()
+
+  return {
+    ...ANIMATION,
+    color: [
+      THEME.color.blue.line,
+      THEME.color.orange.line,
+      THEME.color.red.line,
+      THEME.color.green.line,
+      THEME.color.blue.mid,
+      '#2a97c7',
+      '#68c7dd',
+      '#8aa4ff',
+      '#78d08f',
+      '#f7c66b'
+    ],
+    tooltip: createTooltip({
+      trigger: 'item',
+      formatter(params: any) {
+        return `${params.marker}${params.name}<br/>数量:${params.value} 台<br/>占比:${params.percent}%`
+      }
+    }),
+    legend: {
+      type: 'plain',
+      orient: 'vertical',
+      top: layout.legendTop,
+      right: layout.legendRight,
+      width: '45%',
+      itemWidth: layout.legendItemSize,
+      itemHeight: layout.legendItemSize,
+      itemGap: layout.legendGap,
+      textStyle: {
+        color: THEME.text.regular,
+        fontSize: layout.legendFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      },
+      formatter(name: string) {
+        const item = chartData.value.find((status) => status.name === name)
+        return item ? `${name}  ${item.value}台` : name
+      }
+    },
+    series: [
+      {
+        name: '设备使用状态',
+        type: 'pie',
+        radius: layout.pieRadius,
+        center: [layout.pieCenterX, layout.pieCenterY],
+        minAngle: 5,
+        label: { show: false },
+        data: chartData.value
+      }
+    ]
+  }
+}
+
+function initChart() {
+  if (!chartRef.value) return
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  renderChart()
+}
+
+function renderChart() {
+  chart?.setOption(getChartOption(), true)
+}
+
+function resizeChart() {
+  chart?.resize()
+  renderChart()
+}
+
+function destroyChart() {
+  chart?.dispose()
+  chart = null
+}
+
+onMounted(() => {
+  initChart()
+  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">
+  <div class="panel use-status-panel flex flex-col">
     <div class="panel-title">
       <div class="icon-decorator">
         <span></span>
@@ -9,10 +148,29 @@
       </div>
       设备使用状态
     </div>
-    <div ref="chartRef" class="flex-1 min-h-0"></div>
+    <div class="use-status-body">
+      <div ref="chartRef" class="use-status-chart"></div>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
+
+.use-status-panel {
+  overflow: hidden;
+}
+
+.use-status-body {
+  display: flex;
+  min-height: 0;
+  flex: 1;
+  padding: calc(10px * var(--kb-scale, 1)) calc(16px * var(--kb-scale, 1))
+    calc(14px * var(--kb-scale, 1));
+}
+
+.use-status-chart {
+  min-height: 0;
+  flex: 1;
+}
 </style>

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

@@ -168,7 +168,7 @@ onUnmounted(() => {
         <span></span>
         <span></span>
       </div>
-      设备类别/状态
+      设备类别
     </div>
     <div ref="chartRef" class="flex-1 min-h-0"></div>
   </div>

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

@@ -205,6 +205,9 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['27%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: {
+          show: false
+        },
         data: data.map((item) => ({
           name: item.project,
           value: item.mayInventoryAmount
@@ -216,6 +219,9 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['73%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: {
+          show: false
+        },
         data: data
           .filter((item) => item.mayBacklogAmount > 0)
           .map((item) => ({

+ 6 - 0
src/views/pms/stat/rykb/inventory-situation.vue

@@ -237,6 +237,9 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['27%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: {
+          show: false
+        },
         data: data.map((item) => ({
           name: item.project,
           value: item.mayInventoryAmount
@@ -248,6 +251,9 @@ function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
         radius: layout.distributionPieRadius,
         center: ['73%', layout.distributionPieCenterY],
         minAngle: 5,
+        label: {
+          show: false
+        },
         data: data
           .filter((item) => item.mayBacklogAmount > 0)
           .map((item) => ({