Ver código fonte

✨ feat(报表统计): 工单完成情况

Zimo 11 horas atrás
pai
commit
1bd2b8a574

+ 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>

+ 88 - 53
src/components/WorkOrderCompletionBar/index.vue

@@ -11,29 +11,60 @@ import {
 } 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<{
-  completed: number
-  incomplete: number
-  max: number
+  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
-
-  // 使用 markRaw 避免 Vue 深度监听 echarts 实例导致性能问题
   chartInstance = markRaw(echarts.init(chartRef.value))
   updateChart()
-
   window.addEventListener('resize', handleResize)
 }
 
@@ -44,74 +75,77 @@ const handleResize = () => {
 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',
-      formatter: '{b}: {c}', // 鼠标悬停显示: 已完成: 100
-      confine: true, // 限制提示框在图表区域内
+      confine: true,
       backgroundColor: 'rgba(50,50,50,0.9)',
       borderColor: '#333',
       textStyle: { color: '#fff', fontSize: 12 }
     },
     grid: {
-      top: '12%',
-      bottom: '20%', // 留出底部文字空间
-      left: '2%',
-      right: '2%',
-      containLabel: false
+      top: '15%', // 留出上方数值显示空间
+      bottom: '5%', // 调整底部文字空间
+      left: '-20%',
+      right: '0%',
+      containLabel: true // 改为 true 自动计算 Label 空间,防止被切
     },
     xAxis: {
       type: 'category',
-      data: ['已完成', '未完成'],
+      data: xAxisData,
       axisLine: { show: false },
       axisTick: { show: false },
       axisLabel: {
-        color: '#9ca3af', // 对应 Tailwind gray-400
-        fontSize: 10,
-        margin: 5,
-        interval: 0
+        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: 'value',
-      show: false, // 隐藏 Y 轴
-      max: props.max
+      type: 'log',
+      show: false
+      // max: yAxisMax as number
     },
     series: [
       {
         type: 'bar',
-        barWidth: '50%', // 柱子宽度
+        barWidth: '35%', // 稍微变窄一点,适应5个柱子的情况
         label: {
           show: true,
           position: 'top',
-          fontSize: 16,
-          fontWeight: 'bold'
+          fontSize: 12,
+          fontWeight: 'bold',
+          color: '#374151' // 数值颜色
         },
-        data: [
-          {
-            value: props.completed,
-            name: '已完成',
-            itemStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgb(179,224.5,156.5)' }, // Emerald 400
-                { offset: 1, color: 'rgb(148.6,212.3,117.1)' } // Emerald 600
-              ]),
-              borderRadius: [4, 4, 4, 4] // 顶部圆角
-            }
-          },
-          {
-            value: props.incomplete,
-            name: '未完成',
-            itemStyle: {
-              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-                { offset: 0, color: 'rgb(250,181.5,181.5)' }, // Rose 400
-                { offset: 1, color: 'rgb(248,152.1,152.1)' } // Rose 600
-              ]),
-              borderRadius: [4, 4, 4, 4]
-            }
-          }
-        ],
-        // 动画配置
+        data: seriesData,
         animationDuration: 1000,
         animationEasing: 'cubicOut'
       }
@@ -121,15 +155,16 @@ const updateChart = () => {
   chartInstance.setOption(option)
 }
 
+// 监听 items 数组的变化
 watch(
-  () => [props.completed, props.incomplete],
+  () => props.items,
   () => {
     updateChart()
-  }
+  },
+  { deep: true } // 深度监听数组内部对象变化
 )
 
 onMounted(() => {
-  // 稍微延迟一下确保 DOM 渲染完毕,避免容器宽/高为0
   setTimeout(initChart, 50)
 })
 

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

@@ -297,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>

+ 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 列 -->

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

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

+ 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 列 -->

+ 92 - 73
src/views/report-statistics/work-order-completion.vue

@@ -4,6 +4,7 @@ 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'
@@ -15,13 +16,17 @@ interface Query {
   pageSize: number
 }
 
+interface ChartDataItem {
+  label: string
+  num: number
+}
+
 interface StatItem {
   key: string
   title: string
   icon: string
   total: number
-  completed: number
-  incomplete: number
+  charts: ChartDataItem[]
   trend: number
 }
 
