Zimo hai 2 días
pai
achega
813fab69a4

+ 1 - 1
.env.local

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

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

@@ -180,5 +180,8 @@ export const IotStatApi = {
   },
   getRdWorkloadYear: async (params: any) => {
     return await request.get({ url: `/pms/iot-rd-daily-report/workloadKanban`, params })
+  },
+  getUtilization: async (params: any) => {
+    return await request.get({ url: `/rq/stat/rd/device/utilizationRates`, params })
   }
 }

+ 31 - 29
src/views/pms/stat/rdkb.vue

@@ -191,9 +191,9 @@
     </el-row>
 
     <el-row :gutter="16" class="mb-4">
-      <!-- 备件更换情况部分保持不变 -->
       <el-col :span="12" :xs="24">
-        <el-card class="chart-card" shadow="never">
+        <UtilizationChart />
+        <!-- <el-card class="chart-card" shadow="never">
           <template #header>
             <div style="display: flex; flex-direction: row; justify-content: space-between">
               <span class="text-base font-medium" style="color: #b6c8da">{{
@@ -213,37 +213,38 @@
               </div>
             </div>
           </template>
-          <!-- 添加两个卡片 -->
-          <!--          <div class="flex justify-between mb-2">-->
-          <!--            <el-card class="stat-card">-->
-          <!--              <div class="flex flex-row justify-evenly">-->
-          <!--                <div>-->
-          <!--                  <Icon icon="fa-solid:award" size="30" color="blue" />-->
-          <!--                </div>-->
-          <!--                <div class="flex flex-col items-center">-->
-          <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareCount')}}</span>-->
-          <!--                  <span class="text-lg font-bold">{{ totalMaterialCount }}</span>-->
-          <!--                </div>-->
-          <!--              </div>-->
-          <!--            </el-card>-->
-          <!--            <el-card class="stat-card">-->
-          <!--              <div class="flex flex-row justify-evenly">-->
-          <!--                <div>-->
-          <!--                  <Icon icon="fa-solid:yen-sign" size="30" color="orange" />-->
-          <!--                </div>-->
-          <!--                <div class="flex flex-col items-center">-->
-          <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareAmount')}}</span>-->
-          <!--                  <span class="text-lg font-bold">{{ totalMaterialCost }}</span>-->
-          <!--                </div>-->
-          <!--              </div>-->
-          <!--            </el-card>-->
-          <!--          </div>-->
-          <!--          <div ref="sparePartRef" class="h-[330px]"></div>-->
+
           <div class="table-container">
             <div ref="utilizationRef" style="width: 100%; height: 360px"></div>
           </div>
-        </el-card>
+        </el-card> -->
       </el-col>
+      <!-- 添加两个卡片 -->
+      <!--          <div class="flex justify-between mb-2">-->
+      <!--            <el-card class="stat-card">-->
+      <!--              <div class="flex flex-row justify-evenly">-->
+      <!--                <div>-->
+      <!--                  <Icon icon="fa-solid:award" size="30" color="blue" />-->
+      <!--                </div>-->
+      <!--                <div class="flex flex-col items-center">-->
+      <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareCount')}}</span>-->
+      <!--                  <span class="text-lg font-bold">{{ totalMaterialCount }}</span>-->
+      <!--                </div>-->
+      <!--              </div>-->
+      <!--            </el-card>-->
+      <!--            <el-card class="stat-card">-->
+      <!--              <div class="flex flex-row justify-evenly">-->
+      <!--                <div>-->
+      <!--                  <Icon icon="fa-solid:yen-sign" size="30" color="orange" />-->
+      <!--                </div>-->
+      <!--                <div class="flex flex-col items-center">-->
+      <!--                  <span class="text-sm " style="color: #101010">{{t('stat.spareAmount')}}</span>-->
+      <!--                  <span class="text-lg font-bold">{{ totalMaterialCost }}</span>-->
+      <!--                </div>-->
+      <!--              </div>-->
+      <!--            </el-card>-->
+      <!--          </div>-->
+      <!--          <div ref="sparePartRef" class="h-[330px]"></div>-->
       <!-- 月度工作量表 -->
       <el-col :span="12" :xs="24">
         <el-card class="chart-card" shadow="never">
@@ -511,6 +512,7 @@ import { IotStatApi } from '@/api/pms/stat'
 import { ref, onMounted, computed, watch, nextTick, reactive } from 'vue'
 import { useLocaleStore } from '@/store/modules/locale'
 import WorkloadChart from './rdkb/workload.vue'
+import UtilizationChart from './rdkb/utilization.vue'
 
 /** 会员统计 */
 defineOptions({ name: 'IotRdStat' })

+ 215 - 0
src/views/pms/stat/rdkb/utilization.vue

@@ -0,0 +1,215 @@
+<script lang="ts" setup>
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { IotStatApi } from '@/api/pms/stat'
+import dayjs from 'dayjs'
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
+import * as echarts from 'echarts'
+
+dayjs.extend(quarterOfYear)
+
+const chartRef = ref(null)
+let myChart: echarts.ECharts | null = null
+const currentTimeType = ref('month')
+
+const timeOptions = [
+  { label: '本月', value: 'month' },
+  { label: '本季度', value: 'quarter' },
+  { label: '本年', value: 'year' }
+]
+
+const getDateRange = (type: 'year' | 'quarter' | 'month') => {
+  const now = dayjs()
+  let start: dayjs.Dayjs, end: dayjs.Dayjs
+
+  if (type === 'year') {
+    start = now.startOf('year')
+    end = now.endOf('year')
+  } else if (type === 'quarter') {
+    start = now.startOf('quarter')
+    end = now.endOf('quarter')
+  } else {
+    start = now.startOf('month')
+    end = now.endOf('month')
+  }
+
+  return {
+    start: start.format('YYYY-MM-DD HH:mm:ss'),
+    end: end.format('YYYY-MM-DD HH:mm:ss')
+  }
+}
+
+const fetchData = async () => {
+  if (myChart) {
+    myChart.showLoading({
+      text: '加载中 ...',
+      color: '#409eff',
+      textColor: '#B6C8DA',
+      maskColor: 'rgba(0, 0, 0, 0.2)'
+    })
+  }
+
+  const { start, end } = getDateRange(currentTimeType.value as 'year' | 'quarter' | 'month')
+
+  const params = {
+    'createTime[0]': start,
+    'createTime[1]': end,
+    timeType: currentTimeType.value
+  }
+
+  try {
+    let list: any[] = []
+
+    const res = await IotStatApi.getUtilization(params)
+    if (res && Array.isArray(res)) list = res
+
+    renderChart(list)
+  } catch (error) {
+    console.error('Workload API Error:', error)
+  } finally {
+    myChart?.hideLoading()
+  }
+}
+
+const renderChart = (data: any[]) => {
+  if (!myChart) return
+
+  const xAxisData = data.map((item) => item.projectDeptName.replace('项目部', ''))
+
+  const seriesData = data.map((item) => {
+    const val = item.utilizationRate
+    if (val === null || val === undefined || isNaN(val)) return 0
+    return parseFloat((val * 100).toFixed(2))
+  })
+
+  const option: echarts.EChartsOption = {
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(0,0,0,0.7)',
+      borderColor: '#409eff',
+      textStyle: { color: '#fff' },
+      axisPointer: {
+        type: 'shadow',
+        shadowStyle: { color: 'rgba(255, 255, 255, 0.1)' }
+      },
+      // 自定义 Tooltip 内容,加上 %
+      formatter: (params: any) => {
+        const item = params[0]
+        return `${item.name}<br/>
+                <span style="display:inline-block;margin-right:4px;border-radius:10px;width:10px;height:10px;background-color:${item.color.colorStops ? item.color.colorStops[0].color : item.color};"></span>
+                ${item.seriesName}: <span style="font-weight:bold; color: #fff">${item.value}%</span>`
+      }
+    },
+    grid: { top: '15%', left: '3%', right: '3%', bottom: '5%', containLabel: true },
+    xAxis: {
+      data: xAxisData,
+      type: 'category',
+      boundaryGap: true,
+      axisLabel: { color: '#B6C8DA', interval: 0, fontSize: 12 },
+      axisLine: { lineStyle: { color: '#B6C8DA' } },
+      axisTick: { show: false }
+    },
+    yAxis: {
+      type: 'value',
+      name: '利用率 (%)',
+      axisLabel: { color: '#B6C8DA', formatter: '{value}' },
+      nameTextStyle: { color: '#B6C8DA', padding: [0, 0, 0, 10] },
+      axisLine: { lineStyle: { color: '#B6C8DA' } },
+      splitLine: { lineStyle: { color: '#457794', type: 'dashed' } }
+    },
+    series: {
+      name: '设备利用率',
+      type: 'bar',
+      showBackground: true,
+      backgroundStyle: {
+        color: 'rgba(180, 180, 180, 0.1)',
+        borderRadius: [4, 4, 0, 0]
+      },
+      itemStyle: {
+        borderRadius: [4, 4, 0, 0],
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+          { offset: 0, color: '#23D0F6' },
+          { offset: 1, color: '#1A7BF8' }
+        ])
+      },
+      emphasis: {
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#4FFBDF' },
+            { offset: 1, color: '#23D0F6' }
+          ])
+        }
+      },
+      data: seriesData
+    }
+  }
+
+  myChart.setOption(option)
+}
+
+const handleTimeChange = () => {
+  fetchData()
+}
+
+const resizeChart = () => myChart?.resize()
+
+onMounted(() => {
+  nextTick(() => {
+    myChart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+    fetchData()
+    window.addEventListener('resize', resizeChart)
+  })
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  myChart?.dispose()
+})
+</script>
+
+<template>
+  <div class="card size-full rounded-lg p-4 flex flex-col">
+    <div class="flex justify-between items-center mb-4">
+      <div class="text-[#b6c8da] text-lg font-bold">设备利用率</div>
+      <el-segmented
+        size="default"
+        v-model="currentTimeType"
+        :options="timeOptions"
+        @change="handleTimeChange"
+        class="dark-segmented w-50!"
+        block
+      />
+    </div>
+    <div ref="chartRef" class="flex-1 w-full min-h-0"></div>
+  </div>
+</template>
+
+<style scoped>
+.card {
+  background-color: rgb(0 0 0 / 30%);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
+  }
+}
+
+.dark-segmented {
+  --el-segmented-item-selected-color: #e5eaf3;
+  --el-border-radius-base: 16px;
+  --el-segmented-color: #cfd3dc;
+  --el-segmented-bg-color: #262727;
+  --el-segmented-item-selected-bg-color: #409eff;
+  --el-segmented-item-selected-disabled-bg-color: rgb(42 89 138);
+  --el-segmented-item-hover-color: #e5eaf3;
+  --el-segmented-item-hover-bg-color: #39393a;
+  --el-segmented-item-active-bg-color: #424243;
+  --el-segmented-item-disabled-color: #8d9095;
+}
+
+:deep(.el-segmented__item) {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 186 - 85
src/views/pms/stat/rdkb/workload.vue

@@ -8,7 +8,7 @@ import * as echarts from 'echarts'
 dayjs.extend(quarterOfYear)
 
 const chartRef = ref(null)
-const myChart = ref<echarts.EChartsType | null>(null)
+let myChart: echarts.ECharts | null = null
 const currentTimeType = ref('month')
 
 const timeOptions = [
@@ -24,23 +24,8 @@ const fieldConfig = [
   { key: 'cumulativeWorkingLayers', name: '压裂层数' }
 ]
 
-const colorPalette = ['#5470c6', '#f1d209', '#e14f0f', '#91cc75']
-
-const hexToRgba = (hex: string, opacity: number) => {
-  let c: any
-  if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
-    c = hex.substring(1).split('')
-    if (c.length === 3) {
-      c = [c[0], c[0], c[1], c[1], c[2], c[2]]
-    }
-    c = '0x' + c.join('')
-    return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ',' + opacity + ')'
-  }
-  return hex
-}
-
 const getDateRange = (type: 'year' | 'quarter' | 'month') => {
-  const now = dayjs().subtract(1, 'month')
+  const now = dayjs()
   let start: dayjs.Dayjs, end: dayjs.Dayjs
 
   if (type === 'year') {
@@ -61,9 +46,9 @@ const getDateRange = (type: 'year' | 'quarter' | 'month') => {
 }
 
 const fetchData = async () => {
-  if (myChart.value) {
-    myChart.value.showLoading({
-      text: '',
+  if (myChart) {
+    myChart.showLoading({
+      text: '加载中 ...',
       color: '#409eff',
       textColor: '#B6C8DA',
       maskColor: 'rgba(0, 0, 0, 0.2)'
@@ -94,109 +79,225 @@ const fetchData = async () => {
   } catch (error) {
     console.error('Workload API Error:', error)
   } finally {
-    myChart.value?.hideLoading()
+    myChart?.hideLoading()
   }
 }
 
 const renderChart = (data: any[]) => {
-  if (!myChart.value) return
-
+  if (!myChart) return
   const isYear = currentTimeType.value === 'year'
 
-  const xAxisData = data.map((item) => (isYear ? item.reportDate : item.projectDeptName))
-
-  const series = fieldConfig.map<any>((field, index) => {
-    const color = colorPalette[index]
-
-    // 数据清洗,防止 null/undefined 报错
-    const seriesData = data.map((item) => {
-      const val = item[field.key]
-      return val === null || val === undefined || isNaN(val) ? 0 : val
-    })
-
-    console.log('seriesData :>> ', seriesData)
-
-    return {
-      name: field.name,
-      type: 'line',
-      smooth: true,
-      symbol: 'circle',
-      symbolSize: 8,
-      itemStyle: { color },
-      data: seriesData
+  // --- 高亮配色方案 ---
+  const colorPalettes = [
+    // 1. 冰蓝霓虹 (压裂井数) - 极亮青色 -> 深蓝
+    {
+      line: '#00E5FF',
+      start: '#00E5FF',
+      end: '#2979FF'
+    },
+    // 2. 日落流金 (连油井数) - 亮黄 -> 亮橙
+    {
+      line: '#FFD740',
+      start: '#FFD740',
+      end: '#FF6D00'
+    },
+    // 3. 赛博紫 (泵车台次) - 亮粉紫 -> 深紫
+    {
+      line: '#EA80FC',
+      start: '#EA80FC',
+      end: '#651FFF'
+    },
+    // 4. 极光绿 (压裂层数) - 荧光绿 -> 青绿
+    {
+      line: '#69F0AE',
+      start: '#69F0AE',
+      end: '#00C853'
     }
+  ]
 
-    if (isYear) {
-    } else {
-    }
-  })
+  const xAxisData = data.map((item) =>
+    isYear ? item.reportDate : item.projectDeptName.replace('项目部', '')
+  )
 
-  let option: echarts.EChartsOption
-
-  option = {
+  const option: echarts.EChartsOption = {
     tooltip: {
       trigger: 'axis',
+      backgroundColor: 'rgba(18, 26, 44, 0.9)', // 更深色的背景,对比更强
+      borderColor: '#409eff',
+      textStyle: { color: '#fff' },
+      padding: [10, 15],
       axisPointer: {
-        type: 'cross',
-        label: {
-          backgroundColor: '#283b56'
-        }
+        type: isYear ? 'line' : 'shadow',
+        lineStyle: { color: '#fff', type: 'dashed' },
+        shadowStyle: { color: 'rgba(255, 255, 255, 0.1)' }
       }
     },
+    legend: {
+      data: fieldConfig.map((item) => item.name),
+      textStyle: { color: '#E0E0E0' }, // 图例文字调亮
+      top: 0,
+      itemWidth: 14,
+      itemHeight: 14
+    },
     grid: {
-      top: '5%',
+      top: '18%', // 留出更多空间给图例
       left: '2%',
-      right: '5%',
-      bottom: '0%',
+      right: '2%',
+      bottom: '2%',
       containLabel: true
     },
     xAxis: {
-      type: 'category',
-      boundaryGap: false,
       data: xAxisData,
+      type: 'category',
+      boundaryGap: !isYear,
       axisLabel: {
-        color: '#B6C8DA'
-      },
-      axisLine: {
-        lineStyle: {
-          color: '#B6C8DA' // X轴线白色半透明
+        color: '#D1D5DB', // X轴文字调亮
+        interval: 0,
+        fontSize: 12,
+        formatter: (value: string) => {
+          // 如果名字太长换行显示
+          return value.length > 4 ? value.slice(0, 4) + '\n' + value.slice(4) : value
         }
-      }
+      },
+      axisLine: { lineStyle: { color: '#4B5563' } },
+      axisTick: { show: false }
     },
     yAxis: {
       type: 'value',
-      scale: true,
-      axisLabel: {
-        color: '#B6C8DA'
-      },
+      axisLabel: { color: '#D1D5DB' },
       splitLine: {
-        show: true,
-        lineStyle: {
-          color: '#457794',
-          type: 'dashed'
+        lineStyle: { color: '#374151', type: 'dashed', opacity: 0.5 } // 网格线稍微亮一点
+      }
+    },
+    series: fieldConfig.map((item, index) => {
+      const palette = colorPalettes[index % colorPalettes.length]
+
+      const seriesBase = {
+        name: item.name,
+        data: data.map((d) => {
+          const val = d[item.key]
+          return val === null || val === undefined || isNaN(val) ? 0 : val
+        })
+      }
+
+      if (isYear) {
+        // --- 折线图 (年) ---
+        return {
+          ...seriesBase,
+          type: 'line',
+          smooth: true,
+          showSymbol: false,
+          symbol: 'circle',
+          symbolSize: 8,
+          // 线条非常亮
+          lineStyle: { width: 3, color: palette.line, shadowColor: palette.line, shadowBlur: 10 },
+          itemStyle: { color: palette.line, borderColor: '#fff', borderWidth: 2 },
+          areaStyle: {
+            opacity: 0.5, // 区域透明度适中
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: palette.start }, // 顶部颜色同线条
+              { offset: 1, color: 'rgba(0,0,0,0)' }
+            ])
+          },
+          emphasis: { focus: 'series' }
         }
-      },
-      axisLine: {
-        lineStyle: {
-          color: '#B6C8DA'
+      } else {
+        // --- 柱状图 (月/季) ---
+        return {
+          ...seriesBase,
+          type: 'bar',
+          barMaxWidth: 14,
+          barGap: '30%',
+          itemStyle: {
+            borderRadius: [4, 4, 0, 0],
+            // 柱子使用实色渐变,不再过度透明,显得“脏”
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: palette.start }, // 100% 亮色
+              { offset: 1, color: palette.end } // 底部颜色,透明度由 hex 决定(这里是实色)
+            ]),
+            // 给柱子加一点光晕
+            shadowColor: palette.end,
+            shadowBlur: 5
+          },
+          emphasis: {
+            focus: 'series',
+            itemStyle: {
+              // 鼠标悬停时变得更亮
+              color: palette.line
+            }
+          }
         }
       }
-    },
-    series
+    }) as any
   }
 
-  myChart.value.setOption(option, true)
+  myChart.setOption(option)
 }
+// const renderChart = (data: any[]) => {
+//   if (!myChart) return
+//   const isYear = currentTimeType.value === 'year'
+
+//   const color = ['#5470c6', '#f1d209', '#e14f0f', '#91cc75']
+
+//   const xAxisData = data.map((item) =>
+//     isYear ? item.reportDate : item.projectDeptName.replace('项目部', '')
+//   )
+
+//   const option: echarts.EChartsOption = {
+//     tooltip: {
+//       trigger: 'axis',
+//       axisPointer: {
+//         type: isYear ? 'cross' : 'shadow',
+//         label: { backgroundColor: '#6a7985' }
+//       }
+//     },
+//     legend: {
+//       data: fieldConfig.map((item) => item.name),
+//       textStyle: { color: '#B6C8DA' }
+//     },
+//     grid: { top: '15%', left: '2%', right: '2%', bottom: '2%', containLabel: true },
+//     xAxis: {
+//       data: xAxisData,
+//       type: 'category',
+//       boundaryGap: !isYear,
+//       axisLabel: { color: '#B6C8DA', interval: 0 },
+//       axisLine: { lineStyle: { color: '#B6C8DA' } }
+//     },
+//     yAxis: {
+//       type: 'value',
+//       axisLabel: { color: '#B6C8DA' },
+//       splitLine: { lineStyle: { color: '#457794', type: 'dashed' } },
+//       axisLine: { lineStyle: { color: '#B6C8DA' } }
+//     },
+//     series: fieldConfig.map((item, index) => ({
+//       name: item.name,
+//       type: isYear ? 'line' : 'bar',
+//       smooth: true,
+//       barMaxWidth: 30,
+//       symbol: 'circle',
+//       symbolSize: 8,
+//       itemStyle: { color: color[index] },
+//       emphasis: { focus: 'series' },
+//       lineStyle: { width: 3 },
+//       data: data.map((d) => {
+//         const val = d[item.key]
+//         return val === null || val === undefined || isNaN(val) ? 0 : val
+//       })
+//     }))
+//   }
+
+//   myChart.setOption(option)
+// }
 
 const handleTimeChange = () => {
   fetchData()
 }
 
-const resizeChart = () => myChart.value?.resize()
+const resizeChart = () => myChart?.resize()
 
 onMounted(() => {
   nextTick(() => {
-    myChart.value = echarts.init(chartRef.value)
+    myChart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
     fetchData()
     window.addEventListener('resize', resizeChart)
   })
@@ -204,14 +305,14 @@ onMounted(() => {
 
 onUnmounted(() => {
   window.removeEventListener('resize', resizeChart)
-  myChart.value?.dispose()
+  myChart?.dispose()
 })
 </script>
 
 <template>
   <div class="card size-full rounded-lg p-4 flex flex-col">
     <div class="flex justify-between items-center mb-4">
-      <div class="text-[#b6c8da] text-lg font-bold">工单数量统计</div>
+      <div class="text-[#b6c8da] text-lg font-bold">工作量汇总</div>
       <el-segmented
         size="default"
         v-model="currentTimeType"