Zimo 2 дней назад
Родитель
Сommit
0758eeb8e5
4 измененных файлов с 388 добавлено и 56 удалено
  1. 1 1
      .env.local
  2. 6 0
      src/api/pms/stat/index.ts
  3. 74 55
      src/views/pms/stat/rdkb.vue
  4. 307 0
      src/views/pms/stat/rdkb/workload.vue

+ 1 - 1
.env.local

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

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

@@ -174,5 +174,11 @@ export const IotStatApi = {
   },
   getDevSta: async (params: any) => {
     return await request.get({ url: `/rq/iot-opeation-fill/getDeviceCount`, params })
+  },
+  getRdWorkload: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/summaryStatistics`, params })
+  },
+  getRdWorkloadYear: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/workloadKanban`, params })
   }
 }

+ 74 - 55
src/views/pms/stat/rdkb.vue

@@ -96,7 +96,7 @@
               gap: 6px;
             "
           >
-            <div ref="statusChartRef" style="width: 100%; max-width: 200px; height: 170px"></div>
+            <div ref="statusChartRef" style="width: 100%; height: 170px; max-width: 200px"></div>
             <div class="text-[12px] h-[100px] w-[90%] flex flex-col justify-between items-center">
               <div v-for="item in legendData" :key="item.name" class="flex">
                 <div class="flex items-center gap-1">
@@ -123,25 +123,23 @@
           </template>
           <div
             style="
-              min-height: 295px;
               display: flex;
+              min-height: 295px;
               flex-direction: column;
               align-items: center;
               gap: 8px;
             "
           >
-            <div ref="domesticChartRef" style="width: 100%; max-width: 420px; height: 200px"></div>
+            <div ref="domesticChartRef" style="width: 100%; height: 200px; max-width: 420px"></div>
             <div
               class="domestic-legend"
               style="
+                display: flex;
                 width: 100%;
                 max-width: 520px;
-                display: flex;
+                font-size: 12px;
                 flex-wrap: wrap;
-
                 gap: 1px;
-
-                font-size: 12px;
               "
             >
               <div
@@ -163,12 +161,12 @@
                 <span
                   class="legend-name"
                   style="
-                    color: #fff;
+                    display: inline-block;
                     max-width: 150px;
                     overflow: hidden;
+                    color: #fff;
                     text-overflow: ellipsis;
                     white-space: nowrap;
-                    display: inline-block;
                   "
                   >{{ item.dept }}</span
                 >
@@ -178,7 +176,8 @@
         </el-card>
       </el-col>
       <el-col :span="8" :xs="24">