@@ -40,48 +45,44 @@ const query = ref<Query>({
 
 const defaultStats: StatItem[] = [
   {
-    key: 'run',
+    key: 'yx',
     title: '运行记录',
     icon: 'i-material-symbols:list-alt-outline',
     total: 0,
-    completed: 0,
-    incomplete: 0,
+    charts: [],
     trend: 0
   },
+  // {
+  //   key: 'prod',
+  //   title: '生产日报',
+  //   icon: 'i-material-symbols:calendar-today-outline',
+  //   total: 0,
+  //   completed: 0,
+  //   incomplete: 0,
+  //   trend: 0
+  // },
   {
-    key: 'prod',
-    title: '生产日报',
-    icon: 'i-material-symbols:calendar-today-outline',
-    total: 0,
-    completed: 0,
-    incomplete: 0,
-    trend: 0
-  },
-  {
-    key: 'repair',
+    key: 'wx',
     title: '维修工单',
     icon: 'i-material-symbols:home-repair-service-outline',
     total: 0,
-    completed: 0,
-    incomplete: 0,
+    charts: [],
     trend: 0
   },
   {
-    key: 'maintain',
+    key: 'by',
     title: '保养工单',
     icon: 'i-material-symbols:construction-rounded',
     total: 0,
-    completed: 0,
-    incomplete: 0,
+    charts: [],
     trend: 0
   },
   {
-    key: 'inspect',
+    key: 'xj',
     title: '巡检工单',
     icon: 'i-material-symbols:warning-outline',
     total: 0,
-    completed: 0,
-    incomplete: 0,
+    charts: [],
     trend: 0
   }
 ]
@@ -118,50 +119,72 @@ const handleTimeChange = (type: TimeType, init = false) => {
   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
-  await new Promise((resolve) => setTimeout(resolve, 600)) // 模拟延迟
+
+  const { pageNo, pageSize, ...other } = query.value
+
+  const res = await IotReportApi.getOrderNumber(other)
 
   statList.value.forEach((item) => {
-    const randomTotal = Math.floor(Math.random() * 500) + 50
-    const randomCompleted = Math.floor(randomTotal * (0.6 + Math.random() * 0.3))
-    const randomIncomplete = randomTotal - randomCompleted
-    const randomTrend = Number((Math.random() * 20 - 10).toFixed(1))
-
-    item.total = randomTotal
-    item.completed = randomCompleted
-    item.incomplete = randomIncomplete
-    item.trend = randomTrend
+    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
-  await new Promise((resolve) => setTimeout(resolve, 500))
-
-  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 = mockTableData
-  total.value = 85
+  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)
 
@@ -215,10 +238,10 @@ watch(
         <section
           v-for="item in statList"
           :key="item.key"
-          class="bg-white dark:bg-[#1d1e1f] rounded-xl shadow p-4 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"
+          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 w-[55%]">
+          <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">
@@ -226,7 +249,7 @@ watch(
               </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="2000" />
+                <count-to :start-val="0" :end-val="item.total" :duration="100" />
               </div>
             </div>
 
@@ -256,12 +279,8 @@ watch(
           </div>
 
           <!-- 右侧:ECharts图表 -->
-          <div class="w-[45%] h-full z-10 relative">
-            <MiniBarChart
-              :completed="item.completed"
-              :incomplete="item.incomplete"
-              :max="item.total"
-            />
+          <div class="flex-1 h-full z-10 relative">
+            <MiniBarChart :items="item.charts" :max="item.total" />
           </div>
 
           <!-- 背景装饰:颜色自适应 -->
@@ -302,13 +321,13 @@ watch(
               scrollbar-always-on
             >
               <el-table-column label="序号" type="index" width="60" align="center" />
-              <el-table-column label="工单类别" prop="orderType" align="center" min-width="110" />
-              <el-table-column label="生成日期" prop="createTime" align="center" min-width="160" />
-              <el-table-column label="公司" prop="companyName" align="center" min-width="140" />
-              <el-table-column label="项目部" prop="projectDept" align="center" min-width="120" />
-              <el-table-column label="队伍" prop="teamName" align="center" min-width="100" />
-              <el-table-column label="状态" prop="status" align="center" width="100">
-                <template #default="{ row }">
+              <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
                   >
@@ -322,9 +341,9 @@ watch(
                   <el-tag v-else type="warning" effect="plain" size="small">{{
                     row.status
                   }}</el-tag>
-                </template>
+                </template> -->
               </el-table-column>
-              <el-table-column label="设备" prop="deviceName" align="center" min-width="120" />
+              <el-table-column label="设备" prop="device" align="center" />
             </el-table>
           </template>
         </el-auto-resizer>