Sfoglia il codice sorgente

feat(rd-inventory-safety): 调整瑞都看板库存看板

Zimo 4 giorni fa
parent
commit
97c0feb2d1

+ 537 - 7
src/views/pms/stat/rdkb/rd-Inventory-safety.vue

@@ -1,18 +1,548 @@
-<script lang="ts" setup></script>
+<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 activeTitle = computed(() => (activePanel.value === 'distribution' ? '存货分布' : '积压趋势'))
+
+const inventoryData: InventoryItem[] = [
+  {
+    project: '东营中心仓',
+    mayInventoryAmount: 7.35,
+    yearBeginningBacklog: 10.13,
+    mayBacklogAmount: 7.02
+  },
+  {
+    project: '吉尔吉斯',
+    mayInventoryAmount: 23.82,
+    yearBeginningBacklog: 23.82,
+    mayBacklogAmount: 25.18
+  },
+  {
+    project: '新疆-连油',
+    mayInventoryAmount: 21.9,
+    yearBeginningBacklog: 177.02,
+    mayBacklogAmount: 19.55
+  },
+  {
+    project: '青海',
+    mayInventoryAmount: 52.03,
+    yearBeginningBacklog: 177.97,
+    mayBacklogAmount: 53.46
+  },
+  {
+    project: '东部-陕蒙',
+    mayInventoryAmount: 5.63,
+    yearBeginningBacklog: 84.75,
+    mayBacklogAmount: 7.65
+  },
+  {
+    project: '东部-胜利',
+    mayInventoryAmount: 8.82,
+    yearBeginningBacklog: 59.9,
+    mayBacklogAmount: 10.67
+  },
+  {
+    project: '西南压裂',
+    mayInventoryAmount: 41.78,
+    yearBeginningBacklog: 174.18,
+    mayBacklogAmount: 27.72
+  },
+  {
+    project: '西南连油',
+    mayInventoryAmount: 24.13,
+    yearBeginningBacklog: 90.01,
+    mayBacklogAmount: 23.37
+  },
+  {
+    project: '伊拉克(国内)',
+    mayInventoryAmount: 0.1,
+    yearBeginningBacklog: 190.71,
+    mayBacklogAmount: 0.1
+  },
+  {
+    project: '大庆工具',
+    mayInventoryAmount: 0.12,
+    yearBeginningBacklog: 1.79,
+    mayBacklogAmount: 0.05
+  },
+  {
+    project: '利比亚(国内)',
+    mayInventoryAmount: 0,
+    yearBeginningBacklog: 31.33,
+    mayBacklogAmount: 0
+  }
+]
+
+function formatProjectName(value: string) {
+  return value.replace(/项目$/, '')
+}
+
+function formatAmount(value: number) {
+  return Number(value || 0).toFixed(2)
+}
+
+function getInventoryChartLayout(chartRef: Ref<HTMLDivElement | undefined>) {
+  const { clientWidth = 0, clientHeight = 0 } = chartRef.value ?? {}
+  const compact = clientHeight > 0 && (clientHeight < 210 || clientWidth < 520)
+
+  return {
+    compact,
+    trendGridTop: compact ? 34 : 40,
+    trendGridLeft: compact ? 36 : 44,
+    trendGridRight: compact ? 8 : 16,
+    trendGridBottom: compact ? 12 : 12,
+    legendTop: compact ? 0 : 4,
+    legendRight: compact ? 2 : 6,
+    legendItemSize: compact ? 8 : 10,
+    legendGap: compact ? 8 : 12,
+    legendFontSize: compact ? 10 : 12,
+    axisFontSize: compact ? 10 : 10,
+    yAxisLabelMargin: compact ? 6 : 8,
+    xAxisLabelRotate: compact ? 45 : 36,
+    distributionTitleTop: compact ? 6 : 4,
+    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,
+    pieLabelNameFontSize: compact ? 11 : 13,
+    pieLabelNameLineHeight: compact ? 18 : 22,
+    pieLabelValueFontSize: compact ? 16 : 20,
+    pieLabelValueLineHeight: compact ? 22 : 30,
+    barWidth: compact ? 10 : 12,
+    barGap: compact ? '10%' : '8%',
+    barCategoryGap: compact ? '44%' : '38%',
+    labelDistance: compact ? 4 : 7,
+    labelFontSize: compact ? 10 : 12
+  }
+}
+
+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.yearBeginningBacklog, 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
+    ],
+    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: `总库存金额\n${formatAmount(backlogTotal)} 万元`,
+        left: '71.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: '总库存金额',
+        type: 'pie',
+        radius: layout.distributionPieRadius,
+        center: ['73%', layout.distributionPieCenterY],
+        minAngle: 5,
+        data: data
+          .filter((item) => item.yearBeginningBacklog > 0)
+          .map((item) => ({
+            name: item.project,
+            value: item.yearBeginningBacklog
+          }))
+      }
+    ]
+  }
+}
+
+function getTrendOption(data: InventoryItem[]): echarts.EChartsOption {
+  const projects = data.map((item) => formatProjectName(item.project))
+  const maxBacklog = Math.max(
+    ...data.map((item) => Math.max(item.mayInventoryAmount, item.mayBacklogAmount)),
+    1
+  )
+  const backlogAxisMax = Math.ceil((maxBacklog * 1.15) / 50) * 50
+  const layout = getInventoryChartLayout(trendChartRef)
+  const barLabel = {
+    show: false,
+    position: 'top' as any,
+    distance: layout.labelDistance,
+    color: THEME.text.strong,
+    fontSize: layout.labelFontSize,
+    fontWeight: 700,
+    fontFamily: FONT_FAMILY,
+    formatter(params: any) {
+      const value = Number(params.value)
+
+      return formatAmount(value)
+    }
+  }
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: layout.trendGridTop,
+      left: layout.trendGridLeft,
+      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
+        }
+      },
+      ['2026期初', '5月总积压']
+    ),
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      valueFormatter(value: number) {
+        return `${formatAmount(value)}万元`
+      }
+    }),
+    xAxis: {
+      type: 'category',
+      data: projects,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY,
+        interval: 0,
+        margin: layout.yAxisLabelMargin,
+        rotate: layout.xAxisLabelRotate
+      }
+    },
+    yAxis: {
+      type: 'value',
+      max: backlogAxisMax,
+      splitNumber: 4,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: layout.axisFontSize,
+        fontFamily: FONT_FAMILY,
+        formatter(value: number) {
+          return `${value}`
+        }
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    series: [
+      {
+        name: '2026期初',
+        type: 'bar',
+        data: data.map((item) => item.mayBacklogAmount),
+        barWidth: layout.barWidth,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        barMinHeight: 0,
+        showBackground: false,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: 999
+        },
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: true
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.blue.bg,
+          color: new echarts.graphic.LinearGradient(0, 1, 0, 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.mayInventoryAmount),
+        barWidth: layout.barWidth,
+        barMinHeight: 0,
+        barGap: layout.barGap,
+        barCategoryGap: layout.barCategoryGap,
+        showBackground: false,
+        backgroundStyle: {
+          color: THEME.split,
+          borderRadius: 999
+        },
+        label: barLabel,
+        labelLayout: {
+          hideOverlap: true
+        },
+        itemStyle: {
+          shadowBlur: 10,
+          shadowColor: THEME.color.orange.bg,
+          color: new echarts.graphic.LinearGradient(0, 1, 0, 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('rdkb:resize', resizeCharts)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeCharts)
+  window.removeEventListener('rdkb:resize', resizeCharts)
+  destroyCharts()
+})
+</script>
 
 
 <template>
 <template>
   <div class="panel flex flex-col">
   <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>
       </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>
-    <div ref="chartRef" class="flex-1 min-h-0"></div>
   </div>
   </div>
 </template>
 </template>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
 @import url('@/styles/kb.scss');
 @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>
 </style>

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

@@ -141,7 +141,7 @@ function getInventoryChartLayout(chartRef: Ref<HTMLDivElement | undefined>) {
     axisFontSize: compact ? 10 : 12,
     axisFontSize: compact ? 10 : 12,
     yAxisLabelWidth: compact ? 96 : 132,
     yAxisLabelWidth: compact ? 96 : 132,
     yAxisLabelMargin: compact ? 8 : 12,
     yAxisLabelMargin: compact ? 8 : 12,
-    distributionTitleTop: compact ? 6 : 18,
+    distributionTitleTop: compact ? 6 : 4,
     distributionTitleFontSize: compact ? 12 : 14,
     distributionTitleFontSize: compact ? 12 : 14,
     distributionTitleLineHeight: compact ? 14 : 16,
     distributionTitleLineHeight: compact ? 14 : 16,
     distributionPieRadius: compact ? ['36%', '54%'] : ['48%', '68%'],
     distributionPieRadius: compact ? ['36%', '54%'] : ['48%', '68%'],