-        <el-card class="chart-card" shadow="never">
+        <WorkloadChart />
+        <!-- <el-card class="chart-card" shadow="never">
           <template #header>
             <div class="flex items-center justify-between">
               <span class="text-base font-medium" style="color: #b6c8da">{{
@@ -187,7 +186,7 @@
             </div>
           </template>
           <div ref="qxRef" class="h-[290px]"></div>
-        </el-card>
+        </el-card> -->
       </el-col>
     </el-row>
 
@@ -511,6 +510,7 @@ import { CanvasRenderer } from 'echarts/renderers'
 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'
 
 /** 会员统计 */
 defineOptions({ name: 'IotRdStat' })
@@ -1744,11 +1744,29 @@ onMounted(async () => {
 })
 </script>
 <style lang="scss" scoped>
-/*最外层透明*/
+@media (width <= 768px) {
+  .page-container {
+    padding: 10px;
+  }
+}
+
+@media (width <= 520px) {
+  .status-legend-item {
+    min-width: 100%;
+  }
+
+  .status-legend {
+    justify-content: flex-start;
+    max-height: none;
+    overflow: visible;
+  }
+}
+
 ::v-deep .el-table,
 ::v-deep .el-table__expanded-cell {
   background-color: transparent !important;
 }
+
 /* 表格内背景颜色 */
 
 ::v-deep .el-table tr,
@@ -1769,13 +1787,15 @@ onMounted(async () => {
     margin-bottom: 1rem;
   }
 }
+
 .stat-card {
   width: 48%;
 }
+
 .page-container {
-  background-color: #3a6fa3;
   min-height: 100vh;
   padding: 20px;
+  background-color: #3a6fa3;
 }
 
 .summary {
@@ -1783,32 +1803,32 @@ onMounted(async () => {
 }
 
 ::v-deep .chart-card {
-  background-color: rgba(0, 0, 0, 0.3);
+  background-color: rgb(0 0 0 / 30%);
+  border: none;
   border-radius: 8px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
+  box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
   transition: all 0.3s ease;
-  border: none;
 
   &:hover {
-    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
+    box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
   }
 }
 
 // 安全生产天数卡片样式
 .safety-days-card {
   .safety-days-content {
+    position: relative;
     display: flex;
+    height: 150px;
     flex-direction: column;
     align-items: center;
     justify-content: center;
-    height: 150px;
-    position: relative;
 
     .days-number {
       font-size: 58px;
       font-weight: bold;
-      color: darkorange;
       line-height: 1;
+      color: darkorange;
       transition: all 0.3s ease;
     }
 
@@ -1817,36 +1837,32 @@ onMounted(async () => {
     }
 
     .days-label {
+      margin-top: 8px;
       font-size: 20px;
       color: white;
-      margin-top: 8px;
     }
 
     .safety-desc {
-      font-size: 14px;
-      color: #999;
       position: absolute;
       bottom: 10px;
-      text-align: center;
       width: 90%;
+      font-size: 14px;
+      color: #999;
+      text-align: center;
     }
   }
 }
 
-@media (max-width: 768px) {
-  .page-container {
-    padding: 10px;
-  }
-}
 ::v-deep .el-card__header {
-  border-bottom: none !important;
   padding-bottom: 0;
+  border-bottom: none !important;
 }
+
 .table-container {
-  padding: 16px;
   height: 420px;
-  box-sizing: border-box;
+  padding: 16px;
   overflow: auto;
+  box-sizing: border-box;
 
   // 滚动条样式优化
   &::-webkit-scrollbar {
@@ -1855,7 +1871,7 @@ onMounted(async () => {
   }
 
   &::-webkit-scrollbar-thumb {
-    background-color: rgba(255, 255, 255, 0.2);
+    background-color: rgb(255 255 255 / 20%);
     border-radius: 3px;
   }
 
@@ -1866,63 +1882,71 @@ onMounted(async () => {
 
 // 修复表格hover样式
 ::v-deep .el-table__row:hover > td {
-  background-color: rgba(255, 255, 255, 0.05) !important;
+  background-color: rgb(255 255 255 / 5%) !important;
 }
+
 .custom-scroll-dialog {
   /* 可选:限制对话框整体最大高度(避免超出屏幕) */
   max-height: 90vh;
   overflow: hidden; /* 隐藏整体溢出,避免出现双重滚动条 */
 }
+
 /* 滚动内容容器:核心样式 */
 .dialog-scroll-content {
   max-height: 60vh; /* 固定最大高度(可根据需求调整,如500px) */
-  overflow-y: auto; /* 垂直方向溢出时显示滚动条 */
   padding-right: 8px; /* 避免滚动条遮挡内容(可选) */
+  overflow-y: auto; /* 垂直方向溢出时显示滚动条 */
 }
 
 /* 优化滚动条样式(可选,提升UI体验) */
 .dialog-scroll-content::-webkit-scrollbar {
   width: 6px; /* 滚动条宽度 */
 }
+
 .dialog-scroll-content::-webkit-scrollbar-thumb {
   background-color: #e5e7eb; /* 滚动条滑块颜色 */
   border-radius: 3px; /* 滚动条圆角 */
 }
+
 .dialog-scroll-content::-webkit-scrollbar-thumb:hover {
   background-color: #d1d5db; /*  hover时滑块颜色 */
 }
+
 .custom-table :deep .el-table__row {
   height: 50px !important; /* 高度根据需求调整 */
 }
 
 /* 设备状态图例自适应样式 */
 .status-legend {
-  width: 100%;
   display: flex;
+  width: 100%;
+  max-height: 90px; /* 限制高度,超出显示滚动 */
+  padding: 6px 0;
+  overflow-y: auto;
+  box-sizing: border-box;
   flex-wrap: wrap;
   justify-content: center;
   gap: 8px;
-  padding: 6px 0;
-  box-sizing: border-box;
-  max-height: 90px; /* 限制高度,超出显示滚动 */
-  overflow-y: auto;
 }
+
 .status-legend-item {
   display: flex;
+  max-width: 100%;
+  min-width: 120px;
+  padding: 6px 10px;
+  box-sizing: border-box;
   align-items: center;
   justify-content: space-between;
   gap: 12px;
-  padding: 6px 10px;
-  min-width: 120px;
-  max-width: 100%;
-  box-sizing: border-box;
 }
+
 .status-legend-left {
   display: flex;
   align-items: center;
   gap: 8px;
   min-width: 0;
 }
+
 .status-legend-color {
   display: inline-block;
   width: 12px;
@@ -1930,35 +1954,30 @@ onMounted(async () => {
   border-radius: 50%;
   flex: 0 0 12px;
 }
+
 .status-legend-name {
   max-width: calc(100% - 60px);
   overflow: hidden;
+  color: #fff;
   text-overflow: ellipsis;
   white-space: nowrap;
-  color: #fff;
 }
+
 .status-legend-right {
   display: flex;
   align-items: center;
   gap: 8px;
   flex-shrink: 0;
 }
+
 .status-legend-value {
   font-weight: 700;
   color: #fff;
 }
+
 .status-legend-percent {
   color: #fff;
 }
 
-@media (max-width: 520px) {
-  .status-legend-item {
-    min-width: 100%;
-  }
-  .status-legend {
-    justify-content: flex-start;
-    max-height: none;
-    overflow: visible;
-  }
-}
+/* 最外层透明 */
 </style>

+ 307 - 0
src/views/pms/stat/rdkb/workload.vue

@@ -0,0 +1,307 @@
+<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)
+const myChart = ref<echarts.EChartsType | null>(null)
+const currentTimeType = ref('month')
+
+const timeOptions = [
+  { label: '本月', value: 'month' },
+  { label: '本季度', value: 'quarter' },
+  { label: '本年', value: 'year' }
+]
+
+const fieldConfig = [
+  { key: 'ylWellCount', name: '压裂井数' },
+  { key: 'lyWellCount', name: '连油井数' },
+  { key: 'cumulativePumpTrips', name: '泵车台次' },
+  { 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')
+  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.value) {
+    myChart.value.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 = {
+    deptId: 163,
+    'createTime[0]': start,
+    'createTime[1]': end,
+    timeType: currentTimeType.value
+  }
+
+  try {
+    let list: any[] = []
+
+    if (currentTimeType.value === 'year') {
+      const res = await IotStatApi.getRdWorkloadYear(params)
+      if (res && Array.isArray(res)) list = res
+    } else {
+      const res = await IotStatApi.getRdWorkload(params)
+      if (res && res.list) list = res.list
+    }
+
+    renderChart(list)
+  } catch (error) {
+    console.error('Workload API Error:', error)
+  } finally {
+    myChart.value?.hideLoading()
+  }
+}
+
+const renderChart = (data: any[]) => {
+  if (!myChart.value) return
+
+  const isYear = currentTimeType.value === 'year'
+
+  const xAxisData = data.map((item) => (isYear ? item.reportDate : item.projectDeptName))
+
+  const series = fieldConfig.map((field, index) => {
+    const color = colorPalette[index % colorPalette.length]
+
+    // 数据清洗,防止 null/undefined 报错
+    const seriesData = data.map((item) => {
+      const val = item[field.key]
+      return val === null || val === undefined || isNaN(val) ? 0 : val
+    })
+
+    if (isYear) {
+      return {
+        name: field.name,
+        type: 'line',
+        smooth: true,
+        symbol: 'circle',
+        symbolSize: 6,
+        showSymbol: false,
+        itemStyle: {
+          color: color
+        },
+        lineStyle: {
+          width: 2
+        },
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: hexToRgba(color, 0.4) }, // 顶部半透明
+            { offset: 1, color: hexToRgba(color, 0.05) } // 底部几近透明
+          ])
+        },
+        data: seriesData
+      }
+    } else {
+      // --- 柱状图模式 (增加立体感) ---
+      return {
+        name: field.name,
+        type: 'bar',
+        barMaxWidth: 16, // 柱子不要太宽
+        itemStyle: {
+          borderRadius: [4, 4, 0, 0], // 顶部圆角
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: color }, // 顶部纯色
+            { offset: 1, color: hexToRgba(color, 0.4) } // 底部半透明
+          ])
+        },
+        data: seriesData
+      }
+    }
+  })
+
+  const option = {
+    // 提示框样式优化
+    tooltip: {
+      trigger: 'axis',
+      backgroundColor: 'rgba(50,50,50,0.8)', // 深灰色背景
+      borderColor: '#457794', // 边框呼应网格颜色
+      textStyle: {
+        color: '#fff'
+      },
+      axisPointer: {
+        type: 'cross',
+        label: {
+          backgroundColor: '#6a7985'
+        },
+        lineStyle: {
+          color: '#B6C8DA',
+          type: 'dashed'
+        }
+      }
+    },
+    legend: {
+      data: fieldConfig.map((f) => f.name),
+      top: 0, // 紧贴顶部
+      icon: isYear ? 'circle' : 'roundRect', // 图例图标随图表类型变化
+      textStyle: {
+        color: '#B6C8DA'
+      }
+    },
+    grid: {
+      left: '2%',
+      right: '3%',
+      bottom: '3%',
+      top: '12%', // 留出一点空间给图例
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      // 折线图 false (贴边),柱状图 true (留白)
+      boundaryGap: !isYear,
+      data: xAxisData,
+      axisLabel: {
+        color: '#B6C8DA',
+        formatter: (val: string) => {
+          // 如果名字太长,可以截断或者换行,这里简单处理
+          return val.length > 6 ? val.slice(0, 6) + '...' : val
+        }
+      },
+      axisLine: {
+        lineStyle: {
+          color: '#B6C8DA'
+        }
+      },
+      axisTick: {
+        show: false // 隐藏刻度线,更简洁
+      }
+    },
+    // 单 Y 轴配置
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        color: '#B6C8DA',
+        formatter: '{value}'
+      },
+      // 复刻原图的虚线网格
+      splitLine: {
+        show: true,
+        lineStyle: {
+          color: '#457794', // 蓝灰色
+          type: 'dashed', // 虚线
+          opacity: 0.5 // 半透明
+        }
+      },
+      axisLine: {
+        show: true, // 显示轴线
+        lineStyle: {
+          color: '#B6C8DA'
+        }
+      }
+    },
+    series: series
+  }
+
+  myChart.value.setOption(option, true)
+}
+
+const handleTimeChange = () => {
+  fetchData()
+}
+
+const resizeChart = () => myChart.value?.resize()
+
+onMounted(() => {
+  nextTick(() => {
+    myChart.value = echarts.init(chartRef.value)
+    fetchData()
+    window.addEventListener('resize', resizeChart)
+  })
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  myChart.value?.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>