yanghao před 10 hodinami
rodič
revize
81ee364c20

+ 11 - 0
src/api/pms/iotrddailyreport/index.ts

@@ -102,5 +102,16 @@ export const IotRdDailyReportApi = {
   // 查询项目任务实际进度列表
   taskActualProgress: async (params: any) => {
     return await request.get({ url: `/pms/iot-rd-daily-report/taskActualProgress`, params })
+  },
+
+  totalWorkload: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/totalWorkload`, params })
+  },
+
+  getIotRdDailyReportSummary: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/summaryStatistics`, params })
+  },
+  getIotRdDailyReportSummaryPolyline: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/polylineStatistics`, params })
   }
 }

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

@@ -7,5 +7,11 @@ export const IotReportApi = {
 
   getCostsPage: async (params: any) => {
     return await request.get({ url: `/rq/report/repair/page`, params })
+  },
+  getOrderNumber: async (params: any) => {
+    return await request.get({ url: `/rq/report/order/number`, params })
+  },
+  getOrderPage: async (params: any) => {
+    return await request.get({ url: `/rq/report/order/page`, params })
   }
 }

+ 1 - 1
src/api/system/dept/index.ts

@@ -27,7 +27,7 @@ export const specifiedSimpleDepts = async (deptId: number): Promise<DeptVO[]> =>
 
 export const getTaskWellNames = async (deptId: number, wellName: string) => {
   return await request.get({
-    url: `/rq/iot-project-task/taskWellNames?companyId=${deptId}&wellName=${wellName}`
+    url: `/rq/iot-project-task/taskTreeWellNames?companyId=${deptId}&wellName=${wellName}`
   })
 }
 

+ 83 - 69
src/components/WellSelect/index.vue

@@ -1,5 +1,4 @@
 <script lang="ts" setup>
-import { defaultProps } from '@/utils/tree'
 import { ElTree } from 'element-plus'
 import * as DeptApi from '@/api/system/dept'
 import { Search } from '@element-plus/icons-vue'
@@ -13,92 +12,111 @@ const props = defineProps({
     type: String,
     default: undefined
   },
+  contractName: {
+    type: String,
+    default: undefined
+  },
   title: {
     type: String,
     default: '井'
   }
-  // topId: {
-  //   type: Number,
-  //   required: true
-  // }
 })
 
-const emits = defineEmits(['update:modelValue', 'node-click'])
+const emits = defineEmits(['update:modelValue', 'node-click', 'update:contractName'])
 
 const wellName = ref('')
 
-const deptList = ref<Tree[]>([])
+interface Tree {
+  label: string
+  value: string
+  children?: Tree[]
+  type: '1' | '2'
+  rawData: any
+}
 
+const deptList = ref<Tree[]>([])
 const treeRef = ref<InstanceType<typeof ElTree>>()
-
-// const expandedKeys = ref<string[]>([])
-
-// const sortTreeBySort = (treeNodes: Tree[]) => {
-//   if (!treeNodes || !Array.isArray(treeNodes)) return treeNodes
-//   const sortedNodes = [...treeNodes].sort((a, b) => {
-//     const sortA = a.sort != null ? a.sort : 999999
-//     const sortB = b.sort != null ? b.sort : 999999
-//     return sortA - sortB
-//   })
-
-//   sortedNodes.forEach((node) => {
-//     if (node.children && Array.isArray(node.children)) {
-//       node.children = sortTreeBySort(node.children)
-//     }
-//   })
-//   return sortedNodes
-// }
+const expandedKeys = ref<string[]>([])
 
 const loadTree = async () => {
   try {
-    // let id = props.deptId
-
-    // if (id !== props.topId) {
-    //   const depts = await DeptApi.specifiedSimpleDepts(props.topId)
-
-    //   const self = depts.find((item) => item.id === props.deptId)
-
-    //   if (depts.length && !self) {
-    //     id = props.topId
-    //   }
-    // }
-
-    // emits('update:modelValue', id)
-
     const res = await DeptApi.getTaskWellNames(props.deptId, wellName.value)
-    deptList.value = res.map((item) => ({
-      id: item.wellName,
-      name: item.wellName
-    }))
-
-    emits('update:modelValue', deptList.value[0]?.id || '')
-
-    // // 加载完成后,如果有选中值,尝试高亮并展开
-    // nextTick(() => {
-    //   if (props.modelValue && treeRef.value) {
-    //     treeRef.value.setCurrentKey(props.modelValue)
-    //     expandedKeys.value = [props.modelValue] // 默认展开选中的节点
-    //   } else if (deptList.value.length > 0) {
-    //     // 默认展开第一级
-    //     expandedKeys.value = deptList.value.map((item) => item.id.toString())
-    //   }
-    // })
+
+    // --- 数据处理开始 ---
+    const parentMap = new Map<number, Tree>()
+    const treeData: Tree[] = []
+
+    // 第一步:先找出所有的 Type 1 (父级),建立映射
+    res.forEach((item: any) => {
+      if (item.type === '1') {
+        const node: Tree = {
+          label: item.projectName,
+          value: item.projectName,
+          type: '1',
+          children: [],
+          rawData: item
+        }
+        parentMap.set(item.projectId, node)
+        treeData.push(node)
+      }
+    })
+
+    res.forEach((item: any) => {
+      if (item.type === '2') {
+        const parent = parentMap.get(item.projectId)
+        const childNode: Tree = {
+          label: item.wellName,
+          value: item.wellName,
+          type: '2',
+          rawData: item
+        }
+        if (parent) {
+          parent.children?.push(childNode)
+        }
+      }
+    })
+
+    deptList.value = treeData
+
+    if (!props.modelValue && treeData.length > 0 && treeData[0].children?.length) {
+      const firstChild = treeData[0].children[0]
+      emits('update:modelValue', firstChild.value)
+
+      nextTick(() => {
+        if (treeRef.value) {
+          treeRef.value.setCurrentKey(firstChild.value)
+          expandedKeys.value = [treeData[0].value]
+        }
+      })
+    } else if (props.modelValue) {
+      nextTick(() => {
+        if (treeRef.value) {
+          treeRef.value.setCurrentKey(props.modelValue)
+          expandedKeys.value = treeData.map((node) => node.value)
+        }
+      })
+    }
   } catch (error) {
     console.error('加载井名失败:', error)
   }
 }
 
 const handleNodeClick = (data: Tree) => {
-  // 1. 更新 v-model
-  emits('update:modelValue', data.id)
-  // 2. 抛出点击事件供父组件其他用途
+  if (data.type === '1') {
+    emits('update:contractName', data.value)
+    emits('update:modelValue', '')
+  } else if (data.type === '2') {
+    emits('update:modelValue', data.value)
+    emits('update:contractName', '')
+  }
+
   emits('node-click', data)
 }
 
 /** 筛选节点逻辑 */
 const filterNode = (value: string, data: Tree) => {
   if (!value) return true
-  return data.name.includes(value)
+  return data.label.includes(value)
 }
 
 /** 监听输入框进行过滤 */
@@ -119,20 +137,16 @@ watch(
   () => props.modelValue,
   (newVal) => {
     if (newVal && treeRef.value) {
-      // 设置高亮
       treeRef.value.setCurrentKey(newVal)
-      // 自动展开该节点 (将新ID加入展开数组)
-      // if (!expandedKeys.value.includes(newVal)) {
-      //   expandedKeys.value.push(newVal)
-      // }
+      if (!expandedKeys.value.includes(newVal)) {
+        expandedKeys.value.push(newVal)
+      }
     }
   },
   { immediate: true }
 )
 
-/** 初始化 */
 onMounted(() => {
-  console.log('props :>> ', props)
   loadTree()
 })
 </script>
@@ -154,11 +168,11 @@ onMounted(() => {
             <el-tree
               ref="treeRef"
               :data="deptList"
-              :props="defaultProps"
               :expand-on-click-node="false"
               :filter-node-method="filterNode"
-              node-key="id"
+              node-key="value"
               highlight-current
+              :default-expanded-keys="expandedKeys"
               @node-click="handleNodeClick"
             />
           </el-scrollbar>

+ 179 - 0
src/components/WorkOrderCompletionBar/index.vue

@@ -0,0 +1,179 @@
+<!-- src/components/MiniBarChart.vue -->
+<script lang="ts" setup>
+import { ref, onMounted, watch, onUnmounted, markRaw } from 'vue'
+import * as echarts from 'echarts/core'
+import { BarChart, type BarSeriesOption } from 'echarts/charts'
+import {
+  GridComponent,
+  type GridComponentOption,
+  TooltipComponent,
+  type TooltipComponentOption
+} from 'echarts/components'
+import { CanvasRenderer } from 'echarts/renderers'
+
+// 注册必须的组件
+echarts.use([BarChart, GridComponent, TooltipComponent, CanvasRenderer])
+
+type EChartsOption = echarts.ComposeOption<
+  BarSeriesOption | GridComponentOption | TooltipComponentOption
+>
+
+export interface ChartDataItem {
+  label: string
+  num: number
+}
+
+const props = defineProps<{
+  items: ChartDataItem[]
+  max?: number // 变成可选,如果不传自动计算
+}>()
+
+const chartRef = ref<HTMLElement>()
+let chartInstance: echarts.ECharts | null = null
+
+// 预定义5种渐变色方案 (Tailwind 风格)
+// 顺序:蓝、绿、橙、红、紫
+const colorPalette = [
+  // Blue
+  [
+    { offset: 0, color: 'rgb(96, 165, 250)' }, // Blue 400
+    { offset: 1, color: 'rgb(59, 130, 246)' } // Blue 500
+  ],
+  // Emerald (Green)
+  [
+    { offset: 0, color: 'rgb(52, 211, 153)' }, // Emerald 400
+    { offset: 1, color: 'rgb(16, 185, 129)' } // Emerald 500
+  ],
+  // Orange
+  [
+    { offset: 0, color: 'rgb(251, 146, 60)' }, // Orange 400
+    { offset: 1, color: 'rgb(249, 115, 22)' } // Orange 500
+  ],
+  // Rose (Red)
+  [
+    { offset: 0, color: 'rgb(251, 113, 133)' }, // Rose 400
+    { offset: 1, color: 'rgb(244, 63, 94)' } // Rose 500
+  ],
+  // Purple
+  [
+    { offset: 0, color: 'rgb(192, 132, 252)' }, // Purple 400
+    { offset: 1, color: 'rgb(168, 85, 247)' } // Purple 500
+  ]
+]
+
+const initChart = () => {
+  if (!chartRef.value) return
+  chartInstance = markRaw(echarts.init(chartRef.value))
+  updateChart()
+  window.addEventListener('resize', handleResize)
+}
+
+const handleResize = () => {
+  chartInstance?.resize()
+}
+
+const updateChart = () => {
+  if (!chartInstance) return
+
+  // 1. 提取 X 轴 Label
+  const xAxisData = props.items.map((item) => item.label)
+
+  // 2. 构建 Series Data 并分配颜色
+  const seriesData = props.items.map((item, index) => {
+    // 循环取色:如果 index 超过 4,则回到 0
+    const colorStops = colorPalette[index % colorPalette.length]
+
+    return {
+      value: item.num,
+      name: item.label,
+      itemStyle: {
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, colorStops),
+        borderRadius: [4, 4, 4, 4] // 顶部圆角
+      }
+    }
+  })
+
+  const option: EChartsOption = {
+    tooltip: {
+      trigger: 'item',
+      confine: true,
+      backgroundColor: 'rgba(50,50,50,0.9)',
+      borderColor: '#333',
+      textStyle: { color: '#fff', fontSize: 12 }
+    },
+    grid: {
+      top: '15%', // 留出上方数值显示空间
+      bottom: '5%', // 调整底部文字空间
+      left: '-20%',
+      right: '0%',
+      containLabel: true // 改为 true 自动计算 Label 空间,防止被切
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxisData,
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: {
+        color: '#6b7280',
+        fontSize: 10, // 字体稍大一点
+        interval: 0, // 强制显示所有标签
+        lineHeight: 14,
+        margin: 8
+        // overflow: 'break' // 文字过长换行
+        // formatter: function (value: string) {
+        //   if (value.length > 3) {
+        //     const mid = Math.ceil(value.length / 2)
+        //     return value.slice(0, mid) + '\n' + value.slice(mid)
+        //   }
+        //   return value
+        // }
+      }
+    },
+    yAxis: {
+      type: 'log',
+      show: false
+      // max: yAxisMax as number
+    },
+    series: [
+      {
+        type: 'bar',
+        barWidth: '35%', // 稍微变窄一点,适应5个柱子的情况
+        label: {
+          show: true,
+          position: 'top',
+          fontSize: 12,
+          fontWeight: 'bold',
+          color: '#374151' // 数值颜色
+        },
+        data: seriesData,
+        animationDuration: 1000,
+        animationEasing: 'cubicOut'
+      }
+    ]
+  }
+
+  chartInstance.setOption(option)
+}
+
+// 监听 items 数组的变化
+watch(
+  () => props.items,
+  () => {
+    updateChart()
+  },
+  { deep: true } // 深度监听数组内部对象变化
+)
+
+onMounted(() => {
+  setTimeout(initChart, 50)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize)
+  chartInstance?.dispose()
+})
+</script>
+
+<template>
+  <div ref="chartRef" class="w-full h-full"></div>
+</template>

+ 3 - 1
src/views/pms/iotmainworkorder/index.vue

@@ -113,6 +113,7 @@
             :label="t('iotDevice.serial')"
             align="center"
             :width="columnWidths.serial"
+            fixed="left"
           >
             <template #default="scope">
               {{ scope.$index + 1 }}
@@ -123,6 +124,7 @@
             align="center"
             prop="name"
             :width="columnWidths.name"
+            fixed="left"
           />
           <el-table-column
             :label="t('iotDevice.dept')"
@@ -295,7 +297,7 @@
 </template>
 
 <script setup lang="ts">
-import { dateFormatter2 } from '@/utils/formatTime'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
 import IotMainWorkOrderForm from './IotMainWorkOrderForm.vue'

+ 675 - 0
src/views/pms/iotrddailyreport/summary.vue

@@ -0,0 +1,675 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { IotRdDailyReportApi } from '@/api/pms/iotrddailyreport'
+import { useDebounceFn } from '@vueuse/core'
+import CountTo from '@/components/count-to1.vue'
+import * as echarts from 'echarts'
+// import UnfilledReportDialog from './UnfilledReportDialog.vue'
+
+import { Motion, AnimatePresence } from 'motion-v'
+
+import { rangeShortcuts } from '@/utils/formatTime'
+
+import { useUserStore } from '@/store/modules/user'
+
+const deptId = useUserStore().getUser.deptId
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+}
+
+const id = deptId
+
+const query = ref<Query>({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: deptId,
+  createTime: [
+    ...rangeShortcuts[2].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
+  ]
+})
+
+const totalWorkKeys: [string, string | undefined, string, string, number][] = [
+  [
+    'cumulativeFuels',
+    '万升',
+    '累计油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
+  ['taici', undefined, '台次', 'i-material-symbols:check-circle-outline-rounded text-emerald', 0],
+  [
+    'cumulativeBridgePlug',
+    undefined,
+    '个数',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  [
+    'cumulativeRunCount',
+    undefined,
+    '趟数',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  [
+    'cumulativeWorkingWell',
+    undefined,
+    '井数',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  [
+    'cumulativeWorkingLayers',
+    undefined,
+    '段数',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  [
+    'cumulativeHourCount',
+    undefined,
+    'H',
+    'i-material-symbols:nest-clock-farsight-analog-outline-rounded text-emerald',
+    2
+  ]
+]
+
+const totalWork = ref({
+  cumulativeFuels: 0,
+  taici: 0,
+  cumulativeBridgePlug: 0,
+  cumulativeRunCount: 0,
+  cumulativeWorkingWell: 0,
+  cumulativeWorkingLayers: 0,
+  cumulativeHourCount: 0
+})
+
+const totalLoading = ref(false)
+
+const getTotal = useDebounceFn(async () => {
+  totalLoading.value = true
+
+  const { pageNo, pageSize, ...other } = query.value
+
+  try {
+    // let res1: any[]
+    // if (query.value.createTime && query.value.createTime.length === 2) {
+    //   res1 = await IotRhDailyReportApi.rhDailyReportStatistics({
+    //     createTime: query.value.createTime,
+    //     deptId: query.value.deptId
+    //   })
+
+    //   totalWork.value.totalCount = res1[0].count
+    //   totalWork.value.alreadyReported = res1[1].count
+    //   totalWork.value.notReported = res1[2].count
+    // }
+
+    const res2 = await IotRdDailyReportApi.totalWorkload(other)
+
+    totalWork.value = {
+      ...totalWork.value,
+      taici: res2.taici || 0,
+      cumulativeBridgePlug: res2.cumulativeBridgePlug || 0,
+      cumulativeRunCount: res2.cumulativeRunCount || 0,
+      cumulativeWorkingWell: res2.cumulativeWorkingWell || 0,
+      cumulativeWorkingLayers: res2.cumulativeWorkingLayers || 0,
+      cumulativeHourCount: res2.cumulativeHourCount || 0,
+      ...res2,
+      cumulativeFuels: (res2.cumulativeFuels || 0) / 10000
+    }
+  } finally {
+    totalLoading.value = false
+  }
+}, 500)
+
+interface List {
+  id: number | null
+  name: string | null
+  type: '1' | '2' | '3'
+  cumulativeBridgePlug: number | null
+  cumulativeRunCount: number | null
+  cumulativeWorkingWell: number | null
+  cumulativeHourCount: number | null
+  totalDailyFuel: number | null
+  cumulativeWaterVolume: number | null
+  cumulativeWorkingLayers: number | null
+  cumulativePumpTrips: number | null
+  cumulativeMixSand: number | null
+}
+
+const list = ref<List[]>([])
+
+const type = ref('2')
+
+const columns = (type: string) => {
+  return [
+    {
+      label: type === '2' ? '项目部' : '队伍',
+      prop: 'name'
+    },
+    {
+      label: '桥塞',
+      prop: 'cumulativeBridgePlug'
+    },
+    {
+      label: '趟数',
+      prop: 'cumulativeRunCount'
+    },
+    {
+      label: '井数',
+      prop: 'cumulativeWorkingWell'
+    },
+    {
+      label: '小时(H)',
+      prop: 'cumulativeHourCount'
+    },
+    {
+      label: '油耗(万升)',
+      prop: 'totalDailyFuel'
+    },
+    {
+      label: '水方量(方)',
+      prop: 'cumulativeWaterVolume'
+    },
+    {
+      label: '段数',
+      prop: 'cumulativeWorkingLayers'
+    }
+  ]
+}
+
+const listLoading = ref(false)
+
+// const formatter = (row: List, column: any) => {
+//   if (column.property === 'transitTime') {
+//     return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
+//   } else return row[column.property] ?? 0
+// }
+
+const getList = useDebounceFn(async () => {
+  listLoading.value = true
+  try {
+    const res = await IotRdDailyReportApi.getIotRdDailyReportSummary(query.value)
+
+    const { list: reslist } = res
+
+    type.value = reslist[0]?.type || '2'
+
+    list.value = reslist.map(
+      ({ id, projectDeptId, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => {
+        return {
+          id: type === '2' ? projectDeptId : teamId,
+          name: type === '2' ? projectDeptName : teamName,
+          ...other,
+          cumulativeBridgePlug: other.cumulativeBridgePlug || 0,
+          cumulativeRunCount: other.cumulativeRunCount || 0,
+          cumulativeWorkingWell: other.cumulativeWorkingWell || 0,
+          cumulativeHourCount: other.cumulativeHourCount || 0,
+          totalDailyFuel: ((other.totalDailyFuel || 0) / 10000).toFixed(4),
+          cumulativeWaterVolume: other.cumulativeWaterVolume || 0,
+          cumulativeWorkingLayers: other.cumulativeWorkingLayers || 0,
+          cumulativePumpTrips: other.cumulativePumpTrips || 0,
+          cumulativeMixSand: other.cumulativeMixSand || 0
+        }
+      }
+    )
+  } finally {
+    listLoading.value = false
+  }
+}, 500)
+
+const tab = ref<'表格' | '看板'>('表格')
+
+const currentTab = ref<'表格' | '看板'>('表格')
+
+const deptName = ref('瑞恒兴域')
+
+const direction = ref<'left' | 'right'>('right')
+
+const handleSelectTab = (val: '表格' | '看板') => {
+  tab.value = val
+  direction.value = val === '看板' ? 'right' : 'left'
+  nextTick(() => {
+    currentTab.value = val
+    setTimeout(() => {
+      render()
+    })
+  })
+}
+
+const chartRef = ref<HTMLDivElement | null>(null)
+let chart: echarts.ECharts | null = null
+
+const xAxisData = ref<string[]>([])
+
+const legend = ref<string[][]>([
+  ['个数', 'cumulativeBridgePlug'],
+  ['井数', 'cumulativeWorkingWell'],
+  ['小时 (H)', 'cumulativeHourCount'],
+  ['油耗 (万升)', 'totalDailyFuel'],
+  ['水方量 (方)', 'cumulativeWaterVolume'],
+  ['台次(泵车)', 'cumulativePumpTrips'],
+  ['段数', 'cumulativeWorkingLayers'],
+  ['台次(仪表/混砂)', 'cumulativeMixSand']
+])
+
+const chartData = ref<Record<string, number[]>>({
+  cumulativeFuelConsumption: [],
+  cumulativeGasInjection: [],
+  cumulativePowerConsumption: [],
+  cumulativeWaterInjection: [],
+  transitTime: []
+})
+
+let chartLoading = ref(false)
+
+const getChart = useDebounceFn(async () => {
+  chartLoading.value = true
+
+  try {
+    const res = await IotRdDailyReportApi.getIotRdDailyReportSummaryPolyline(query.value)
+
+    chartData.value = {
+      cumulativeBridgePlug: res.map((item) => item.cumulativeBridgePlug || 0),
+      cumulativeWorkingWell: res.map((item) => item.cumulativeWorkingWell || 0),
+      cumulativeHourCount: res.map((item) => item.cumulativeHourCount || 0),
+      totalDailyFuel: res.map((item) => (item.totalDailyFuel || 0) / 10000),
+      cumulativeWaterVolume: res.map((item) => item.cumulativeWaterVolume || 0),
+      cumulativeWorkingLayers: res.map((item) => item.cumulativeWorkingLayers || 0),
+      cumulativePumpTrips: res.map((item) => item.cumulativePumpTrips || 0),
+      cumulativeMixSand: res.map((item) => item.cumulativeMixSand || 0)
+    }
+
+    xAxisData.value = res.map((item) => item.reportDate || '')
+  } finally {
+    chartLoading.value = false
+  }
+}, 500)
+
+const resizer = () => {
+  chart?.resize()
+}
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizer)
+})
+
+const render = () => {
+  if (!chartRef.value) return
+
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+
+  window.addEventListener('resize', resizer)
+
+  const values: number[] = []
+
+  for (const [_name, key] of legend.value) {
+    values.push(...(chartData.value[key] || []))
+  }
+
+  const maxVal = values.length === 0 ? 10000 : Math.max(...values)
+  const minVal = values.length === 0 ? 0 : Math.min(...values) > 0 ? 0 : Math.min(...values)
+
+  const maxDigits = (Math.floor(maxVal) + '').length
+  const minDigits = minVal === 0 ? 0 : (Math.floor(Math.abs(minVal)) + '').length
+  const interval = Math.max(maxDigits, minDigits)
+
+  const maxInterval = interval
+  const minInterval = minDigits
+
+  const intervalArr = [0]
+  for (let i = 1; i <= interval; i++) {
+    intervalArr.push(Math.pow(10, i))
+  }
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'line'
+      },
+      formatter: (params) => {
+        let d = `${params[0].axisValueLabel}<br>`
+        let item = params.map((el) => {
+          return `<div class="flex items-center justify-between mt-1 gap-1">
+            <span>${el.marker} ${el.seriesName}</span>
+            <span>${chartData.value[legend.value[el.componentIndex][1]][el.dataIndex].toFixed(2)} ${el.seriesName.split(' ')[1] ?? ''}</span>
+          </div>`
+        })
+
+        return d + item.join('')
+      }
+    },
+    legend: {
+      data: legend.value.map(([name]) => name),
+      show: true
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxisData.value
+    },
+    yAxis: {
+      type: 'value',
+      min: -minInterval,
+      max: maxInterval,
+      interval: 1,
+      axisLabel: {
+        formatter: (v) => {
+          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
+
+          return num.toLocaleString()
+        }
+      }
+    },
+    series: legend.value.map(([name, key]) => ({
+      name,
+      type: 'line',
+      smooth: true,
+      showSymbol: true,
+      data: chartData.value[key].map((value) => {
+        // return value
+        if (value === 0) return 0
+
+        const isPositive = value > 0
+        const absItem = Math.abs(value)
+
+        const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
+        const min_index = intervalArr.findIndex((v) => v === min_value)
+
+        const new_value =
+          (absItem - min_value) / (intervalArr[min_index + 1] - intervalArr[min_index]) + min_index
+
+        return isPositive ? new_value : -new_value
+      })
+    }))
+  })
+}
+
+const handleDeptNodeClick = (node: any) => {
+  deptName.value = node.name
+  handleQuery()
+}
+
+const handleQuery = (setPage = true) => {
+  if (setPage) {
+    query.value.pageNo = 1
+  }
+  getChart().then(() => {
+    render()
+  })
+  getList()
+  getTotal()
+}
+
+const resetQuery = () => {
+  query.value = {
+    pageNo: 1,
+    pageSize: 10,
+    deptId: deptId,
+    contractName: '',
+    taskName: '',
+    createTime: []
+  }
+  handleQuery()
+}
+
+watch(
+  () => query.value.createTime,
+  () => {
+    // if (!val) {
+    //   totalWork.value.totalCount = 0
+    //   totalWork.value.notReported = 0
+    //   totalWork.value.alreadyReported = 0
+    // }
+    handleQuery(false)
+  }
+)
+
+watch([() => query.value.contractName, () => query.value.taskName], () => {
+  handleQuery(false)
+})
+
+onMounted(() => {
+  handleQuery()
+})
+
+const exportChart = () => {
+  if (!chart) return
+  let img = new Image()
+  img.src = chart.getDataURL({
+    type: 'png',
+    pixelRatio: 1,
+    backgroundColor: '#fff'
+  })
+
+  img.onload = function () {
+    let canvas = document.createElement('canvas')
+    canvas.width = img.width
+    canvas.height = img.height
+    let ctx = canvas.getContext('2d')
+    ctx?.drawImage(img, 0, 0)
+    let dataURL = canvas.toDataURL('image/png')
+
+    let a = document.createElement('a')
+
+    let event = new MouseEvent('click')
+
+    a.href = dataURL
+    a.download = `瑞恒日报统计数据.png`
+    a.dispatchEvent(event)
+  }
+}
+
+const exportData = async () => {
+  // const res = await IotRdDailyReportApi.exportRdDailyReportStatistics(query.value)
+  // download.excel(res, '瑞恒日报统计数据.xlsx')
+}
+
+const exportAll = async () => {
+  if (tab.value === '看板') exportChart()
+  else exportData()
+}
+
+const router = useRouter()
+
+const tolist = (id: number) => {
+  const { pageNo, pageSize, ...rest } = query.value
+
+  router.push({
+    path: '/iotdayilyreport/IotRhDailyReport',
+    query: {
+      ...rest,
+      deptId: id
+    }
+  })
+}
+</script>
+
+<template>
+  <div class="grid grid-cols-[16%_1fr] gap-5">
+    <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-4">
+      <!-- <DeptTree2 :deptId="id" @node-click="handleDeptNodeClick" /> -->
+      <DeptTreeSelect
+        :deptId="id"
+        :top-id="163"
+        v-model="query.deptId"
+        @node-click="handleDeptNodeClick"
+      />
+    </div>
+    <div class="grid grid-rows-[62px_164px_1fr] h-full gap-4">
+      <el-form
+        size="default"
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
+      >
+        <div class="flex items-center gap-8">
+          <el-form-item label="项目">
+            <el-input
+              v-model="query.contractName"
+              placeholder="请输入项目"
+              clearable
+              @keyup.enter="handleQuery()"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="任务">
+            <el-input
+              v-model="query.taskName"
+              placeholder="请输入任务"
+              clearable
+              @keyup.enter="handleQuery()"
+              class="!w-240px"
+            />
+          </el-form-item>
+          <el-form-item label="创建时间">
+            <el-date-picker
+              v-model="query.createTime"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :shortcuts="rangeShortcuts"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              class="!w-220px"
+            />
+          </el-form-item>
+        </div>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery()">
+            <Icon icon="ep:search" class="mr-5px" /> 搜索
+          </el-button>
+          <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        </el-form-item>
+      </el-form>
+      <div class="grid grid-cols-7 gap-8">
+        <div
+          v-for="info in totalWorkKeys"
+          :key="info[0]"
+          class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-1 flex flex-col items-center justify-center gap-1"
+        >
+          <div class="size-7.5" :class="info[3]"></div>
+          <count-to
+            class="text-2xl font-medium"
+            :start-val="0"
+            :end-val="totalWork[info[0]]"
+            :decimals="info[4]"
+          >
+            <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
+          </count-to>
+          <div class="text-xs font-medium text-[var(--el-text-color-regular)]">{{ info[1] }}</div>
+          <div class="text-sm font-medium text-[var(--el-text-color-regular)]">{{ info[2] }}</div>
+        </div>
+      </div>
+      <div
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-4 grid grid-rows-[48px_1fr] gap-2"
+      >
+        <div class="flex items-center justify-between">
+          <el-button-group>
+            <el-button
+              size="default"
+              :type="tab === '表格' ? 'primary' : 'default'"
+              @click="handleSelectTab('表格')"
+              >表格
+            </el-button>
+            <el-button
+              size="default"
+              :type="tab === '看板' ? 'primary' : 'default'"
+              @click="handleSelectTab('看板')"
+              >看板
+            </el-button>
+          </el-button-group>
+          <h3 class="text-xl font-medium">{{ `${deptName}-${tab}` }}</h3>
+          <el-button size="default" type="primary" @click="exportAll">导出</el-button>
+        </div>
+        <!-- <el-auto-resizer>
+          <template #default="{ height, width }"> -->
+        <Motion
+          as="div"
+          :style="{ position: 'relative', overflow: 'hidden' }"
+          :animate="{ height: `${500}px`, width: `100%` }"
+          :transition="{ type: 'spring', stiffness: 200, damping: 25, duration: 0.3 }"
+        >
+          <AnimatePresence :initial="false" mode="sync">
+            <Motion
+              :key="currentTab"
+              as="div"
+              :initial="{ x: direction === 'left' ? '-100%' : '100%', opacity: 0 }"
+              :animate="{ x: '0%', opacity: 1 }"
+              :exit="{ x: direction === 'left' ? '50%' : '-50%', opacity: 0 }"
+              :transition="{ type: 'tween', stiffness: 300, damping: 30, duration: 0.3 }"
+              :style="{ position: 'absolute', left: 0, right: 0, top: 0 }"
+            >
+              <div :style="{ width: `100%`, height: `${500}px` }">
+                <el-table
+                  v-if="currentTab === '表格'"
+                  v-loading="listLoading"
+                  :data="list"
+                  :stripe="true"
+                  :max-height="500"
+                  show-overflow-tooltip
+                >
+                  <template v-for="item in columns(type)" :key="item.prop">
+                    <el-table-column
+                      v-if="item.prop !== 'name'"
+                      :label="item.label"
+                      :prop="item.prop"
+                      align="center"
+                    />
+                    <el-table-column v-else :label="item.label" :prop="item.prop" align="center">
+                      <template #default="{ row }">
+                        <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                          row.name
+                        }}</el-button>
+                      </template>
+                    </el-table-column>
+                  </template>
+                  <el-table-column label="台次" align="center">
+                    <el-table-column label="泵车" prop="cumulativePumpTrips" align="center" />
+                    <el-table-column label="仪表/混砂" prop="cumulativeMixSand" align="center" />
+                  </el-table-column>
+                </el-table>
+                <div
+                  ref="chartRef"
+                  v-loading="chartLoading"
+                  :key="dayjs().valueOf()"
+                  v-else
+                  :style="{ width: `100%`, height: `${500}px` }"
+                >
+                </div>
+              </div>
+            </Motion>
+          </AnimatePresence>
+        </Motion>
+        <!-- </template>
+        </el-auto-resizer> -->
+      </div>
+    </div>
+  </div>
+  <!-- <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" /> -->
+</template>
+
+<style scoped>
+:deep(.el-form-item) {
+  margin-bottom: 0;
+}
+
+:deep(.el-table) {
+  border-top-right-radius: 8px;
+  border-top-left-radius: 8px;
+
+  .el-table__cell {
+    height: 40px;
+  }
+
+  .el-table__header-wrapper {
+    .el-table__cell {
+      background: var(--el-fill-color-light);
+    }
+  }
+}
+</style>

+ 2 - 2
src/views/pms/iotrydailyreport/xapproval.vue

@@ -817,12 +817,12 @@ const submitForm = async (auditStatus: 20 | 30) => {
           <el-form-item label="套生段产管尺寸(mm)" prop="casingPipeSize">
             <el-input v-model="form.casingPipeSize" placeholder="" disabled />
           </el-form-item>
-          <el-form-item label="当日油耗(L)" prop="dailyFuel">
+          <el-form-item label="当日油耗()" prop="dailyFuel">
             <el-input-number
               class="w-full!"
               :min="0"
               v-model="form.dailyFuel"
-              placeholder="请输入当日油耗(L)"
+              placeholder="请输入当日油耗()"
             />
           </el-form-item>
           <el-form-item :label="t('project.currentOperation')" prop="currentOperation">

+ 2 - 2
src/views/pms/iotrydailyreport/xfill.vue

@@ -830,12 +830,12 @@ const submitForm = async () => {
           <el-form-item label="套生段产管尺寸(mm)" prop="casingPipeSize">
             <el-input v-model="form.casingPipeSize" placeholder="" disabled />
           </el-form-item>
-          <el-form-item label="当日油耗(L)" prop="dailyFuel">
+          <el-form-item label="当日油耗()" prop="dailyFuel">
             <el-input-number
               class="w-full!"
               :min="0"
               v-model="form.dailyFuel"
-              placeholder="请输入当日油耗(L)"
+              placeholder="请输入当日油耗()"
             />
           </el-form-item>
           <el-form-item :label="t('project.currentOperation')" prop="currentOperation">

+ 1 - 1
src/views/report-statistics/costs.vue

@@ -77,7 +77,7 @@ const defaultStats: StatItem[] = [
     trend: 0
   },
   {
-    key: 'msg',
+    key: 'byFee',
     title: '保养成本',
     icon: 'i-material-symbols:construction-rounded',
     class: {

+ 6 - 1
src/views/report-statistics/daily-report.vue

@@ -492,7 +492,12 @@ const expandRowKeys = computed(() => {
         v-model="query.deptId"
         title="队伍"
       />
-      <WellSelect v-show="tab === '井'" :deptId="157" v-model="query.wellName" />
+      <WellSelect
+        v-show="tab === '井'"
+        v-model:contract-name="query.contractName"
+        :deptId="157"
+        v-model:model-value="query.wellName"
+      />
     </div>
 
     <!-- 第二行右侧:自动落入第 2 行第 2 列 -->

+ 124 - 181
src/views/report-statistics/rd-daily-report.vue

@@ -4,7 +4,6 @@ import { rangeShortcuts } from '@/utils/formatTime'
 import { useDebounceFn } from '@vueuse/core'
 import dayjs from 'dayjs'
 import { DICT_TYPE, getDictOptions } from '@/utils/dict'
-import { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
 import { useUserStore } from '@/store/modules/user'
 
 defineOptions({ name: 'DailyReport' })
@@ -21,29 +20,22 @@ interface List {
   deptId: number
   projectId: number
   taskId: number
-  relocationDays: number // 搬迁安装天数
-  designInjection: string // 设计注气量(万方)
-  transitTime: number // 运行时效
-  dailyGasInjection: number // 注气量(万方)
-  dailyWaterInjection: number // 注水量(方)
-  dailyInjectGasTime: number // 注气时间(H)
-  dailyInjectWaterTime: number // 注水时间(H)
-  dailyPowerUsage: number // 日耗电量(度)
-  dailyOilUsage: number // 日耗油量(升)
-  nonProductionTime: number // 非生产时间(小时)
-  nptReason: string // 非生产时间原因
-  constructionStartDate: number // 施工开始日期
-  constructionEndDate: number // 施工结束日期
-  productionStatus: string // 生产动态
-  constructionStatus: string // 施工状态
-  totalGasInjection: number // 注气量(万方)
-  totalWaterInjection: number // 注水量(方)
-  cumulativeCompletion: number // 完工井次
-  capacity: number // 产能(万方)
-  remark: string // 备注
-  auditStatus: number // 审核状态
-  status: number // 状态
-  opinion: string // 审核意见
+  timeRange: any // 时间节点
+  rdStatus: string // 施工状态
+  cumulativeWorkingWell: number // 施工井
+  cumulativeWorkingLayers: number // 施工层
+  dailyPumpTrips: number // 泵车台次
+  dailyToolsSand: number // 仪表/混砂
+  runCount: number // 趟数
+  bridgePlug: number // 桥塞
+  waterVolume: number // 水方量
+  hourCount: number // 时间H
+  dailyFuel: number // 油耗(L)
+  productionStatus: string // 当日生产动态
+  nextPlan: string // 下步工作计划
+  externalRental: string // 外租设备
+  malfunction: string // 故障情况
+  faultDowntime: number // 故障误工H
   lastGroupIdFlag: boolean
 }
 
@@ -79,147 +71,91 @@ const columns = ref<Column[]>([
     'min-width': '120px'
   },
   {
-    label: '施工状态',
-    prop: 'constructionStatus',
-    fixed: 'left',
-    'min-width': '120px',
-    isTag: true,
-    dictType: DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE
+    label: '时间节点',
+    prop: 'timeRange',
+    'min-width': '120px'
   },
   {
-    label: '审批状态',
-    prop: 'auditStatus',
+    label: '施工状态',
+    prop: 'rdStatus',
     'min-width': '120px',
     isTag: true,
-    formatter: (row: List) => {
-      switch (row.auditStatus) {
-        case 0:
-          return '待提交'
-        case 10:
-          return '待审批'
-        case 20:
-          return '审批通过'
-        case 30:
-          return '审批拒绝'
-        default:
-          return ''
-      }
-    }
-  },
-  {
-    label: '搬迁安装天数',
-    prop: 'relocationDays',
-    'min-width': '120px',
-    formatter: (row: List) => (row.relocationDays < 0 ? '0' : String(row.relocationDays))
-  },
-  {
-    label: '设计注气量(万方)',
-    prop: 'designInjection',
-    'min-width': '120px',
-    formatter: (row: List) => row.designInjection || '0'
-  },
-  {
-    label: '运行时效',
-    prop: 'transitTime',
-    'min-width': '120px',
-    formatter: (row: List) => (row.transitTime * 100).toFixed(2) + '%'
+    dictType: DICT_TYPE.PMS_PROJECT_RD_STATUS
   },
   {
     label: '当日',
     children: [
       {
-        label: '注气量(万方)',
-        prop: 'dailyGasInjection',
-        'min-width': '120px',
-        formatter: (row: List) => (row.dailyGasInjection / 10000).toFixed(2)
-      },
-      {
-        label: '注水量(方)',
-        prop: 'dailyWaterInjection',
-        'min-width': '120px'
-      },
-      {
-        label: '注气时间(H)',
-        prop: 'dailyInjectGasTime',
+        label: '施工井',
+        prop: 'cumulativeWorkingWell',
         'min-width': '120px'
       },
       {
-        label: '注水时间(H)',
-        prop: 'dailyInjectWaterTime',
+        label: '施工层',
+        prop: 'cumulativeWorkingLayers',
         'min-width': '120px'
       },
       {
-        label: '用电量(kWh)',
-        prop: 'dailyPowerUsage',
+        label: '泵车台次',
+        prop: 'dailyPumpTrips',
         'min-width': '120px'
       },
       {
-        label: '油耗(L)',
-        prop: 'dailyOilUsage',
+        label: '仪表/混砂',
+        prop: 'dailyToolsSand',
         'min-width': '120px'
       }
     ]
   },
   {
-    label: '非生产时间(H)',
-    prop: 'nonProductionTime',
+    label: '趟数',
+    prop: 'runCount',
     'min-width': '120px'
   },
   {
-    label: '非生产时间原因',
-    prop: 'nptReason',
-    'min-width': '120px',
-    isTag: true,
-    dictType: DICT_TYPE.PMS_PROJECT_NPT_REASON
+    label: '桥塞',
+    prop: 'bridgePlug',
+    'min-width': '120px'
+  },
+  {
+    label: '水方量',
+    prop: 'waterVolume',
+    'min-width': '120px'
   },
-  // {
-  //   label: '施工开始日期',
-  //   prop: 'constructionStartDate',
-  //   'min-width': '120px',
-  //   formatter: (row: List) => dayjs(row.constructionStartDate).format('YYYY-MM-DD HH:mm:ss')
-  // },
-  // {
-  //   label: '施工结束日期',
-  //   prop: 'constructionEndDate',
-  //   'min-width': '120px',
-  //   formatter: (row: List) => dayjs(row.constructionEndDate).format('YYYY-MM-DD HH:mm:ss')
-  // },
   {
-    label: '生产动态',
+    label: '时间H',
+    prop: 'hourCount',
+    'min-width': '120px'
+  },
+  {
+    label: '油耗(L)',
+    prop: 'dailyFuel',
+    'min-width': '120px'
+  },
+  {
+    label: '当日生产动态',
     prop: 'productionStatus',
     'min-width': '120px'
   },
   {
-    label: '项目',
-    prop: 'contractName',
+    label: '下步工作计划',
+    prop: 'nextPlan',
     'min-width': '120px'
   },
   {
-    label: '累计',
-    children: [
-      {
-        label: '注气量(万方)',
-        prop: 'totalGasInjection',
-        'min-width': '120px',
-        formatter: (row: List) => (row.totalGasInjection / 10000).toFixed(2)
-      },
-      {
-        label: '注水量(方)',
-        prop: 'totalWaterInjection',
-        'min-width': '120px'
-      },
-      {
-        label: '完工井次',
-        prop: 'cumulativeCompletion',
-        'min-width': '120px'
-      }
-    ]
+    label: '外租设备',
+    prop: 'externalRental',
+    'min-width': '120px'
   },
   {
-    label: '产能(万方)',
-    prop: 'capacity',
-    'min-width': '120px',
-    formatter: (row: List) => (row.capacity / 10000).toFixed(2)
+    label: '故障情况',
+    prop: 'malfunction',
+    'min-width': '120px'
+  },
+  {
+    label: '故障误工H',
+    prop: 'faultDowntime',
+    'min-width': '120px'
   }
 ])
 
@@ -276,50 +212,50 @@ const calculateColumnWidths = (colums: Column[]) => {
   }
 }
 
-function checkTimeSumEquals24(row: List) {
-  // 获取三个字段的值,转换为数字,如果为空则视为0
-  const gasTime = row.dailyInjectGasTime || 0
-  const waterTime = row.dailyInjectWaterTime || 0
-  const nonProdTime = row.nonProductionTime || 0
-
-  // 计算总和
-  const sum = gasTime + waterTime + nonProdTime
-
-  // 返回是否等于24(允许一定的浮点数误差)
-  return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
-}
-
-function cellStyle(data: {
-  row: List
-  column: TableColumnCtx<List>
-  rowIndex: number
-  columnIndex: number
-}) {
-  const { row, column } = data
-
-  if (column.property === 'transitTime') {
-    const originalValue = row.transitTime ?? 0
-
-    if (originalValue > 1.2)
-      return {
-        color: 'red',
-        fontWeight: 'bold'
-      }
-  }
-
-  const timeFields = ['dailyInjectGasTime', 'dailyInjectWaterTime', 'nonProductionTime']
-  if (timeFields.includes(column.property)) {
-    if (!checkTimeSumEquals24(row)) {
-      return {
-        color: 'orange',
-        fontWeight: 'bold'
-      }
-    }
-  }
-
-  // 默认返回空对象,不应用特殊样式
-  return {}
-}
+// function checkTimeSumEquals24(row: List) {
+//   // 获取三个字段的值,转换为数字,如果为空则视为0
+//   const gasTime = row.dailyInjectGasTime || 0
+//   const waterTime = row.dailyInjectWaterTime || 0
+//   const nonProdTime = row.nonProductionTime || 0
+
+//   // 计算总和
+//   const sum = gasTime + waterTime + nonProdTime
+
+//   // 返回是否等于24(允许一定的浮点数误差)
+//   return Math.abs(sum - 24) < 0.01 // 使用0.01作为误差范围
+// }
+
+// function cellStyle(data: {
+//   row: List
+//   column: TableColumnCtx<List>
+//   rowIndex: number
+//   columnIndex: number
+// }) {
+//   const { row, column } = data
+
+//   if (column.property === 'transitTime') {
+//     const originalValue = row.transitTime ?? 0
+
+//     if (originalValue > 1.2)
+//       return {
+//         color: 'red',
+//         fontWeight: 'bold'
+//       }
+//   }
+
+//   const timeFields = ['dailyInjectGasTime', 'dailyInjectWaterTime', 'nonProductionTime']
+//   if (timeFields.includes(column.property)) {
+//     if (!checkTimeSumEquals24(row)) {
+//       return {
+//         color: 'orange',
+//         fontWeight: 'bold'
+//       }
+//     }
+//   }
+
+//   // 默认返回空对象,不应用特殊样式
+//   return {}
+// }
 
 function rowClassName(data: { row: List; rowIndex: number }) {
   const { row } = data
@@ -487,12 +423,17 @@ const expandRowKeys = computed(() => {
     <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg h-full">
       <DeptTreeSelect
         v-show="tab === '队伍'"
-        :top-id="157"
+        :top-id="163"
         :deptId="deptId"
         v-model="query.deptId"
         title="队伍"
       />
-      <WellSelect v-show="tab === '井'" :deptId="157" v-model="query.wellName" />
+      <WellSelect
+        v-show="tab === '井'"
+        :deptId="163"
+        v-model="query.wellName"
+        v-model:contract-name="query.contractName"
+      />
     </div>
 
     <!-- 第二行右侧:自动落入第 2 行第 2 列 -->
@@ -509,7 +450,6 @@ const expandRowKeys = computed(() => {
               :height="height"
               show-overflow-tooltip
               :width="width"
-              :cell-style="cellStyle"
               :row-class-name="rowClassName"
               :tree-props="{ hasChildren: 'lastGroupIdFlag' }"
               row-key="id"
@@ -522,12 +462,15 @@ const expandRowKeys = computed(() => {
                   <div
                     class="flex items-center gap-8 h-10 px-14 sticky left-0 w-fit box-border bg-[var(--el-bg-color)]"
                   >
-                    <el-tag>累计注气量(方): {{ row.groupIdGasInjection }} </el-tag>
-                    <el-tag>累计注水量(方): {{ row.groupIdWaterInjection }} </el-tag>
-                    <el-tag>累计用电量(kWh) : {{ row.groupIdPower }} </el-tag>
-                    <el-tag>累计油耗(L) : {{ row.groupIdFuel }} </el-tag>
-                    <el-tag>累计注气时间H : {{ row.groupIdGasTime }} </el-tag>
-                    <el-tag>累计非生产时间H : {{ row.groupIdNoProductTime }} </el-tag>
+                    <el-tag>桥塞: {{ row.groupIdBridgePlug }} </el-tag>
+                    <el-tag>趟数: {{ row.groupIdRunCount }} </el-tag>
+                    <el-tag>井数: {{ row.groupIdCumulativeWorkingWell }} </el-tag>
+                    <el-tag>小时H: {{ row.groupIdHourCount }} </el-tag>
+                    <el-tag>油耗L: {{ row.groupIdFuel }} </el-tag>
+                    <el-tag>水方量(方): {{ row.groupIdWaterVolume }} </el-tag>
+                    <el-tag>台次(泵车): {{ row.groupIdPumpTrips }} </el-tag>
+                    <el-tag>段数: {{ row.groupIdCumulativeWorkingLayers }} </el-tag>
+                    <el-tag>台次(仪表/混砂): {{ row.groupIdMixSand }} </el-tag>
                   </div>
                 </template>
               </el-table-column>

+ 6 - 1
src/views/report-statistics/ry-daily-report.vue

@@ -611,7 +611,12 @@ const expandRowKeys = computed(() => {
         v-model="query.deptId"
         title="队伍"
       />
-      <WellSelect v-show="tab === '井'" :deptId="158" v-model="query.wellName" />
+      <WellSelect
+        v-show="tab === '井'"
+        :deptId="158"
+        v-model="query.wellName"
+        v-model:contract-name="query.contractName"
+      />
     </div>
 
     <!-- 第二行右侧:自动落入第 2 行第 2 列 -->

+ 384 - 0
src/views/report-statistics/work-order-completion.vue

@@ -0,0 +1,384 @@
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import dayjs from 'dayjs'
+import { useDebounceFn } from '@vueuse/core'
+import MiniBarChart from '@/components/WorkOrderCompletionBar/index.vue'
+import CountTo from '@/components/count-to1.vue'
+import { IotReportApi } from '@/api/pms/report'
+
+// 定义时间类型
+type TimeType = 'year' | 'month' | 'day'
+
+interface Query {
+  deptId?: number
+  createTime?: [string, string]
+  pageNo: number
+  pageSize: number
+}
+
+interface ChartDataItem {
+  label: string
+  num: number
+}
+
+interface StatItem {
+  key: string
+  title: string
+  icon: string
+  total: number
+  charts: ChartDataItem[]
+  trend: number
+}
+
+// 选项配置数组
+const timeOptions: { label: string; value: TimeType }[] = [
+  { label: '年', value: 'year' },
+  { label: '月', value: 'month' },
+  { label: '日', value: 'day' }
+]
+
+const activeTimeType = ref<TimeType>('year')
+const query = ref<Query>({
+  pageNo: 1,
+  pageSize: 10
+})
+
+const defaultStats: StatItem[] = [
+  {
+    key: 'yx',
+    title: '运行记录',
+    icon: 'i-material-symbols:list-alt-outline',
+    total: 0,
+    charts: [],
+    trend: 0
+  },
+  // {
+  //   key: 'prod',
+  //   title: '生产日报',
+  //   icon: 'i-material-symbols:calendar-today-outline',
+  //   total: 0,
+  //   completed: 0,
+  //   incomplete: 0,
+  //   trend: 0
+  // },
+  {
+    key: 'wx',
+    title: '维修工单',
+    icon: 'i-material-symbols:home-repair-service-outline',
+    total: 0,
+    charts: [],
+    trend: 0
+  },
+  {
+    key: 'by',
+    title: '保养工单',
+    icon: 'i-material-symbols:construction-rounded',
+    total: 0,
+    charts: [],
+    trend: 0
+  },
+  {
+    key: 'xj',
+    title: '巡检工单',
+    icon: 'i-material-symbols:warning-outline',
+    total: 0,
+    charts: [],
+    trend: 0
+  }
+]
+
+const statList = ref<StatItem[]>(JSON.parse(JSON.stringify(defaultStats)))
+const dataLoading = ref(false)
+const list = ref<any[]>([])
+const loading = ref(false)
+const total = ref(0)
+
+const handleTimeChange = (type: TimeType, init = false) => {
+  activeTimeType.value = type
+
+  const formatStr = 'YYYY-MM-DD HH:mm:ss'
+  const endTime = dayjs().endOf('day').format(formatStr)
+  let startTime = ''
+
+  switch (type) {
+    case 'year':
+      startTime = dayjs().startOf('year').format(formatStr)
+      break
+    case 'month':
+      startTime = dayjs().startOf('month').format(formatStr)
+      break
+    case 'day':
+      startTime = dayjs().startOf('day').format(formatStr)
+      break
+  }
+
+  query.value.createTime = [startTime, endTime]
+
+  console.log(`切换为[${type}]:`, query.value.createTime)
+
+  if (!init) loadData()
+}
+
+const labelMap = {
+  wxoareject: '审批不通过',
+  wxoa: '审批中',
+  wxclose: '关闭',
+  wxfinished: '完成',
+  wxtx: '待填写',
+  xjtodo: '待执行',
+  xjignore: '忽略',
+  xjfinished: '已执行',
+  yx0: '待执行',
+  yx1: '已执行',
+  yx2: '执行中',
+  yx3: '填写中',
+  by0: '已保养',
+  by1: '未保养'
+}
+
+// 模拟数据加载
+const loadData = useDebounceFn(async function () {
+  dataLoading.value = true
+
+  const { pageNo, pageSize, ...other } = query.value
+
+  const res = await IotReportApi.getOrderNumber(other)
+
+  statList.value.forEach((item) => {
+    const data = res[item.key] || []
+
+    item.total = data.reduce((acc, cur) => acc + cur.num, 0)
+    item.charts = data.map((d) => ({
+      label: labelMap[item.key + d.status],
+      num: d.num
+    }))
+  })
+
+  dataLoading.value = false
+}, 500)
+
+const loadList = useDebounceFn(async function () {
+  loading.value = true
+
+  const res = await IotReportApi.getOrderPage(query.value)
+
+  // console.log('res :>> ', res)
+
+  // const mockTableData = Array.from({ length: query.value.pageSize }).map((_, index) => {
+  //   const types = ['维修工单', '保养工单', '巡检工单', '运行记录', '生产日报']
+  //   const companies = ['第一工程公司', '第二工程公司', '总包单位']
+  //   const statuses = ['已完成', '未完成', '处理中']
+
+  //   return {
+  //     id: index + 1,
+  //     orderType: types[Math.floor(Math.random() * types.length)],
+  //     createTime: dayjs()
+  //       .subtract(Math.floor(Math.random() * 10), 'day')
+  //       .format('YYYY-MM-DD HH:mm:ss'),
+  //     companyName: companies[Math.floor(Math.random() * companies.length)],
+  //     projectDept: `项目部-${Math.floor(Math.random() * 10) + 1}`,
+  //     teamName: `作业队-${String.fromCharCode(65 + Math.floor(Math.random() * 5))}`,
+  //     status: statuses[Math.floor(Math.random() * statuses.length)],
+  //     deviceName: `设备-${Math.floor(Math.random() * 1000)}`
+  //   }
+  // })
+
+  list.value = res.list
+  total.value = res.total
+  loading.value = false
+}, 500)
+
+function handleSizeChange(val: number) {
+  query.value.pageSize = val
+  query.value.pageNo = 1
+  loadList()
+}
+
+function handleCurrentChange(val: number) {
+  query.value.pageNo = val
+  loadList()
+}
+
+function handleQuery(setPage = true) {
+  if (setPage) {
+    query.value.pageNo = 1
+  }
+  loadList()
+  loadData()
+}
+
+onMounted(() => {
+  handleTimeChange('year', true)
+})
+
+watch(
+  [() => query.value.createTime, () => query.value.deptId],
+  () => {
+    handleQuery()
+  },
+  { immediate: true }
+)
+</script>
+
+<template>
+  <div
+    class="grid grid-cols-[15%_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+  >
+    <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
+      <DeptTreeSelect
+        :top-id="156"
+        :deptId="156"
+        v-model="query.deptId"
+        :init-select="false"
+        :show-title="false"
+      />
+    </div>
+    <div class="flex flex-col gap-4 h-full overflow-hidden">
+      <div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-4" v-loading="dataLoading">
+        <section
+          v-for="item in statList"
+          :key="item.key"
+          class="bg-white dark:bg-[#1d1e1f] rounded-xl shadow flex justify-between items-stretch min-h-[140px] border border-gray-200 dark:border-gray-700/50 relative overflow-hidden group transition-colors duration-300"
+        >
+          <!-- 左侧:文字信息 -->
+          <div class="flex flex-col justify-between z-10 p-4 pr-0">
+            <div>
+              <!-- 标题:白底深灰,黑底浅灰 -->
+              <div class="text-gray-500 dark:text-gray-400 text-sm font-medium mb-1">
+                {{ item.title }}
+              </div>
+              <!-- 数值:白底黑色,黑底白色 -->
+              <div class="text-3xl font-bold tracking-tight text-gray-900! dark:text-white! mt-1">
+                <count-to :start-val="0" :end-val="item.total" :duration="100" />
+              </div>
+            </div>
+
+            <!-- 环比 -->
+            <div class="flex items-center gap-2 mt-2">
+              <!-- 标签:针对亮色/暗色分别设置背景和文字颜色 -->
+              <div
+                class="px-2 py-0.5 rounded text-xs font-bold flex items-center space-x-1"
+                :class="
+                  item.trend >= 0
+                    ? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400'
+                    : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
+                "
+              >
+                <div
+                  :class="
+                    item.trend >= 0
+                      ? 'i-material-symbols:trending-up'
+                      : 'i-material-symbols:trending-down'
+                  "
+                  class="text-sm"
+                ></div>
+                <span>{{ Math.abs(item.trend) }}%</span>
+              </div>
+              <span class="text-xs text-gray-500 dark:text-gray-400">环比</span>
+            </div>
+          </div>
+
+          <!-- 右侧:ECharts图表 -->
+          <div class="flex-1 h-full z-10 relative">
+            <MiniBarChart :items="item.charts" :max="item.total" />
+          </div>
+
+          <!-- 背景装饰:颜色自适应 -->
+          <div
+            class="absolute -right-6 -bottom-6 opacity-[0.05] pointer-events-none transition-transform group-hover:scale-150 duration-500 text-black dark:text-white"
+          >
+            <div :class="item.icon" class="text-9xl"></div>
+          </div>
+        </section>
+      </div>
+      <div class="flex justify-between">
+        <el-button-group size="default">
+          <el-button
+            v-for="item in timeOptions"
+            :key="item.value"
+            :type="activeTimeType === item.value ? 'primary' : ''"
+            @click="handleTimeChange(item.value)"
+          >
+            {{ item.label }}
+          </el-button>
+        </el-button-group>
+        <el-button size="default" type="primary">导出</el-button>
+      </div>
+    </div>
+    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
+      <div class="flex-1 relative">
+        <el-auto-resizer class="absolute">
+          <template #default="{ width, height }">
+            <el-table
+              :data="list"
+              v-loading="loading"
+              stripe
+              class="absolute"
+              :max-height="height"
+              :height="height"
+              show-overflow-tooltip
+              :width="width"
+              scrollbar-always-on
+            >
+              <el-table-column label="序号" type="index" width="60" align="center" />
+              <el-table-column label="工单类别" prop="type" align="center" width="80" />
+              <el-table-column label="生成日期" prop="createTime" align="center" width="160" />
+              <el-table-column label="公司" prop="company" align="center" width="100" />
+              <el-table-column label="项目部" prop="project" align="center" />
+              <el-table-column label="队伍" prop="deptName" align="center" />
+              <el-table-column label="状态" prop="status" align="center" width="80">
+                <!-- <template #default="{ row }">
+                  <el-tag v-if="row.status === '已完成'" type="success" effect="dark" size="small"
+                    >已完成</el-tag
+                  >
+                  <el-tag
+                    v-else-if="row.status === '未完成'"
+                    type="danger"
+                    effect="dark"
+                    size="small"
+                    >未完成</el-tag
+                  >
+                  <el-tag v-else type="warning" effect="plain" size="small">{{
+                    row.status
+                  }}</el-tag>
+                </template> -->
+              </el-table-column>
+              <el-table-column label="设备" prop="device" align="center" />
+            </el-table>
+          </template>
+        </el-auto-resizer>
+      </div>
+      <div class="h-10 mt-4 flex items-center justify-end">
+        <el-pagination
+          size="default"
+          v-show="total > 0"
+          v-model:current-page="query.pageNo"
+          v-model:page-size="query.pageSize"
+          :background="true"
+          :page-sizes="[10, 20, 30, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.el-table) {
+  border-top-right-radius: 8px;
+  border-top-left-radius: 8px;
+
+  .el-table__cell {
+    height: 52px;
+  }
+
+  .el-table__header-wrapper {
+    .el-table__cell {
+      background: var(--el-fill-color-light);
+    }
+  }
+}
+</style>