瀏覽代碼

feat(pms-stat): 扩展瑞鹰看板统计模块

- 调整瑞鹰看板布局,将图表区域从 2 行扩展为 3 行 3 列,接入更多统计面板
- 新增设备类别统计面板,支持按类别/价值切换,并展示自有与非自有设备分布
- 新增异常警示面板,支持按本日、本月、本季、全年筛选生产异常 TOP 9
- 新增存货情况面板,支持存货分布与积压趋势切换展示
- 预留历史工作量数据面板入口
- 调整运行概况汇总卡片,新增累计设备利用率指标
- 为钻修进尺/修井工单汇总接口增加年度时间范围参数
- 新增设备利用率和 NPT 异常统计接口
- 优化瑞鹰看板卡片入场动画延迟和图表坐标轴标题对齐
Zimo 1 周之前
父節點
當前提交
bb3e644f3d

+ 1 - 1
.env.local

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

+ 8 - 2
src/api/pms/stat/index.ts

@@ -57,8 +57,11 @@ export const IotStatApi = {
   getInspectStatuss: async (params: any, dept: any) => {
     return await request.get({ url: `/rq/stat/inspect/statuss/` + dept, params })
   },
-  getInspectZjxjCount: async () => {
-    return await request.get({ url: `/rq/stat/home/ry/count/zjxj` })
+  getInspectZjxjCount: async (params?: any) => {
+    return await request.get({ url: `/rq/stat/home/ry/count/zjxj`, params })
+  },
+  getInspectRate: async (params: any) => {
+    return await request.get({ url: `/rq/stat/ry/device/totalUtilizationRate`, params })
   },
   getProject: async (params: any) => {
     return await request.get({ url: `/rq/stat/project/` + params })
@@ -162,6 +165,9 @@ export const IotStatApi = {
   getRyProductionBriefs: async (params: any) => {
     return await request.get({ url: `/pms/iot-ry-daily-report/productionBriefs`, params })
   },
+  getRyNptCount: async (params: any) => {
+    return await request.get({ url: `/rq/stat/ry/device/nptCount`, params })
+  },
   getRdTeamRate: async (params: any) => {
     return await request.get({ url: `rq/stat/rd/device/teamUtilizationRate`, params })
   },

+ 9 - 1
src/styles/kb.scss

@@ -614,7 +614,15 @@
 }
 
 .kb-stage-card--8 {
-  --panel-delay: 0.46s;
+  --panel-delay: 0.44s;
+}
+
+.kb-stage-card--9 {
+  --panel-delay: 0.5s;
+}
+
+.kb-stage-card--10 {
+  --panel-delay: 0.56s;
 }
 
 @keyframes kb-panel-enter {

+ 17 - 10
src/views/pms/stat/rykb.vue

@@ -7,7 +7,10 @@ import zjStatsSwitch from './rykb/zjStatsSwitch.vue'
 import xjwork from './rykb/xjwork.vue'
 import rydeviceList from './rykb/rydeviceList.vue'
 import ryProductionBriefs from './rykb/ryProductionBriefs.vue'
-import rydeviceType from './rykb/rydeviceType.vue'
+import equipmentCategory from './rykb/equipment-category.vue'
+import exceptionPrompt from './rykb/exception-prompt.vue'
+import historicalWorkload from './rykb/historical-workload.vue'
+import inventorySituation from './rykb/inventory-situation.vue'
 
 defineOptions({
   name: 'IotRyStatt'
@@ -103,11 +106,16 @@ onUnmounted(() => {
             <rysummary class="kb-stage-card kb-stage-card--1" />
             <div class="kb-chart-grid">
               <rydeviceStatus class="kb-stage-card kb-stage-card--2" />
-              <rydeviceType class="kb-stage-card kb-stage-card--6" />
-              <rydeviceList class="kb-stage-card kb-stage-card--4 kb-stage-card--list" />
-              <safeday class="kb-stage-card kb-stage-card--3" />
-              <zjStatsSwitch class="kb-stage-card kb-stage-card--5" />
-              <xjwork class="kb-stage-card kb-stage-card--7" />
+              <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" />
+              <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" />
+              <historicalWorkload class="kb-stage-card kb-stage-card--9" />
+              <inventorySituation class="kb-stage-card kb-stage-card--10" />
+
+              <!-- <rydeviceType class="kb-stage-card kb-stage-card--6" /> -->
             </div>
           </div>
 
@@ -132,8 +140,7 @@ onUnmounted(() => {
 .kb-content {
   position: relative;
   height: calc(100% - 52px * var(--kb-scale));
-  padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale))
-    calc(20px * var(--kb-scale));
+  padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
 }
 
 .page-tabs {
@@ -147,8 +154,8 @@ onUnmounted(() => {
 }
 
 .page-tab {
-  min-width: calc(82px * var(--kb-scale));
   height: calc(28px * var(--kb-scale));
+  min-width: calc(82px * var(--kb-scale));
   padding: 0 calc(14px * var(--kb-scale));
   font-family: YouSheBiaoTiHei, sans-serif;
   font-size: calc(15px * var(--kb-scale));
@@ -194,7 +201,7 @@ onUnmounted(() => {
   flex: 1;
   margin-top: calc(12px * var(--kb-scale));
   gap: calc(12px * var(--kb-scale));
-  grid-template-rows: repeat(2, minmax(0, 1fr));
+  grid-template-rows: repeat(3, minmax(0, 1fr));
   grid-template-columns: repeat(3, minmax(0, 1fr));
 }
 

+ 351 - 0
src/views/pms/stat/rykb/equipment-category.vue

@@ -0,0 +1,351 @@
+<script lang="ts" setup>
+import * as echarts from 'echarts'
+import {
+  ANIMATION,
+  CHART_RENDERER,
+  createLegend,
+  createTooltip,
+  FONT_FAMILY,
+  THEME
+} from '@/utils/kb'
+
+type ActivePanel = 'category' | 'value'
+
+type EquipmentCategoryItem = {
+  project: string
+  model: string
+  total: number
+  owned: number
+}
+
+const activePanel = ref<ActivePanel>('category')
+const categoryChartRef = ref<HTMLDivElement>()
+let categoryChart: echarts.ECharts | null = null
+
+const panelOptions: Array<{ label: string; value: ActivePanel }> = [
+  {
+    label: '类别',
+    value: 'category'
+  },
+  {
+    label: '价值',
+    value: 'value'
+  }
+]
+
+const activeTitle = computed(() =>
+  activePanel.value === 'category' ? '设备类别统计' : '设备价值统计'
+)
+
+const categoryData: EquipmentCategoryItem[] = [
+  { project: 'SCP项目', model: '50L', total: 4, owned: 3 },
+  { project: '紫金山项目', model: '40J', total: 2, owned: 1 },
+  { project: '新疆修井项目', model: '550HP', total: 9, owned: 9 },
+  { project: '新疆修井项目', model: '650HP', total: 1, owned: 1 },
+  { project: '伊拉克钻修项目', model: '750HP', total: 4, owned: 4 },
+  { project: '伊拉克钻修项目', model: '1000HP', total: 2, owned: 2 },
+  { project: '伊拉克钻修项目', model: '70D', total: 1, owned: 1 },
+  { project: '伊拉克一体化钻井项目', model: '70D', total: 1, owned: 0 },
+  { project: '阿根廷项目', model: '350HP', total: 3, owned: 3 },
+  { project: '埃塞项目', model: '50D', total: 2, owned: 0 }
+]
+
+function formatProjectName(value: string) {
+  return value.replace(/项目$/, '')
+}
+
+const categoryRows = computed(() =>
+  categoryData.map((item, index) => {
+    const nonOwned = Math.max(item.total - item.owned, 0)
+    const displayProject = formatProjectName(item.project)
+    const isFirstProjectRow = index === 0 || categoryData[index - 1].project !== item.project
+
+    return {
+      ...item,
+      displayProject,
+      projectLabel: isFirstProjectRow ? displayProject : '',
+      nonOwned
+    }
+  })
+)
+
+function getCategoryOption(data: any): echarts.EChartsOption {
+  const modelLabels = data.map((item) => item.model)
+  const projectLabels = data.map((item) => item.projectLabel)
+  const totalData = data.map((item) => item.total)
+  const ownedData = data.map((item) => item.owned)
+  const nonOwnedData = data.map((item) => item.nonOwned)
+  const maxTotal = Math.max(...totalData, 1)
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: 34,
+      right: 18,
+      bottom: 24,
+      left: 200,
+      containLabel: false
+    },
+    color: [THEME.color.green.line, THEME.color.orange.line],
+    legend: createLegend(
+      {
+        top: 0,
+        right: 0,
+        left: 360,
+        itemWidth: 10,
+        itemHeight: 10,
+        itemGap: 14,
+        selectedMode: true
+      },
+      ['自有设备', '非自有设备']
+    ),
+    tooltip: createTooltip({
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: {
+          color: THEME.split
+        }
+      },
+      formatter(params: any[]) {
+        const first = params[0]
+        const row = data[first?.dataIndex ?? 0]
+        if (!row) return ''
+
+        const ownedMarker =
+          '<span style="display:inline-block;width:8px;height:8px;margin-right:6px;border-radius:2px;background:#2e9a5b;"></span>'
+        const nonOwnedMarker =
+          '<span style="display:inline-block;width:8px;height:8px;margin-right:6px;border-radius:2px;background:#f08c2e;"></span>'
+
+        return [
+          `<div style="min-width:132px;font-weight:700;color:#24364f;margin-bottom:6px;">${row.displayProject} / ${row.model}</div>`,
+          `<div style="display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>${ownedMarker}自有设备</span><b style="color:#2e9a5b;">${row.owned}</b></div>`,
+          `<div style="display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>${nonOwnedMarker}非自有设备</span><b style="color:#f08c2e;">${row.nonOwned}</b></div>`,
+          `<div style="margin-top:4px;padding-top:4px;border-top:1px solid rgba(31,91,184,.12);display:flex;justify-content:space-between;gap:18px;line-height:22px;"><span>设备合计</span><b style="color:#1f5bb8;">${row.total}</b></div>`
+        ].join('')
+      }
+    }),
+    xAxis: {
+      type: 'value',
+      max: Math.max(10, maxTotal),
+      axisLine: {
+        show: true
+      },
+      axisTick: {
+        show: true
+      },
+      axisLabel: {
+        show: true,
+        color: THEME.text.regular,
+        fontSize: 12,
+        fontWeight: 500,
+        fontFamily: FONT_FAMILY
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: [
+      {
+        type: 'category',
+        data: modelLabels,
+        inverse: true,
+        axisLine: {
+          show: false
+        },
+        axisTick: {
+          show: false
+        },
+        axisLabel: {
+          color: THEME.text.regular,
+          fontSize: 12,
+          fontWeight: 500,
+          fontFamily: FONT_FAMILY,
+          margin: 10,
+          width: 54,
+          overflow: 'truncate',
+          align: 'right'
+        }
+      },
+      {
+        type: 'category',
+        data: projectLabels,
+        inverse: true,
+        position: 'left',
+        offset: 70,
+        axisLine: {
+          show: false
+        },
+        axisTick: {
+          show: false
+        },
+        axisLabel: {
+          color: THEME.text.strong,
+          fontSize: 13,
+          fontWeight: 700,
+          fontFamily: FONT_FAMILY,
+          margin: 10,
+          width: 104,
+          overflow: 'break',
+          align: 'right'
+        }
+      }
+    ],
+    series: [
+      {
+        name: '设备数量',
+        type: 'bar',
+        data: totalData,
+        barWidth: 13,
+        silent: true,
+        z: 1,
+        itemStyle: {
+          borderRadius: 999,
+          color: THEME.split
+        },
+        tooltip: {
+          show: false
+        }
+      },
+      {
+        name: '自有设备',
+        type: 'bar',
+        stack: 'device',
+        data: ownedData,
+        barWidth: 13,
+        barGap: '-100%',
+        z: 3,
+        itemStyle: {
+          borderRadius: [999, 0, 0, 999],
+          color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+            { offset: 0, color: THEME.color.green.light },
+            { offset: 0.55, color: THEME.color.green.mid },
+            { offset: 1, color: THEME.color.green.line }
+          ])
+        }
+      },
+      {
+        name: '非自有设备',
+        type: 'bar',
+        stack: 'device',
+        data: nonOwnedData,
+        barWidth: 13,
+        z: 3,
+        itemStyle: {
+          borderRadius: [0, 999, 999, 0],
+          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 initCategoryChart() {
+  if (!categoryChartRef.value) return
+
+  if (categoryChart) {
+    categoryChart.dispose()
+  }
+
+  categoryChart = echarts.init(categoryChartRef.value, undefined, {
+    renderer: CHART_RENDERER
+  })
+  renderCategoryChart()
+}
+
+function renderCategoryChart() {
+  categoryChart?.setOption(getCategoryOption(categoryRows.value), true)
+}
+
+function resizeCategoryChart() {
+  categoryChart?.resize()
+}
+
+function destroyCategoryChart() {
+  if (categoryChart) {
+    categoryChart.dispose()
+    categoryChart = null
+  }
+}
+
+watch(activePanel, (value) => {
+  if (value !== 'category') return
+
+  nextTick(() => {
+    resizeCategoryChart()
+  })
+})
+
+onMounted(() => {
+  initCategoryChart()
+  window.addEventListener('resize', resizeCategoryChart)
+  window.addEventListener('rykb:resize', resizeCategoryChart)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeCategoryChart)
+  window.removeEventListener('rykb:resize', resizeCategoryChart)
+  destroyCategoryChart()
+})
+</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="equipment-category-switch" />
+    </div>
+    <div class="flex-1 min-h-0">
+      <div v-show="activePanel === 'category'" ref="categoryChartRef" class="category-chart"></div>
+      <div v-show="activePanel === 'value'" class="h-full"></div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.equipment-category-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;
+  }
+}
+
+.category-chart {
+  width: 100%;
+  height: 100%;
+  min-height: 0;
+}
+</style>

+ 318 - 0
src/views/pms/stat/rykb/exception-prompt.vue

@@ -0,0 +1,318 @@
+<script lang="ts" setup>
+import dayjs from 'dayjs'
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import { IotStatApi } from '@/api/pms/stat'
+
+dayjs.extend(quarterOfYear)
+
+type TimeRange = 'day' | 'month' | 'quarter' | 'year'
+
+type NptCountItem = {
+  nptName: string
+  teamCount: number
+}
+
+const activeTimeRange = ref<TimeRange>('day')
+const loading = ref(false)
+const productionList = ref<NptCountItem[]>([])
+
+const timeOptions: Array<{ label: string; value: TimeRange }> = [
+  { label: '本日', value: 'day' },
+  { label: '本月', value: 'month' },
+  { label: '本季', value: 'quarter' },
+  { label: '全年', value: 'year' }
+]
+
+const productionTopList = computed(() => productionList.value.slice(0, 9))
+const deviceExceptionCount = ref(0)
+
+function getCreateTime(type: TimeRange) {
+  const now = dayjs()
+  const start = now.startOf(type === 'day' ? 'day' : type === 'year' ? 'year' : type)
+  const end = now.endOf(type === 'day' ? 'day' : type === 'year' ? 'year' : type)
+
+  return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
+}
+
+async function loadData() {
+  loading.value = true
+  try {
+    const res = await IotStatApi.getRyNptCount({
+      createTime: getCreateTime(activeTimeRange.value)
+    })
+
+    productionList.value = Array.isArray(res)
+      ? res.map((item) => ({
+          nptName: item.nptName || '-',
+          teamCount: Number(item.teamCount || 0)
+        }))
+      : []
+  } catch (error) {
+    console.error('获取生产异常失败:', error)
+    productionList.value = []
+  } finally {
+    loading.value = false
+  }
+}
+
+watch(activeTimeRange, () => {
+  loadData()
+})
+
+onMounted(() => {
+  loadData()
+})
+</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>
+        异常警示
+      </div>
+      <el-segmented
+        v-model="activeTimeRange"
+        :options="timeOptions"
+        size="small"
+        class="exception-time-switch" />
+    </div>
+
+    <div v-loading="loading" class="exception-body">
+      <section class="exception-section exception-section--production">
+        <div class="exception-section__title">
+          <span class="exception-section__marker"></span>
+          生产异常
+          <span class="exception-section__hint">TOP 9</span>
+        </div>
+        <div class="production-grid">
+          <div
+            v-for="(item, index) in productionTopList"
+            :key="item.nptName"
+            class="production-item">
+            <span class="production-item__rank">{{ index + 1 }}</span>
+            <span class="production-item__name">{{ item.nptName }}</span>
+            <span class="production-item__count">
+              <span class="production-item__value">{{ item.teamCount }}</span>
+              <span class="production-item__unit">个</span>
+            </span>
+          </div>
+          <div v-if="!productionTopList.length && !loading" class="exception-empty">
+            暂无生产异常数据
+          </div>
+        </div>
+      </section>
+
+      <section class="exception-section exception-section--device">
+        <div class="exception-section__title">
+          <span class="exception-section__marker"></span>
+          设备异常
+        </div>
+        <div class="device-exception-card">
+          <span class="device-exception-card__label">异常数量:</span>
+          <span class="device-exception-card__value">{{ deviceExceptionCount }}</span>
+          <span class="device-exception-card__unit">个</span>
+        </div>
+      </section>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import url('@/styles/kb.scss');
+
+.exception-time-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(7px * var(--kb-scale, 1));
+    font-size: calc(13px * var(--kb-scale, 1));
+    font-weight: 600;
+    color: #29527f;
+  }
+}
+
+.exception-body {
+  display: flex;
+  min-height: 0;
+  flex: 1;
+  flex-direction: column;
+  gap: calc(7px * var(--kb-scale, 1));
+  padding: calc(10px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1))
+    calc(12px * var(--kb-scale, 1));
+
+  :deep(.el-loading-mask) {
+    background-color: rgb(221 233 251 / 42%);
+  }
+}
+
+.exception-section {
+  min-height: 0;
+}
+
+.exception-section--production {
+  display: flex;
+  flex: 1 1 0;
+  flex-direction: column;
+}
+
+.exception-section--device {
+  position: relative;
+  z-index: 1;
+  flex: none;
+}
+
+.exception-section__title {
+  display: flex;
+  height: calc(19px * var(--kb-scale, 1));
+  align-items: center;
+  gap: calc(7px * var(--kb-scale, 1));
+  margin-bottom: calc(5px * var(--kb-scale, 1));
+  font-size: calc(14px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: calc(20px * var(--kb-scale, 1));
+  color: #24364f;
+}
+
+.exception-section__marker {
+  width: calc(5px * var(--kb-scale, 1));
+  height: calc(16px * var(--kb-scale, 1));
+  background: linear-gradient(180deg, #2d7cf8 0%, #03409b 100%);
+  border-radius: 999px;
+  box-shadow: 0 0 calc(8px * var(--kb-scale, 1)) rgb(45 124 248 / 28%);
+}
+
+.exception-section__hint {
+  padding-top: calc(1px * var(--kb-scale, 1));
+  margin-left: auto;
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 600;
+  color: #6f85aa;
+}
+
+.production-grid {
+  display: grid;
+  min-height: 0;
+  flex: 1;
+  padding: calc(6px * var(--kb-scale, 1));
+  gap: calc(5px * var(--kb-scale, 1)) calc(7px * var(--kb-scale, 1));
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  grid-template-rows: repeat(3, minmax(0, 1fr));
+}
+
+.production-item {
+  display: grid;
+  height: 100%;
+  min-width: 0;
+  min-height: 0;
+  align-items: center;
+  padding: 0 calc(7px * var(--kb-scale, 1));
+  overflow: hidden;
+  background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 36%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(6px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1)) rgb(63 103 171 / 7%);
+  grid-template-columns:
+    calc(18px * var(--kb-scale, 1)) minmax(0, 1fr)
+    calc(46px * var(--kb-scale, 1));
+  gap: calc(6px * var(--kb-scale, 1));
+}
+
+.production-item__rank {
+  display: inline-flex;
+  width: calc(18px * var(--kb-scale, 1));
+  height: calc(18px * var(--kb-scale, 1));
+  align-items: center;
+  justify-content: center;
+  font-size: calc(11px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #1f5bb8;
+  background: rgb(31 91 184 / 8%);
+  border: 1px solid rgb(31 91 184 / 10%);
+  border-radius: 999px;
+}
+
+.production-item__name {
+  overflow: hidden;
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 600;
+  line-height: calc(16px * var(--kb-scale, 1));
+  color: #24364f;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.production-item__count {
+  display: inline-flex;
+  align-items: baseline;
+  justify-content: flex-end;
+  gap: calc(2px * var(--kb-scale, 1));
+  min-width: 0;
+}
+
+.production-item__value {
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(19px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: 1;
+  color: #e43f5f;
+  text-align: right;
+}
+
+.production-item__unit {
+  font-size: calc(12px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #24364f;
+}
+
+.exception-empty {
+  grid-column: 1 / -1;
+  place-self: center;
+  font-size: calc(14px * var(--kb-scale, 1));
+  font-weight: 600;
+  color: #6f85aa;
+}
+
+.device-exception-card {
+  display: flex;
+  height: calc(42px * var(--kb-scale, 1));
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(180deg, rgb(255 255 255 / 58%) 0%, rgb(213 227 249 / 36%) 100%);
+  border: 1px solid rgb(255 255 255 / 74%);
+  border-radius: calc(6px * var(--kb-scale, 1));
+  box-shadow:
+    inset 0 1px 0 rgb(255 255 255 / 78%),
+    0 calc(6px * var(--kb-scale, 1)) calc(14px * var(--kb-scale, 1)) rgb(63 103 171 / 7%);
+}
+
+.device-exception-card__label,
+.device-exception-card__unit {
+  font-size: calc(15px * var(--kb-scale, 1));
+  font-weight: 700;
+  color: #24364f;
+}
+
+.device-exception-card__value {
+  margin: 0 calc(6px * var(--kb-scale, 1));
+  font-family: YouSheBiaoTiHei, sans-serif;
+  font-size: calc(23px * var(--kb-scale, 1));
+  font-weight: 700;
+  line-height: 1;
+  color: #e43f5f;
+}
+</style>

+ 18 - 0
src/views/pms/stat/rykb/historical-workload.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>

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

@@ -0,0 +1,560 @@
+<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
+  yearBeginningAmount: number
+  mayInventoryAmount: number
+  increaseAmount: number
+  yearBeginningBacklog: number
+  mayBacklogAmount: number
+  backlogRatio: string
+}
+
+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: 'SCP项目',
+    yearBeginningAmount: 119.08,
+    mayInventoryAmount: 123.63,
+    increaseAmount: 4.55,
+    yearBeginningBacklog: 23.63,
+    mayBacklogAmount: 43.07,
+    backlogRatio: '34.84%'
+  },
+  {
+    project: '紫金山项目',
+    yearBeginningAmount: 79.97,
+    mayInventoryAmount: 82.95,
+    increaseAmount: 2.98,
+    yearBeginningBacklog: 4.32,
+    mayBacklogAmount: 24.76,
+    backlogRatio: '29.85%'
+  },
+  {
+    project: '青海东台项目',
+    yearBeginningAmount: 10.42,
+    mayInventoryAmount: 10.32,
+    increaseAmount: -0.1,
+    yearBeginningBacklog: 10.42,
+    mayBacklogAmount: 10.32,
+    backlogRatio: '100.00%'
+  },
+  {
+    project: '新疆项目',
+    yearBeginningAmount: 156.11,
+    mayInventoryAmount: 159.45,
+    increaseAmount: 3.34,
+    yearBeginningBacklog: 28,
+    mayBacklogAmount: 26.84,
+    backlogRatio: '16.83%'
+  },
+  {
+    project: '泰安项目',
+    yearBeginningAmount: 13.93,
+    mayInventoryAmount: 13.62,
+    increaseAmount: -0.31,
+    yearBeginningBacklog: 5.18,
+    mayBacklogAmount: 6.31,
+    backlogRatio: '46.35%'
+  },
+  {
+    project: '伊拉克项目',
+    yearBeginningAmount: 1205.77,
+    mayInventoryAmount: 1098.32,
+    increaseAmount: -107.45,
+    yearBeginningBacklog: 227.67,
+    mayBacklogAmount: 350.58,
+    backlogRatio: '31.92%'
+  },
+  {
+    project: '伊拉克一体化项目',
+    yearBeginningAmount: 333.36,
+    mayInventoryAmount: 369.97,
+    increaseAmount: 36.61,
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0,
+    backlogRatio: '0.00%'
+  },
+  {
+    project: '国外项目暂存物资',
+    yearBeginningAmount: 134.49,
+    mayInventoryAmount: 156.73,
+    increaseAmount: 22.24,
+    yearBeginningBacklog: 0,
+    mayBacklogAmount: 0,
+    backlogRatio: '0.00%'
+  }
+]
+
+function formatProjectName(value: string) {
+  return value.replace(/项目$/, '')
+}
+
+function formatAmount(value: number) {
+  return Number(value || 0).toFixed(2)
+}
+
+function getDistributionOption(data: InventoryItem[]): echarts.EChartsOption {
+  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: 13,
+        fontWeight: 500,
+        lineHeight: 22,
+        fontFamily: FONT_FAMILY
+      },
+      value: {
+        color: THEME.text.strong,
+        fontSize: 20,
+        fontWeight: 700,
+        lineHeight: 30,
+        fontFamily: FONT_FAMILY
+      }
+    }
+  }
+
+  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: 18,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: 14,
+          fontWeight: 700,
+          lineHeight: 16,
+          fontFamily: FONT_FAMILY
+        }
+      },
+      {
+        text: `5月底积压库存金额\n${formatAmount(backlogTotal)} 万元`,
+        left: '72.5%',
+        top: 18,
+        textAlign: 'center',
+        textStyle: {
+          color: THEME.text.strong,
+          fontSize: 14,
+          fontWeight: 700,
+          lineHeight: 16,
+          fontFamily: FONT_FAMILY
+        }
+      }
+    ],
+    legend: createLegend({
+      type: 'scroll',
+      bottom: 10,
+      left: 10,
+      right: 10,
+      itemWidth: 13,
+      itemHeight: 13,
+      itemGap: 14,
+      textStyle: {
+        color: THEME.text.regular,
+        fontSize: 14,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY
+      }
+    }),
+    series: [
+      {
+        name: '5月底库存金额',
+        type: 'pie',
+        radius: ['48%', '68%'],
+        center: ['27%', '57%'],
+        minAngle: 5,
+        avoidLabelOverlap: true,
+        label: pieLabel,
+        emphasis: {
+          label: {
+            show: true
+          },
+          itemStyle: {
+            shadowBlur: 14,
+            shadowColor: THEME.color.blue.shadow
+          }
+        },
+        data: inventoryPieData
+      },
+      {
+        name: '5月底积压库存金额',
+        type: 'pie',
+        radius: ['48%', '68%'],
+        center: ['73%', '57%'],
+        minAngle: 5,
+        avoidLabelOverlap: true,
+        label: pieLabel,
+        emphasis: {
+          label: {
+            show: true
+          },
+          itemStyle: {
+            shadowBlur: 14,
+            shadowColor: THEME.color.blue.shadow
+          }
+        },
+        data: backlogPieData
+      }
+    ]
+  }
+}
+
+function getTrendOption(data: InventoryItem[]): echarts.EChartsOption {
+  const projects = data.map((item) => formatProjectName(item.project))
+  const maxBacklog = Math.max(
+    ...data.map((item) => Math.max(item.yearBeginningBacklog, item.mayBacklogAmount)),
+    1
+  )
+  const backlogAxisMax = Math.ceil((maxBacklog * 1.15) / 50) * 50
+  const barLabel = {
+    show: true,
+    position: 'right' as any,
+    distance: 7,
+    color: THEME.text.strong,
+    fontSize: 12,
+    fontWeight: 700,
+    fontFamily: FONT_FAMILY,
+    formatter(params: any) {
+      const value = Number(params.value)
+
+      return formatAmount(value)
+    }
+  }
+
+  return {
+    ...ANIMATION,
+    grid: {
+      ...THEME.grid,
+      top: 32
+    },
+    color: [THEME.color.blue.line, THEME.color.orange.line],
+    legend: createLegend(
+      {
+        top: 4,
+        right: 6,
+        itemWidth: 12,
+        itemHeight: 12,
+        itemGap: 16,
+        textStyle: {
+          color: THEME.text.regular,
+          fontSize: 14,
+          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: 12,
+        fontFamily: FONT_FAMILY,
+        formatter(value: number) {
+          return `${value}`
+        }
+      },
+      splitLine: {
+        lineStyle: {
+          color: THEME.split,
+          type: 'dashed'
+        }
+      }
+    },
+    yAxis: {
+      type: 'category',
+      data: projects,
+      inverse: true,
+      axisLine: {
+        show: false
+      },
+      axisTick: {
+        show: false
+      },
+      axisLabel: {
+        color: THEME.text.regular,
+        fontSize: 12,
+        fontWeight: 600,
+        fontFamily: FONT_FAMILY,
+        margin: 12,
+        width: 132,
+        overflow: 'break',
+        align: 'right'
+      }
+    },
+    series: [
+      {
+        name: '年初积压库存',
+        type: 'bar',
+        data: data.map((item) => item.yearBeginningBacklog),
+        barWidth: 12,
+        barGap: '5%',
+        barCategoryGap: '36%',
+        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, 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: 12,
+        barMinHeight: 0,
+        barGap: '5%',
+        barCategoryGap: '36%',
+        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, 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()
+}
+
+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('rykb:resize', resizeCharts)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeCharts)
+  window.removeEventListener('rykb: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>

+ 72 - 34
src/views/pms/stat/rykb/rysummary.vue

@@ -14,6 +14,7 @@ type CardKey =
   | 'inspecttfinished'
   | 'zj'
   | 'xj'
+  | 'utilizationRate'
 
 type CardConfig = SummaryCardConfig<CardKey>
 
@@ -39,27 +40,13 @@ const cardConfigs: CardConfig[] = [
     accent: THEME.color.orange.strong,
     glow: THEME.color.orange.glow
   },
-  {
-    key: 'filledCount',
-    title: '运行已填写',
-    icon: 'i-solar:clipboard-check-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
-  {
-    key: 'bytodo',
-    title: '待保养',
-    icon: 'i-solar:shield-warning-linear',
-    accent: THEME.color.orange.strong,
-    glow: THEME.color.orange.glow
-  },
-  {
-    key: 'byfinished',
-    title: '已保养',
-    icon: 'i-solar:shield-check-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
-  },
+  // {
+  //   key: 'filledCount',
+  //   title: '运行已填写',
+  //   icon: 'i-solar:clipboard-check-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
   {
     key: 'inspectttodo',
     title: '待巡检',
@@ -68,12 +55,26 @@ const cardConfigs: CardConfig[] = [
     glow: THEME.color.orange.glow
   },
   {
-    key: 'inspecttfinished',
-    title: '已巡检',
-    icon: 'i-solar:check-circle-linear',
-    accent: THEME.color.green.strong,
-    glow: THEME.color.green.glow
+    key: 'bytodo',
+    title: '待保养',
+    icon: 'i-solar:shield-warning-linear',
+    accent: THEME.color.orange.strong,
+    glow: THEME.color.orange.glow
   },
+  // {
+  //   key: 'byfinished',
+  //   title: '已保养',
+  //   icon: 'i-solar:shield-check-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
+  // {
+  //   key: 'inspecttfinished',
+  //   title: '已巡检',
+  //   icon: 'i-solar:check-circle-linear',
+  //   accent: THEME.color.green.strong,
+  //   glow: THEME.color.green.glow
+  // },
   {
     key: 'zj',
     title: '钻井总进尺(m)',
@@ -87,6 +88,13 @@ const cardConfigs: CardConfig[] = [
     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
   }
 ]
 
@@ -101,7 +109,8 @@ function createDefaultCardState(): Record<CardKey, CardStateItem> {
     inspectttodo: { value: 0, loading: true },
     inspecttfinished: { value: 0, loading: true },
     zj: { value: 0, loading: true },
-    xj: { value: 0, loading: true }
+    xj: { value: 0, loading: true },
+    utilizationRate: { value: 0, loading: true }
   }
 }
 
@@ -130,6 +139,13 @@ function setCardLoading(keys: CardKey[], loading: boolean) {
   })
 }
 
+function getCurrentYearCreateTime() {
+  return [
+    dayjs().startOf('year').format('YYYY-MM-DD HH:mm:ss'),
+    dayjs().endOf('year').format('YYYY-MM-DD HH:mm:ss')
+  ]
+}
+
 async function loadDeviceCard() {
   const keys: CardKey[] = ['device']
   setCardLoading(keys, true)
@@ -232,8 +248,12 @@ async function loadZjXj() {
   const keys: CardKey[] = ['zj', 'xj']
   setCardLoading(keys, true)
 
+  const params = {
+    createTime: getCurrentYearCreateTime()
+  }
+
   try {
-    const res = await IotStatApi.getInspectZjxjCount()
+    const res = await IotStatApi.getInspectZjxjCount(params)
     setCardValue('zj', res?.zj)
     setCardValue('xj', res?.xj)
   } catch (error) {
@@ -245,6 +265,25 @@ async function loadZjXj() {
   }
 }
 
+async function loadRate() {
+  const keys: CardKey[] = ['utilizationRate']
+  setCardLoading(keys, true)
+
+  const params = {
+    createTime: getCurrentYearCreateTime()
+  }
+
+  try {
+    const res = await IotStatApi.getInspectRate(params)
+    setCardValue('utilizationRate', (res * 100).toFixed(2))
+  } catch (error) {
+    console.error('获取累计设备利用率失败:', error)
+    setCardValue('utilizationRate', 0)
+  } finally {
+    setCardLoading(keys, false)
+  }
+}
+
 function loadAllCards() {
   loadDeviceCard()
   loadMaintainCard()
@@ -252,6 +291,7 @@ function loadAllCards() {
   loadMaintainStatusCards()
   loadInspectCards()
   loadZjXj()
+  loadRate()
 }
 
 onMounted(() => {
@@ -266,10 +306,10 @@ onMounted(() => {
         <span></span>
         <span></span>
       </div>
-      工单情
+      运行概
     </div>
 
-    <div class="summary-panel__grid grid grid-cols-10 flex-1">
+    <div class="summary-panel__grid grid grid-cols-8 flex-1">
       <article
         v-for="card in summaryCards"
         :key="card.key"
@@ -277,8 +317,7 @@ onMounted(() => {
         :style="{
           '--card-accent': card.accent,
           '--card-glow': card.glow
-        }"
-      >
+        }">
         <div class="summary-card__shine"></div>
 
         <div class="summary-card__icon">
@@ -293,8 +332,7 @@ onMounted(() => {
               style="color: #1f5bb8"
               :start-val="0"
               :end-val="card.value"
-              :duration="1200"
-            />
+              :duration="1200" />
             <span v-else class="summary-card__placeholder">--</span>
           </div>
         </div>

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

@@ -89,7 +89,8 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
       nameTextStyle: {
         color: THEME.text.regular,
         fontSize: 13,
-        fontFamily: FONT_FAMILY
+        fontFamily: FONT_FAMILY,
+        align: 'left'
       },
       splitNumber: 4,
       axisLine: {

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

@@ -98,7 +98,8 @@ function getChartOption(data: ChartData): echarts.EChartsOption {
         nameTextStyle: {
           color: THEME.text.regular,
           fontSize: 13,
-          fontFamily: FONT_FAMILY
+          fontFamily: FONT_FAMILY,
+          align: 'left'
         },
         splitNumber: 4,
         axisLine: {