Sfoglia il codice sorgente

feat(dailyreport): 日报管理 瑞恒瑞鹰日报汇总添加非生产时效tab,瑞恒瑞鹰瑞都统一添加非生产时效汇总行,并且可以跳转日报详情

Zimo 1 giorno fa
parent
commit
9e8a73d21f

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

@@ -47,6 +47,9 @@ export interface IotRdDailyReportVO {
 
 // 瑞都日报 API
 export const IotRdDailyReportApi = {
+  nptStatistics: async (params: any) => {
+    return await request.get({ url: `/pms/iot-rd-daily-report/nptStatistics`, params })
+  },
   // 查询瑞都日报分页
   getIotRdDailyReportPage: async (params: any) => {
     return await request.get({ url: `/pms/iot-rd-daily-report/page`, params })

+ 1 - 1
src/api/pms/iotrhdailyreport/index.ts

@@ -70,7 +70,7 @@ export const IotRhDailyReportApi = {
   },
 
   nptStatistics: async (params: any) => {
-    return await request.get({ url: `/pms/iot-rd-daily-report/nptStatistics`, params })
+    return await request.get({ url: `/pms/iot-rh-daily-report/nptStatistics`, params })
   },
 
   // 累计工作量统计

+ 4 - 0
src/api/pms/iotrydailyreport/index.ts

@@ -86,6 +86,10 @@ export const IotRyDailyReportApi = {
     return await request.get({ url: `/pms/iot-ry-daily-report/statistics`, params })
   },
 
+  nptStatistics: async (params: any) => {
+    return await request.get({ url: `/pms/iot-ry-daily-report/nptStatistics`, params })
+  },
+
   // 累计工作量统计
   totalWorkload: async (params: any) => {
     return await request.get({ url: `/pms/iot-ry-daily-report/totalWorkload`, params })

+ 50 - 0
src/components/ZmTable/index.vue

@@ -419,6 +419,10 @@ defineExpose({
   --zm-table-stripe-bg: #fcfdff;
   --zm-table-hover-bg: #f5f9ff;
   --zm-table-current-bg: #eef6ff;
+  --zm-table-summary-bg: #f7f9fc;
+  --zm-table-summary-text-color: var(--zm-table-strong-text-color);
+  --zm-table-summary-font-weight: 600;
+  --zm-table-summary-border-color: var(--zm-table-border-color);
   --zm-table-cell-height: 38px;
   --zm-table-cell-padding-x: 8px;
   --zm-table-cell-first-padding-left: 0px;
@@ -589,6 +593,52 @@ defineExpose({
     color: var(--zm-table-empty-text-color);
   }
 
+  .el-table__footer-wrapper {
+    background: var(--zm-table-summary-bg);
+    border-top: 1px solid var(--zm-table-summary-border-color);
+  }
+
+  .el-table__footer {
+    color: var(--zm-table-summary-text-color);
+
+    .el-table__cell {
+      height: var(--zm-table-cell-height);
+      font-weight: var(--zm-table-summary-font-weight);
+      color: var(--zm-table-summary-text-color);
+      background: var(--zm-table-summary-bg) !important;
+      border-right: 1px solid var(--zm-table-row-border-color) !important;
+      border-bottom: none !important;
+
+      .cell {
+        display: flex;
+        min-height: 100%;
+        align-items: center;
+        justify-content: center;
+      }
+
+      &:last-child {
+        border-right: none !important;
+      }
+    }
+
+    tr:last-child {
+      .el-table__cell {
+        &:first-child {
+          border-bottom-left-radius: var(--zm-table-radius);
+        }
+
+        &:last-child {
+          border-bottom-right-radius: var(--zm-table-radius);
+        }
+      }
+    }
+
+    .el-table__cell.el-table-fixed-column--left.is-last-column,
+    .el-table__cell.el-table-fixed-column--right.is-first-column {
+      box-shadow: none;
+    }
+  }
+
   .el-table__cell.el-table-fixed-column--left,
   .el-table__cell.el-table-fixed-column--right {
     background: inherit;

+ 0 - 1
src/layout/components/TagsView/src/helper.ts

@@ -5,7 +5,6 @@ export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') =>
   let tags: RouteLocationNormalizedLoaded[] = []
   routes.forEach((route) => {
     const meta = route.meta as RouteMeta
-    console.log('meta', meta)
     const tagPath = pathResolve(parentPath, route.path)
     const currentSource = sessionStorage.getItem('LOGIN_SOURCE')
     if (currentSource === 'zhly' && route.meta.title === '设备地图') {

+ 30 - 18
src/views/pms/iotrddailyreport/components/DailyStatistics.vue

@@ -133,6 +133,7 @@ const getTotal = useDebounceFn(async () => {
 interface List {
   id: number | null
   name: string | null
+  projectDeptId?: number
   type: '1' | '2' | '3'
   cumulativeBridgePlug: number | null
   cumulativeRunCount: number | null
@@ -157,7 +158,7 @@ const getList = useDebounceFn(async () => {
     const reslist = res?.list || []
 
     list.value = reslist.map(
-      ({ projectDeptId, projectDeptName, teamId, teamName, type, ...other }) => {
+      ({ projectDeptId, projectDeptName, teamId, teamName, type, id, ...other }) => {
         return {
           id: type === '2' ? projectDeptId : teamId,
           name: type === '2' ? projectDeptName : teamName,
@@ -175,6 +176,8 @@ const getList = useDebounceFn(async () => {
         }
       }
     )
+
+    console.log('list', list.value)
   } finally {
     listLoading.value = false
   }
@@ -421,17 +424,25 @@ const exportAll = async () => {
 const router = useRouter()
 
 const tolist = (id: number | null) => {
+  console.log('id', id)
   if (!id) return
 
   router.push({
-    path: '/iotdayilyreport/IotRdDailyReport',
+    name: 'IotRdDailyReport',
     query: {
-      ...getQueryWithoutPage(),
-      deptId: id
+      deptId: id,
+      createTime: props.query.createTime
     }
   })
 }
 
+const handleRowClick = (row: List) => {
+  console.log('row', row)
+  tolist(row.id)
+}
+
+const getClickableRowClassName = () => 'summary-clickable-row'
+
 const { ZmTable, ZmTableColumn } = useTableComponents()
 </script>
 
@@ -441,15 +452,13 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
       <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"
-      >
+        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]"
-        >
+          :decimals="info[4]">
           <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
         </count-to>
         <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
@@ -484,8 +493,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
               as="div"
               :style="{ position: 'relative', overflow: 'hidden' }"
               :animate="{ height: `${height}px`, width: `100%` }"
-              :transition="{ type: 'spring', stiffness: 200, damping: 25, duration: 0.3 }"
-            >
+              :transition="{ type: 'spring', stiffness: 200, damping: 25, duration: 0.3 }">
               <AnimatePresence :initial="false" mode="sync">
                 <Motion
                   :key="currentTab"
@@ -494,8 +502,7 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
                   :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 }"
-                >
+                  :style="{ position: 'absolute', left: 0, right: 0, top: 0 }">
                   <div :style="{ width: `100%`, height: `${height}px` }">
                     <zm-table
                       v-if="currentTab === '表格'"
@@ -503,10 +510,11 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
                       :data="list"
                       :height="height"
                       show-border
-                    >
+                      :row-class-name="getClickableRowClassName"
+                      @row-click="handleRowClick">
                       <zm-table-column label="部门" prop="name">
                         <template #default="{ row }">
-                          <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                          <el-button text type="primary" @click.stop="tolist(row.id)">{{
                             row.name
                           }}</el-button>
                         </template>
@@ -529,16 +537,14 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
                         action
                         :real-value="
                           (row: List) => (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
-                        "
-                      />
+                        " />
                     </zm-table>
                     <div
                       ref="chartRef"
                       v-loading="chartLoading"
                       :key="dayjs().valueOf()"
                       v-else
-                      :style="{ width: `100%`, height: `${height}px` }"
-                    >
+                      :style="{ width: `100%`, height: `${height}px` }">
                     </div>
                   </div>
                 </Motion>
@@ -550,3 +556,9 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
     </div>
   </div>
 </template>
+
+<style scoped>
+:deep(.summary-clickable-row) {
+  cursor: pointer;
+}
+</style>

+ 84 - 23
src/views/pms/iotrddailyreport/components/NonProductionEfficiency.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
 import { useTableComponents } from '@/components/ZmTable/useTableComponents'
 import { useDebounceFn } from '@vueuse/core'
 import * as echarts from 'echarts'
+import { IotRdDailyReportApi } from '../../../../api/pms/iotrddailyreport'
 
 interface Query {
   pageNo: number
@@ -20,7 +20,11 @@ const props = defineProps<{
 }>()
 
 interface ListItem {
-  id?: number
+  id?: number | null
+  name?: string | null
+  type?: string
+  teamId?: number
+  projectDeptId?: number
   teamName?: string
   projectDeptName?: string
   accidentTime: number
@@ -45,6 +49,7 @@ const loading = ref(false)
 const tab = ref<'表格' | '看板'>('表格')
 const chartRef = ref<HTMLDivElement | null>(null)
 let chart: echarts.ECharts | null = null
+const router = useRouter()
 
 const nonProductionTimeFields: [keyof ListItem, string][] = [
   ['accidentTime', '工程质量'],
@@ -71,17 +76,24 @@ const getQueryWithoutPage = () => {
 }
 
 const normalizeListData = (data: any) => {
-  if (Array.isArray(data)) {
-    return data
-  }
-
-  return data?.list || data?.records || []
+  const rows = Array.isArray(data) ? data : data?.list || data?.records || []
+
+  return rows.map(({ projectDeptId, projectDeptName, teamId, teamName, type, ...other }) => ({
+    ...other,
+    type,
+    projectDeptId,
+    projectDeptName,
+    teamId,
+    teamName,
+    id: type === '2' ? projectDeptId : teamId,
+    name: type === '2' ? projectDeptName : teamName
+  }))
 }
 
 const loadList = useDebounceFn(async function () {
   loading.value = true
   try {
-    const data = await IotRhDailyReportApi.nptStatistics(getQueryWithoutPage())
+    const data = await IotRdDailyReportApi.nptStatistics(getQueryWithoutPage())
 
     list.value = normalizeListData(data)
     if (tab.value === '看板') {
@@ -127,14 +139,62 @@ const formatNumber = (value: unknown) => {
   return Number.isInteger(num) ? `${num}` : num.toFixed(2)
 }
 
+const getFieldTotal = (field: keyof ListItem) => {
+  return list.value.reduce((total, row) => total + Number(row[field] || 0), 0)
+}
+
+const getSummaryRate = () => {
+  const nptTotal = getFieldTotal('nptTotal')
+  const calendarTime = getFieldTotal('calendarTime')
+
+  if (!calendarTime) return '0.00%'
+
+  return `${((nptTotal / calendarTime) * 100).toFixed(2)}%`
+}
+
+const getSummaries = ({ columns }: { columns: Array<{ property?: keyof ListItem | string }> }) => {
+  const nonSummaryFields = ['id', 'name', 'teamName', 'projectDeptName']
+
+  return columns.map((column, index) => {
+    if (index === 0) return '合计'
+
+    const property = column.property as keyof ListItem | undefined
+    if (!property) return ''
+
+    if (property === 'nptRate') return getSummaryRate()
+    if (nonSummaryFields.includes(property)) return ''
+
+    return formatNumber(getFieldTotal(property))
+  })
+}
+
 const formatTeamName = (row: ListItem) => {
-  return row.teamName || row.projectDeptName || '-'
+  return row.name || '-'
 }
 
 const formatRate = (row: ListItem) => {
   return `${(Number(row.nptRate || 0) * 100).toFixed(2)}%`
 }
 
+const getRowDeptId = (row: ListItem) => {
+  return row.id
+}
+
+const handleRowClick = (row: ListItem) => {
+  const deptId = getRowDeptId(row)
+  if (!deptId) return
+
+  router.push({
+    name: 'IotRdDailyReport',
+    query: {
+      deptId,
+      createTime: props.query.createTime
+    }
+  })
+}
+
+const getClickableRowClassName = () => 'summary-clickable-row'
+
 const getPieData = () => {
   return list.value
     .map((row) => ({
@@ -245,15 +305,17 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
             :max-height="height"
             :height="height"
             show-border
-          >
+            show-summary
+            :summary-method="getSummaries"
+            :row-class-name="getClickableRowClassName"
+            @row-click="handleRowClick">
             <zm-table-column
-              prop="teamName"
+              prop="name"
               label="队伍"
               min-width="120"
               fixed="left"
               cover-formatter
-              :real-value="formatTeamName"
-            />
+              :real-value="formatTeamName" />
             <zm-table-column
               v-for="[prop, label] in nonProductionTimeFields"
               :key="prop"
@@ -261,23 +323,20 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
               :label="label"
               min-width="92"
               cover-formatter
-              :real-value="(row: ListItem) => formatNumber(row[prop])"
-            />
+              :real-value="(row: ListItem) => formatNumber(row[prop])" />
             <zm-table-column label="npt合计" is-parent>
               <zm-table-column
                 prop="nptTotal"
                 label="时长(H)"
                 min-width="92"
                 cover-formatter
-                :real-value="(row: ListItem) => formatNumber(row.nptTotal)"
-              />
+                :real-value="(row: ListItem) => formatNumber(row.nptTotal)" />
               <zm-table-column
                 prop="nptRate"
                 label="占比"
                 min-width="92"
                 cover-formatter
-                :real-value="formatRate"
-              />
+                :real-value="formatRate" />
             </zm-table-column>
             <zm-table-column
               prop="calendarTime"
@@ -285,16 +344,14 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
               min-width="92"
               cover-formatter
               action
-              :real-value="(row: ListItem) => formatNumber(row.calendarTime)"
-            />
+              :real-value="(row: ListItem) => formatNumber(row.calendarTime)" />
           </zm-table>
           <div
             v-else
             ref="chartRef"
             v-loading="loading"
             class="npt-board-container"
-            :style="{ width: `${width}px`, height: `${height}px` }"
-          >
+            :style="{ width: `${width}px`, height: `${height}px` }">
           </div>
         </template>
       </el-auto-resizer>
@@ -307,6 +364,10 @@ const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
   background-color: #fff566 !important;
 }
 
+:deep(.summary-clickable-row) {
+  cursor: pointer;
+}
+
 .npt-board-container {
   background: var(--el-bg-color);
   border: 1px solid var(--el-border-color-lighter);

+ 23 - 3
src/views/pms/iotrddailyreport/index.vue

@@ -33,13 +33,25 @@ interface Query {
   pageSize: number
 }
 
+const getDefaultCreateTime = () => {
+  return [...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
+}
+
+const getRouteCreateTime = () => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    return createTime.filter((item): item is string => typeof item === 'string')
+  }
+
+  return typeof createTime === 'string' ? [createTime] : getDefaultCreateTime()
+}
+
 const initQuery: Query = {
   pageNo: 1,
   pageSize: 10,
   deptId: route.query.deptId ? Number(route.query.deptId) : id,
-  createTime: route.query.createTime
-    ? (route.query.createTime as string[])
-    : [...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))],
+  createTime: getRouteCreateTime(),
   wellName: route.query.wellName ? (route.query.wellName as string) : undefined,
   taskId: route.query.taskId ? Number(route.query.taskId) : undefined
 }
@@ -161,6 +173,14 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => [route.query.deptId, route.query.createTime],
+  () => {
+    query.value.deptId = route.query.deptId ? Number(route.query.deptId) : id
+    query.value.createTime = getRouteCreateTime()
+  }
+)
+
 // const formRef = ref()
 // const openForm = (type: string, id?: number) => {
 //   formRef.value.open(type, id)

+ 876 - 0
src/views/pms/iotrhdailyreport/components/DailyStatistics.vue

@@ -0,0 +1,876 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import type { ECharts } from 'echarts/core'
+import { DataZoomComponent } from 'echarts/components'
+import {
+  IotRhDailyReportApi,
+  type IotRhDailyReportTotalWorkloadVO
+} from '@/api/pms/iotrhdailyreport'
+import { useDebounceFn } from '@vueuse/core'
+import CountTo from '@/components/count-to1.vue'
+import echarts from '@/plugins/echarts'
+import UnfilledReportDialog from '../UnfilledReportDialog.vue'
+
+import { Motion, AnimatePresence } from 'motion-v'
+
+import download from '@/utils/download'
+
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+
+echarts.use([DataZoomComponent])
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+}
+
+const props = defineProps<{
+  query: Query
+  deptName: string
+  refreshKey: number
+}>()
+
+const totalWorkKeys: [string, string, string, string, number][] = [
+  ['totalGasInjection', '万方', '累计注气量', 'i-material-symbols:cloud-outline text-sky', 2],
+  [
+    'totalWaterInjection',
+    '万方',
+    '累计注水量',
+    'i-material-symbols:water-drop-outline-rounded text-sky',
+    2
+  ],
+  [
+    'utilizationRate',
+    '%',
+    '设备利用率',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  [
+    'totalPowerConsumption',
+    'MWh',
+    '累计用电量',
+    'i-material-symbols:electric-bolt-outline-rounded text-sky',
+    2
+  ],
+  [
+    'totalFuelConsumption',
+    '升',
+    '累计油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
+  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
+  [
+    'alreadyReported',
+    '个',
+    '已填报',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
+]
+
+const totalWork = ref({
+  totalCount: 0,
+  alreadyReported: 0,
+  notReported: 0,
+  totalFuelConsumption: 0,
+  totalPowerConsumption: 0,
+  totalWaterInjection: 0,
+  totalGasInjection: 0,
+  utilizationRate: 0
+})
+
+const totalLoading = ref(false)
+
+const totalWorkloadDetail = ref({
+  totalN2GasInjection: 0,
+  totalNaturalGasInjection: 0
+})
+
+const formatGasInjectionTooltipValue = (value?: number | null) => ((value || 0) / 10000).toFixed(4)
+
+const getTotal = useDebounceFn(async () => {
+  totalLoading.value = true
+
+  const { pageNo, pageSize, ...other } = props.query
+
+  try {
+    let res1: any[]
+    if (props.query.createTime && props.query.createTime.length === 2) {
+      res1 = await IotRhDailyReportApi.rhDailyReportStatistics({
+        createTime: props.query.createTime,
+        deptId: props.query.deptId
+      })
+
+      totalWork.value.totalCount = res1[0].count
+      totalWork.value.alreadyReported = res1[1].count
+      totalWork.value.notReported = res1[2].count
+    }
+
+    const res2: IotRhDailyReportTotalWorkloadVO = await IotRhDailyReportApi.totalWorkload(other)
+
+    totalWorkloadDetail.value = {
+      totalN2GasInjection: res2.totalN2GasInjection || 0,
+      totalNaturalGasInjection: res2.totalNaturalGasInjection || 0
+    }
+
+    totalWork.value = {
+      ...totalWork.value,
+      ...res2,
+      totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
+      totalWaterInjection: (res2.totalWaterInjection || 0) / 10000,
+      totalGasInjection: (res2.totalGasInjection || 0) / 10000,
+      totalFuelConsumption: res2.totalFuelConsumption || 0,
+      utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
+    }
+  } finally {
+    totalLoading.value = false
+  }
+}, 500)
+
+interface List {
+  id: number | null
+  name: string | null
+  type: '1' | '2' | '3'
+  cumulativeGasInjection: number | null
+  cumulativeWaterInjection: number | null
+  cumulativePowerConsumption: number | null
+  cumulativeFuelConsumption: number | null
+  transitTime: number | null
+  nonProductiveTime: number | null
+  utilizationRate: number | null
+}
+
+const list = ref<List[]>([])
+
+const type = ref('2')
+
+function checkIsSameDay(createTime: string[]): boolean {
+  if (!createTime || createTime.length < 2) {
+    return false
+  }
+
+  const [startTime, endTime] = createTime
+
+  return dayjs(startTime).isSame(endTime, 'day')
+}
+
+const columns = (type: string) => {
+  return [
+    {
+      label: type === '2' ? '项目部' : '队伍',
+      prop: 'name'
+    },
+    {
+      label: '累计注气量(万方)',
+      prop: 'cumulativeGasInjection'
+    },
+    {
+      label: '累计注水量(万方)',
+      prop: 'cumulativeWaterInjection'
+    },
+    {
+      label: '累计用电量(MWh)',
+      prop: 'cumulativePowerConsumption'
+    },
+    {
+      label: '累计油耗(升)',
+      prop: 'cumulativeFuelConsumption'
+    },
+    {
+      label: '平均时效(%)',
+      prop: 'transitTime'
+    },
+    {
+      label: '非生产时效(%)',
+      prop: 'nonProductiveTime'
+    },
+    ...(type === '2' && checkIsSameDay(props.query.createTime)
+      ? [
+          {
+            label: '队伍总数',
+            prop: 'teamCount'
+          },
+          {
+            label: '驻地待命',
+            prop: 'zddmTeamCount'
+          },
+          {
+            label: '施工准备',
+            prop: 'zbTeamCount'
+          },
+          {
+            label: '施工队伍',
+            prop: 'sgTeamCount'
+          }
+        ]
+      : []),
+    {
+      label: '设备利用率(%)',
+      prop: 'utilizationRate',
+      action: true
+    }
+  ]
+}
+
+const listLoading = ref(false)
+
+const formatter = (row: List, column: any) => {
+  if (column.property === 'transitTime') {
+    return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
+  } else if (column.property === 'nonProductiveTime') {
+    return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
+  } else if (column.property === 'utilizationRate') {
+    return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
+  } else return row[column.property] ?? 0
+}
+
+const getList = useDebounceFn(async () => {
+  listLoading.value = true
+  try {
+    const res = await IotRhDailyReportApi.getIotRhDailyReportSummary(props.query)
+
+    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,
+          cumulativeGasInjection: ((other.cumulativeGasInjection || 0) / 10000).toFixed(4),
+          cumulativeWaterInjection: ((other.cumulativeWaterInjection || 0) / 10000).toFixed(2),
+          cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
+          cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
+          utilizationRate: other.utilizationRate || 0
+        }
+      }
+    )
+  } finally {
+    listLoading.value = false
+  }
+}, 500)
+
+const tab = ref<'表格' | '看板'>('表格')
+
+const currentTab = 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 | null = null
+let chartContainerEl: HTMLDivElement | null = null
+
+const xAxisData = ref<string[]>([])
+
+type ChartKey =
+  | 'cumulativeFuelConsumption'
+  | 'cumulativeGasInjection'
+  | 'cumulativePowerConsumption'
+  | 'cumulativeWaterInjection'
+  | 'transitTime'
+  | 'utilizationRate'
+
+interface LegendItem {
+  name: string
+  key: ChartKey
+  unit: string
+  decimals: number
+}
+
+const legendItems: LegendItem[] = [
+  { name: '累计油耗 (升)', key: 'cumulativeFuelConsumption', unit: '升', decimals: 2 },
+  { name: '累计注气量 (万方)', key: 'cumulativeGasInjection', unit: '万方', decimals: 2 },
+  { name: '累计用电量 (KWh)', key: 'cumulativePowerConsumption', unit: 'KWh', decimals: 2 },
+  { name: '累计注水量 (方)', key: 'cumulativeWaterInjection', unit: '方', decimals: 2 },
+  { name: '平均时效 (%)', key: 'transitTime', unit: '%', decimals: 2 },
+  { name: '设备利用率 (%)', key: 'utilizationRate', unit: '%', decimals: 2 }
+]
+
+const legendItemMap = legendItems.reduce<Record<string, LegendItem>>((map, item) => {
+  map[item.name] = item
+  return map
+}, {})
+
+const chartData = ref<Record<ChartKey, number[]>>({
+  cumulativeFuelConsumption: [],
+  cumulativeGasInjection: [],
+  cumulativePowerConsumption: [],
+  cumulativeWaterInjection: [],
+  transitTime: [],
+  utilizationRate: []
+})
+
+let chartLoading = ref(false)
+
+const getChart = useDebounceFn(async () => {
+  chartLoading.value = true
+
+  try {
+    const res = await IotRhDailyReportApi.getIotRhDailyReportSummaryPolyline(props.query)
+
+    chartData.value = {
+      cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
+      // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
+      cumulativeGasInjection: res.map((item) => (item.cumulativeGasInjection || 0) / 10000),
+      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
+      // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
+      cumulativeWaterInjection: res.map((item) => item.cumulativeWaterInjection || 0),
+      // cumulativeWaterInjection: res.map((item) => (item.cumulativeWaterInjection || 0) / 10000),
+      transitTime: res.map((item) => (item.transitTime || 0) * 100),
+      utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
+    }
+
+    xAxisData.value = res.map((item) => item.reportDate || '')
+    resetVisibleZoomRange()
+  } finally {
+    chartLoading.value = false
+  }
+}, 500)
+
+const resizer = useDebounceFn(() => {
+  chart?.resize()
+}, 100)
+
+const selectedLegends = ref<Record<string, boolean>>({})
+const visibleZoomRange = ref({
+  startIndex: 0,
+  endIndex: 0
+})
+
+const NORMALIZED_AXIS_MIN = 0
+const NORMALIZED_AXIS_MAX = 100
+const NORMALIZED_AXIS_CENTER = 50
+const NORMALIZED_AXIS_PADDING = 4
+
+const ensureLegendSelection = () => {
+  legendItems.forEach(({ name }) => {
+    if (selectedLegends.value[name] === undefined) {
+      selectedLegends.value[name] = true
+    }
+  })
+}
+
+const resetVisibleZoomRange = () => {
+  const lastIndex = Math.max(xAxisData.value.length - 1, 0)
+
+  visibleZoomRange.value = {
+    startIndex: 0,
+    endIndex: lastIndex
+  }
+}
+
+const clampZoomIndex = (value: number, lastIndex: number) => {
+  return Math.min(Math.max(Math.round(value), 0), lastIndex)
+}
+
+const syncVisibleZoomRangeFromChart = () => {
+  const lastIndex = Math.max(xAxisData.value.length - 1, 0)
+
+  if (!chart || lastIndex <= 0) {
+    resetVisibleZoomRange()
+    return
+  }
+
+  const dataZoomOptions = chart.getOption().dataZoom
+  const primaryDataZoom = Array.isArray(dataZoomOptions) ? dataZoomOptions[0] : undefined
+
+  if (!primaryDataZoom) {
+    resetVisibleZoomRange()
+    return
+  }
+
+  const startValue = Number(primaryDataZoom.startValue)
+  const endValue = Number(primaryDataZoom.endValue)
+
+  let nextStartIndex = 0
+  let nextEndIndex = lastIndex
+
+  if (Number.isFinite(startValue) && Number.isFinite(endValue)) {
+    nextStartIndex = clampZoomIndex(startValue, lastIndex)
+    nextEndIndex = clampZoomIndex(endValue, lastIndex)
+  } else {
+    const startPercent = Number(primaryDataZoom.start ?? 0)
+    const endPercent = Number(primaryDataZoom.end ?? 100)
+
+    nextStartIndex = clampZoomIndex((startPercent / 100) * lastIndex, lastIndex)
+    nextEndIndex = clampZoomIndex((endPercent / 100) * lastIndex, lastIndex)
+  }
+
+  visibleZoomRange.value = {
+    startIndex: Math.min(nextStartIndex, nextEndIndex),
+    endIndex: Math.max(nextStartIndex, nextEndIndex)
+  }
+}
+
+const getVisibleDatasetValues = (dataset: number[]) => {
+  if (!dataset.length) return []
+
+  const lastIndex = dataset.length - 1
+  const startIndex = clampZoomIndex(visibleZoomRange.value.startIndex, lastIndex)
+  const endIndex = Math.max(startIndex, clampZoomIndex(visibleZoomRange.value.endIndex, lastIndex))
+
+  return dataset.slice(startIndex, endIndex + 1).filter((value) => Number.isFinite(value))
+}
+
+const normalizeSeriesData = (dataset: number[]) => {
+  if (!dataset.length) return []
+
+  const validValues = getVisibleDatasetValues(dataset)
+
+  if (!validValues.length) {
+    return dataset.map(() => NORMALIZED_AXIS_MIN)
+  }
+
+  const min = Math.min(...validValues)
+  const max = Math.max(...validValues)
+
+  if (min === max) {
+    return dataset.map(() => (min === 0 ? NORMALIZED_AXIS_MIN : NORMALIZED_AXIS_CENTER))
+  }
+
+  const usableHeight = NORMALIZED_AXIS_MAX - NORMALIZED_AXIS_MIN - NORMALIZED_AXIS_PADDING * 2
+
+  return dataset.map((value) =>
+    Number((NORMALIZED_AXIS_PADDING + ((value - min) / (max - min)) * usableHeight).toFixed(2))
+  )
+}
+
+const formatTrendAxisLabel = (value: number) => {
+  if (value === NORMALIZED_AXIS_MIN) return '低'
+  if (value === NORMALIZED_AXIS_CENTER) return '中'
+  if (value === NORMALIZED_AXIS_MAX) return '高'
+  return ''
+}
+
+const formatSeriesValue = (name: string, value: number) => {
+  const item = legendItemMap[name]
+  const safeValue = Number.isFinite(value) ? value : 0
+
+  if (!item) return safeValue.toFixed(2)
+
+  return `${safeValue.toFixed(item.decimals)} ${item.unit}`
+}
+
+const getSeries = () => {
+  const enableSampling = xAxisData.value.length > 120
+
+  return legendItems.map((item) => ({
+    name: item.name,
+    type: 'line',
+    smooth: false,
+    showSymbol: true,
+    symbol: 'circle',
+    symbolSize: 6,
+    connectNulls: true,
+    lineStyle: {
+      width: 2
+    },
+    emphasis: {
+      focus: 'series'
+    },
+    sampling: enableSampling ? 'lttb' : undefined,
+    progressive: 300,
+    progressiveThreshold: 1500,
+    data: normalizeSeriesData(chartData.value[item.key])
+  }))
+}
+
+const getChartOption = () => ({
+  animation: xAxisData.value.length <= 120,
+  animationDuration: 280,
+  animationDurationUpdate: 180,
+  grid: {
+    top: 72,
+    right: 24,
+    bottom: 68,
+    left: 48,
+    containLabel: true
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: { type: 'line' },
+    formatter: (params: any) => {
+      const list = Array.isArray(params) ? params : [params]
+
+      if (!list.length) return ''
+
+      const content = list.map((item: any) => {
+        const legendItem = legendItemMap[item.seriesName]
+        const realValue = legendItem ? (chartData.value[legendItem.key][item.dataIndex] ?? 0) : 0
+
+        return `<div class="flex items-center justify-between mt-1 gap-1">
+            <span>${item.marker} ${item.seriesName}</span>
+            <span>${formatSeriesValue(item.seriesName, realValue)}</span>
+          </div>`
+      })
+
+      return `${list[0].axisValueLabel}<br>${content.join('')}`
+    }
+  },
+  legend: {
+    type: 'scroll',
+    top: 16,
+    data: legendItems.map((item) => item.name),
+    selected: selectedLegends.value,
+    show: true
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: false,
+    data: xAxisData.value,
+    axisLabel: {
+      hideOverlap: true
+    }
+  },
+  dataZoom: [
+    {
+      type: 'inside',
+      xAxisIndex: 0,
+      filterMode: 'none',
+      throttle: 50,
+      startValue: visibleZoomRange.value.startIndex,
+      endValue: visibleZoomRange.value.endIndex
+    },
+    {
+      type: 'slider',
+      xAxisIndex: 0,
+      filterMode: 'none',
+      height: 18,
+      bottom: 8,
+      brushSelect: false,
+      showDetail: false,
+      moveHandleSize: 0,
+      throttle: 50,
+      startValue: visibleZoomRange.value.startIndex,
+      endValue: visibleZoomRange.value.endIndex
+    }
+  ],
+  yAxis: {
+    type: 'value',
+    min: NORMALIZED_AXIS_MIN,
+    max: NORMALIZED_AXIS_MAX,
+    splitNumber: 4,
+    name: '相对趋势',
+    nameGap: 16,
+    axisLabel: {
+      formatter: formatTrendAxisLabel
+    },
+    splitLine: {
+      lineStyle: {
+        type: 'dashed'
+      }
+    }
+  },
+  series: getSeries()
+})
+
+const initChart = () => {
+  if (!chartRef.value) return
+
+  if (chart && chartContainerEl === chartRef.value) {
+    return
+  }
+
+  chart?.dispose()
+  window.removeEventListener('resize', resizer)
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas', useDirtyRect: true })
+  chartContainerEl = chartRef.value
+  window.addEventListener('resize', resizer)
+
+  chart.off('legendselectchanged')
+  chart.on('legendselectchanged', (params: any) => {
+    selectedLegends.value = { ...params.selected }
+  })
+
+  chart.off('datazoom')
+  chart.on('datazoom', () => {
+    syncVisibleZoomRangeFromChart()
+    chart?.setOption(
+      {
+        series: getSeries()
+      },
+      { lazyUpdate: true }
+    )
+  })
+}
+
+const render = () => {
+  if (!chartRef.value) return
+
+  initChart()
+
+  ensureLegendSelection()
+
+  chart?.setOption(getChartOption(), { notMerge: true, lazyUpdate: true })
+}
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizer)
+  chart?.dispose()
+  chart = null
+  chartContainerEl = null
+})
+
+const handleQuery = () => {
+  getChart().then(() => {
+    render()
+  })
+  getList()
+  getTotal()
+}
+
+watch(
+  () => [
+    props.refreshKey,
+    props.query.deptId,
+    props.query.contractName,
+    props.query.taskName,
+    props.query.createTime?.[0],
+    props.query.createTime?.[1]
+  ],
+  (val) => {
+    if (!props.query.createTime) {
+      totalWork.value.totalCount = 0
+      totalWork.value.notReported = 0
+      totalWork.value.alreadyReported = 0
+    }
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+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 IotRhDailyReportApi.exportRhDailyReportStatistics(props.query)
+
+  download.excel(res, '瑞恒日报统计数据.xlsx')
+}
+
+const exportAll = async () => {
+  if (tab.value === '看板') exportChart()
+  else exportData()
+}
+
+const message = useMessage()
+
+const unfilledDialogRef = ref()
+
+const openUnfilledDialog = () => {
+  // 检查是否选择了创建时间
+  if (!props.query.createTime || props.query.createTime.length === 0) {
+    message.warning('请先选择创建时间范围')
+    return
+  }
+
+  // 打开弹窗
+  unfilledDialogRef.value?.open()
+}
+
+const router = useRouter()
+
+const tolist = (id: number, non: boolean = false) => {
+  const { pageNo, pageSize, ...rest } = props.query
+
+  router.push({
+    path: '/iotdayilyreport/IotRhDailyReport',
+    query: {
+      ...rest,
+      deptId: id,
+      ...(non ? { nonProductFlag: 'Y' } : {})
+    }
+  })
+}
+
+const { ZmTable, ZmTableColumn } = useTableComponents()
+</script>
+
+<template>
+  <div class="grid grid-rows-[128px_1fr] gap-4 h-full min-h-0">
+    <div class="grid grid-cols-8 gap-8">
+      <template v-for="info in totalWorkKeys" :key="info[0]">
+        <el-tooltip :disabled="info[0] !== 'totalGasInjection'" placement="top">
+          <template #content>
+            <div>
+              累计氮气注气量:{{
+                formatGasInjectionTooltipValue(totalWorkloadDetail.totalN2GasInjection)
+              }}
+              万方
+            </div>
+            <div>
+              累计天然气注气量:{{
+                formatGasInjectionTooltipValue(totalWorkloadDetail.totalNaturalGasInjection)
+              }}
+              万方
+            </div>
+          </template>
+          <div
+            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"
+              :class="{ 'cursor-help': info[0] === 'totalGasInjection' }"
+              :start-val="0"
+              :end-val="totalWork[info[0]]"
+              :decimals="info[4]"
+              @click="info[2] === '未填报' ? openUnfilledDialog() : ''"
+            >
+              <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
+            </count-to>
+
+            <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
+              {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
+            </div>
+          </div>
+        </el-tooltip>
+      </template>
+    </div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2 min-h-0">
+      <div class="flex h-12 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">{{ `${props.deptName}-${tab}` }}</h3>
+        <el-button size="default" type="primary" @click="exportAll">导出</el-button>
+      </div>
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ height }">
+            <Motion
+              as="div"
+              :style="{ position: 'relative', overflow: 'hidden' }"
+              :animate="{ height: `${height}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: `${height}px` }">
+                    <zm-table
+                      v-if="currentTab === '表格'"
+                      :loading="listLoading"
+                      :data="list"
+                      :height="height"
+                      show-border
+                    >
+                      <template v-for="item in columns(type)" :key="item.prop">
+                        <zm-table-column
+                          v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
+                          :label="item.label"
+                          :prop="item.prop"
+                          :formatter="formatter"
+                          :action="item.action"
+                        />
+                        <zm-table-column
+                          v-else-if="item.prop === 'name'"
+                          :label="item.label"
+                          :prop="item.prop"
+                        >
+                          <template #default="{ row }">
+                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                              row.name
+                            }}</el-button>
+                          </template>
+                        </zm-table-column>
+                        <zm-table-column v-else :label="item.label" :prop="item.prop">
+                          <template #default="{ row }">
+                            <el-button
+                              v-if="row.nonProductiveTime > 0"
+                              text
+                              type="primary"
+                              @click.prevent="tolist(row.id, true)"
+                            >
+                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
+                            </el-button>
+                            <span v-else>
+                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
+                            </span>
+                          </template>
+                        </zm-table-column>
+                      </template>
+                    </zm-table>
+                    <div
+                      ref="chartRef"
+                      v-loading="chartLoading"
+                      v-else
+                      :style="{ width: `100%`, height: `${height}px` }"
+                    >
+                    </div>
+                  </div>
+                </Motion>
+              </AnimatePresence>
+            </Motion>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </div>
+  </div>
+
+  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="props.query" />
+</template>

+ 378 - 0
src/views/pms/iotrhdailyreport/components/NonProductionEfficiency.vue

@@ -0,0 +1,378 @@
+<script setup lang="ts">
+import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import { useDebounceFn } from '@vueuse/core'
+import * as echarts from 'echarts'
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+}
+
+const props = defineProps<{
+  query: Query
+  deptName: string
+  refreshKey: number
+}>()
+
+interface ListItem {
+  id?: number | null
+  name?: string | null
+  type?: string
+  teamId?: number
+  projectDeptId?: number
+  teamName?: string
+  projectDeptName?: string
+  accidentTime: number
+  repairTime: number
+  selfStopTime: number
+  complexityTime: number
+  relocationTime: number
+  rectificationTime: number
+  waitingStopTime: number
+  winterBreakTime: number
+  partyaDesign: number
+  partyaPrepare: number
+  partyaResource: number
+  otherNptTime: number
+  nptTotal: number
+  nptRate: number
+  calendarTime: number
+}
+
+const list = ref<ListItem[]>([])
+const loading = ref(false)
+const tab = ref<'表格' | '看板'>('表格')
+const chartRef = ref<HTMLDivElement | null>(null)
+let chart: echarts.ECharts | null = null
+const router = useRouter()
+
+const nonProductionTimeFields: [keyof ListItem, string][] = [
+  ['accidentTime', '工程质量'],
+  ['repairTime', '设备故障'],
+  ['selfStopTime', '设备保养'],
+  ['complexityTime', '技术受限'],
+  ['relocationTime', '生产配合'],
+  ['rectificationTime', '生产组织'],
+  ['waitingStopTime', '不可抗力'],
+  ['winterBreakTime', '待命'],
+  ['partyaDesign', '甲方设计'],
+  ['partyaPrepare', '甲方准备'],
+  ['partyaResource', '甲方资源'],
+  ['otherNptTime', '其它']
+]
+
+const getQueryWithoutPage = () => {
+  const { pageNo: _pageNo, pageSize: _pageSize, ...query } = props.query
+
+  void _pageNo
+  void _pageSize
+
+  return query
+}
+
+const normalizeListData = (data: any) => {
+  const rows = Array.isArray(data) ? data : data?.list || data?.records || []
+
+  return rows.map(({ projectDeptId, projectDeptName, teamId, teamName, type, ...other }) => ({
+    ...other,
+    type,
+    projectDeptId,
+    projectDeptName,
+    teamId,
+    teamName,
+    id: type === '2' ? projectDeptId : teamId,
+    name: type === '2' ? projectDeptName : teamName
+  }))
+}
+
+const loadList = useDebounceFn(async function () {
+  loading.value = true
+  try {
+    const data = await IotRhDailyReportApi.nptStatistics(getQueryWithoutPage())
+
+    list.value = normalizeListData(data)
+    if (tab.value === '看板') {
+      nextTick(renderChart)
+    }
+  } finally {
+    loading.value = false
+  }
+}, 500)
+
+function handleQuery() {
+  loadList()
+}
+
+watch(
+  () => [
+    props.refreshKey,
+    props.query.deptId,
+    props.query.contractName,
+    props.query.taskName,
+    props.query.createTime?.[0],
+    props.query.createTime?.[1]
+  ],
+  () => {
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+const handleSelectTab = (val: '表格' | '看板') => {
+  tab.value = val
+
+  if (val === '看板') {
+    nextTick(renderChart)
+  } else {
+    chart?.dispose()
+    chart = null
+  }
+}
+
+const formatNumber = (value: unknown) => {
+  const num = Number(value || 0)
+  return Number.isInteger(num) ? `${num}` : num.toFixed(2)
+}
+
+const getFieldTotal = (field: keyof ListItem) => {
+  return list.value.reduce((total, row) => total + Number(row[field] || 0), 0)
+}
+
+const getSummaryRate = () => {
+  const nptTotal = getFieldTotal('nptTotal')
+  const calendarTime = getFieldTotal('calendarTime')
+
+  if (!calendarTime) return '0.00%'
+
+  return `${((nptTotal / calendarTime) * 100).toFixed(2)}%`
+}
+
+const getSummaries = ({ columns }: { columns: Array<{ property?: keyof ListItem | string }> }) => {
+  const nonSummaryFields = ['id', 'name', 'teamName', 'projectDeptName']
+
+  return columns.map((column, index) => {
+    if (index === 0) return '合计'
+
+    const property = column.property as keyof ListItem | undefined
+    if (!property) return ''
+
+    if (property === 'nptRate') return getSummaryRate()
+    if (nonSummaryFields.includes(property)) return ''
+
+    return formatNumber(getFieldTotal(property))
+  })
+}
+
+const formatTeamName = (row: ListItem) => {
+  return row.name || '-'
+}
+
+const formatRate = (row: ListItem) => {
+  return `${(Number(row.nptRate || 0) * 100).toFixed(2)}%`
+}
+
+const handleRowClick = (row: ListItem) => {
+  if (!row.id) return
+
+  router.push({
+    name: 'IotRhDailyReport',
+    query: {
+      deptId: row.id,
+      createTime: props.query.createTime
+    }
+  })
+}
+
+const getClickableRowClassName = () => 'summary-clickable-row'
+
+const getPieData = () => {
+  return list.value
+    .map((row) => ({
+      name: formatTeamName(row),
+      value: Number(row.nptTotal || 0)
+    }))
+    .filter((item) => item.value > 0)
+}
+
+const resizeChart = () => {
+  chart?.resize()
+}
+
+const renderChart = () => {
+  if (!chartRef.value) return
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+
+  window.removeEventListener('resize', resizeChart)
+  window.addEventListener('resize', resizeChart)
+
+  const pieData = getPieData()
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}<br/>NPT合计: {c} H<br/>占比: {d}%'
+    },
+    legend: {
+      type: 'scroll',
+      orient: 'vertical',
+      right: 24,
+      top: 32,
+      bottom: 24
+    },
+    graphic:
+      pieData.length === 0
+        ? {
+            type: 'text',
+            left: 'center',
+            top: 'middle',
+            style: {
+              text: '暂无NPT数据',
+              fill: '#909399',
+              fontSize: 14
+            }
+          }
+        : undefined,
+    series: [
+      {
+        name: 'NPT合计',
+        type: 'pie',
+        radius: ['42%', '68%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: true,
+        itemStyle: {
+          borderRadius: 4,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: {
+          formatter: '{b}: {d}%'
+        },
+        data: pieData
+      }
+    ]
+  })
+}
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  chart?.dispose()
+})
+
+const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
+</script>
+
+<template>
+  <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2 h-full min-h-0">
+    <div class="flex h-12 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> -->
+      <div class="w-80px"></div>
+    </div>
+
+    <div class="flex-1 relative min-h-0">
+      <el-auto-resizer class="absolute">
+        <template #default="{ width, height }">
+          <zm-table
+            v-if="tab === '表格'"
+            :data="list"
+            :loading="loading"
+            :width="width"
+            :max-height="height"
+            :height="height"
+            show-border
+            show-summary
+            :summary-method="getSummaries"
+            :row-class-name="getClickableRowClassName"
+            @row-click="handleRowClick"
+          >
+            <zm-table-column
+              prop="name"
+              label="队伍"
+              min-width="120"
+              fixed="left"
+              cover-formatter
+              :real-value="formatTeamName"
+            />
+            <zm-table-column
+              v-for="[prop, label] in nonProductionTimeFields"
+              :key="prop"
+              :prop="prop"
+              :label="label"
+              min-width="92"
+              cover-formatter
+              :real-value="(row: ListItem) => formatNumber(row[prop])"
+            />
+            <zm-table-column label="npt合计" is-parent>
+              <zm-table-column
+                prop="nptTotal"
+                label="时长(H)"
+                min-width="92"
+                cover-formatter
+                :real-value="(row: ListItem) => formatNumber(row.nptTotal)"
+              />
+              <zm-table-column
+                prop="nptRate"
+                label="占比"
+                min-width="92"
+                cover-formatter
+                :real-value="formatRate"
+              />
+            </zm-table-column>
+            <zm-table-column
+              prop="calendarTime"
+              label="自然时间"
+              min-width="92"
+              cover-formatter
+              action
+              :real-value="(row: ListItem) => formatNumber(row.calendarTime)"
+            />
+          </zm-table>
+          <div
+            v-else
+            ref="chartRef"
+            v-loading="loading"
+            class="npt-board-container"
+            :style="{ width: `${width}px`, height: `${height}px` }"
+          >
+          </div>
+        </template>
+      </el-auto-resizer>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.npt-cell-highlight) {
+  background-color: #fff566 !important;
+}
+
+:deep(.summary-clickable-row) {
+  cursor: pointer;
+}
+
+.npt-board-container {
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 4px;
+}
+</style>

+ 24 - 0
src/views/pms/iotrhdailyreport/index.vue

@@ -44,6 +44,23 @@ const initQuery: Query = {
 
 const query = ref<Query>({ ...initQuery })
 
+const getRouteCreateTime = () => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    return createTime.filter((item): item is string => typeof item === 'string')
+  }
+
+  return typeof createTime === 'string'
+    ? [createTime]
+    : [...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
+}
+
+const syncQueryFromRoute = () => {
+  query.value.deptId = route.query.deptId ? Number(route.query.deptId) : id
+  query.value.createTime = getRouteCreateTime()
+}
+
 const totalWorkKeys: [string, string, string, string, number][] = [
   ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
   [
@@ -199,6 +216,13 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => [route.query.deptId, route.query.createTime],
+  () => {
+    syncQueryFromRoute()
+  }
+)
+
 // const selectedRowData = ref<any>(null)
 
 // const formRef = ref()

+ 43 - 868
src/views/pms/iotrhdailyreport/summary.vue

@@ -1,25 +1,9 @@
 <script setup lang="ts">
 import dayjs from 'dayjs'
-import type { ECharts } from 'echarts/core'
-import { DataZoomComponent } from 'echarts/components'
-import {
-  IotRhDailyReportApi,
-  type IotRhDailyReportTotalWorkloadVO
-} from '@/api/pms/iotrhdailyreport'
-import { useDebounceFn } from '@vueuse/core'
-import CountTo from '@/components/count-to1.vue'
-import echarts from '@/plugins/echarts'
-import UnfilledReportDialog from './UnfilledReportDialog.vue'
-
-import { Motion, AnimatePresence } from 'motion-v'
-
 import { rangeShortcuts } from '@/utils/formatTime'
-import download from '@/utils/download'
-
 import { useUserStore } from '@/store/modules/user'
-import { useTableComponents } from '@/components/ZmTable/useTableComponents'
-
-echarts.use([DataZoomComponent])
+import DailyStatistics from './components/DailyStatistics.vue'
+import NonProductionEfficiency from './components/NonProductionEfficiency.vue'
 
 const deptId = useUserStore().getUser.deptId
 
@@ -34,738 +18,42 @@ interface Query {
 
 const id = deptId
 
-const query = ref<Query>({
+const createDefaultQuery = (): Query => ({
   pageNo: 1,
   pageSize: 10,
   deptId: deptId,
+  contractName: '',
+  taskName: '',
   createTime: [
     ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
   ]
 })
 
-const totalWorkKeys: [string, string, string, string, number][] = [
-  ['totalGasInjection', '万方', '累计注气量', 'i-material-symbols:cloud-outline text-sky', 2],
-  [
-    'totalWaterInjection',
-    '万方',
-    '累计注水量',
-    'i-material-symbols:water-drop-outline-rounded text-sky',
-    2
-  ],
-  [
-    'utilizationRate',
-    '%',
-    '设备利用率',
-    'i-material-symbols:check-circle-outline-rounded text-emerald',
-    0
-  ],
-  [
-    'totalPowerConsumption',
-    'MWh',
-    '累计用电量',
-    'i-material-symbols:electric-bolt-outline-rounded text-sky',
-    2
-  ],
-  [
-    'totalFuelConsumption',
-    '升',
-    '累计油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky',
-    2
-  ],
-  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
-  [
-    'alreadyReported',
-    '个',
-    '已填报',
-    'i-material-symbols:check-circle-outline-rounded text-emerald',
-    0
-  ],
-  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
-]
-
-const totalWork = ref({
-  totalCount: 0,
-  alreadyReported: 0,
-  notReported: 0,
-  totalFuelConsumption: 0,
-  totalPowerConsumption: 0,
-  totalWaterInjection: 0,
-  totalGasInjection: 0,
-  utilizationRate: 0
-})
-
-const totalLoading = ref(false)
-
-const totalWorkloadDetail = ref({
-  totalN2GasInjection: 0,
-  totalNaturalGasInjection: 0
-})
-
-const formatGasInjectionTooltipValue = (value?: number | null) => ((value || 0) / 10000).toFixed(4)
-
-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: IotRhDailyReportTotalWorkloadVO = await IotRhDailyReportApi.totalWorkload(other)
-
-    totalWorkloadDetail.value = {
-      totalN2GasInjection: res2.totalN2GasInjection || 0,
-      totalNaturalGasInjection: res2.totalNaturalGasInjection || 0
-    }
-
-    totalWork.value = {
-      ...totalWork.value,
-      ...res2,
-      totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
-      totalWaterInjection: (res2.totalWaterInjection || 0) / 10000,
-      totalGasInjection: (res2.totalGasInjection || 0) / 10000,
-      totalFuelConsumption: res2.totalFuelConsumption || 0,
-      utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
-    }
-  } finally {
-    totalLoading.value = false
-  }
-}, 500)
-
-interface List {
-  id: number | null
-  name: string | null
-  type: '1' | '2' | '3'
-  cumulativeGasInjection: number | null
-  cumulativeWaterInjection: number | null
-  cumulativePowerConsumption: number | null
-  cumulativeFuelConsumption: number | null
-  transitTime: number | null
-  nonProductiveTime: number | null
-  utilizationRate: number | null
-}
-
-const list = ref<List[]>([])
-
-const type = ref('2')
-
-function checkIsSameDay(createTime: string[]): boolean {
-  if (!createTime || createTime.length < 2) {
-    return false
-  }
-
-  const [startTime, endTime] = createTime
-
-  return dayjs(startTime).isSame(endTime, 'day')
-}
-
-const columns = (type: string) => {
-  return [
-    {
-      label: type === '2' ? '项目部' : '队伍',
-      prop: 'name'
-    },
-    {
-      label: '累计注气量(万方)',
-      prop: 'cumulativeGasInjection'
-    },
-    {
-      label: '累计注水量(万方)',
-      prop: 'cumulativeWaterInjection'
-    },
-    {
-      label: '累计用电量(MWh)',
-      prop: 'cumulativePowerConsumption'
-    },
-    {
-      label: '累计油耗(升)',
-      prop: 'cumulativeFuelConsumption'
-    },
-    {
-      label: '平均时效(%)',
-      prop: 'transitTime'
-    },
-    {
-      label: '非生产时效(%)',
-      prop: 'nonProductiveTime'
-    },
-    ...(type === '2' && checkIsSameDay(query.value.createTime)
-      ? [
-          {
-            label: '队伍总数',
-            prop: 'teamCount'
-          },
-          {
-            label: '驻地待命',
-            prop: 'zddmTeamCount'
-          },
-          {
-            label: '施工准备',
-            prop: 'zbTeamCount'
-          },
-          {
-            label: '施工队伍',
-            prop: 'sgTeamCount'
-          }
-        ]
-      : []),
-    {
-      label: '设备利用率(%)',
-      prop: 'utilizationRate',
-      action: true
-    }
-  ]
-}
-
-const listLoading = ref(false)
-
-const formatter = (row: List, column: any) => {
-  if (column.property === 'transitTime') {
-    return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
-  } else if (column.property === 'nonProductiveTime') {
-    return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
-  } else if (column.property === 'utilizationRate') {
-    return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
-  } else return row[column.property] ?? 0
-}
-
-const getList = useDebounceFn(async () => {
-  listLoading.value = true
-  try {
-    const res = await IotRhDailyReportApi.getIotRhDailyReportSummary(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,
-          cumulativeGasInjection: ((other.cumulativeGasInjection || 0) / 10000).toFixed(4),
-          cumulativeWaterInjection: ((other.cumulativeWaterInjection || 0) / 10000).toFixed(2),
-          cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
-          cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
-          utilizationRate: other.utilizationRate || 0
-        }
-      }
-    )
-  } finally {
-    listLoading.value = false
-  }
-}, 500)
-
-const tab = ref<'表格' | '看板'>('表格')
-
-const currentTab = ref<'表格' | '看板'>('表格')
-
+const query = ref<Query>(createDefaultQuery())
+const activeTab = 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 | null = null
-let chartContainerEl: HTMLDivElement | null = null
-
-const xAxisData = ref<string[]>([])
-
-type ChartKey =
-  | 'cumulativeFuelConsumption'
-  | 'cumulativeGasInjection'
-  | 'cumulativePowerConsumption'
-  | 'cumulativeWaterInjection'
-  | 'transitTime'
-  | 'utilizationRate'
-
-interface LegendItem {
-  name: string
-  key: ChartKey
-  unit: string
-  decimals: number
-}
-
-const legendItems: LegendItem[] = [
-  { name: '累计油耗 (升)', key: 'cumulativeFuelConsumption', unit: '升', decimals: 2 },
-  { name: '累计注气量 (万方)', key: 'cumulativeGasInjection', unit: '万方', decimals: 2 },
-  { name: '累计用电量 (KWh)', key: 'cumulativePowerConsumption', unit: 'KWh', decimals: 2 },
-  { name: '累计注水量 (方)', key: 'cumulativeWaterInjection', unit: '方', decimals: 2 },
-  { name: '平均时效 (%)', key: 'transitTime', unit: '%', decimals: 2 },
-  { name: '设备利用率 (%)', key: 'utilizationRate', unit: '%', decimals: 2 }
-]
-
-const legendItemMap = legendItems.reduce<Record<string, LegendItem>>((map, item) => {
-  map[item.name] = item
-  return map
-}, {})
-
-const chartData = ref<Record<ChartKey, number[]>>({
-  cumulativeFuelConsumption: [],
-  cumulativeGasInjection: [],
-  cumulativePowerConsumption: [],
-  cumulativeWaterInjection: [],
-  transitTime: [],
-  utilizationRate: []
-})
-
-let chartLoading = ref(false)
-
-const getChart = useDebounceFn(async () => {
-  chartLoading.value = true
-
-  try {
-    const res = await IotRhDailyReportApi.getIotRhDailyReportSummaryPolyline(query.value)
-
-    chartData.value = {
-      cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
-      // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
-      cumulativeGasInjection: res.map((item) => (item.cumulativeGasInjection || 0) / 10000),
-      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
-      // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
-      cumulativeWaterInjection: res.map((item) => item.cumulativeWaterInjection || 0),
-      // cumulativeWaterInjection: res.map((item) => (item.cumulativeWaterInjection || 0) / 10000),
-      transitTime: res.map((item) => (item.transitTime || 0) * 100),
-      utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
-    }
-
-    xAxisData.value = res.map((item) => item.reportDate || '')
-    resetVisibleZoomRange()
-  } finally {
-    chartLoading.value = false
-  }
-}, 500)
-
-const resizer = useDebounceFn(() => {
-  chart?.resize()
-}, 100)
-
-const selectedLegends = ref<Record<string, boolean>>({})
-const visibleZoomRange = ref({
-  startIndex: 0,
-  endIndex: 0
-})
-
-const NORMALIZED_AXIS_MIN = 0
-const NORMALIZED_AXIS_MAX = 100
-const NORMALIZED_AXIS_CENTER = 50
-const NORMALIZED_AXIS_PADDING = 4
-
-const ensureLegendSelection = () => {
-  legendItems.forEach(({ name }) => {
-    if (selectedLegends.value[name] === undefined) {
-      selectedLegends.value[name] = true
-    }
-  })
-}
-
-const resetVisibleZoomRange = () => {
-  const lastIndex = Math.max(xAxisData.value.length - 1, 0)
-
-  visibleZoomRange.value = {
-    startIndex: 0,
-    endIndex: lastIndex
-  }
-}
-
-const clampZoomIndex = (value: number, lastIndex: number) => {
-  return Math.min(Math.max(Math.round(value), 0), lastIndex)
-}
-
-const syncVisibleZoomRangeFromChart = () => {
-  const lastIndex = Math.max(xAxisData.value.length - 1, 0)
-
-  if (!chart || lastIndex <= 0) {
-    resetVisibleZoomRange()
-    return
-  }
-
-  const dataZoomOptions = chart.getOption().dataZoom
-  const primaryDataZoom = Array.isArray(dataZoomOptions) ? dataZoomOptions[0] : undefined
-
-  if (!primaryDataZoom) {
-    resetVisibleZoomRange()
-    return
-  }
-
-  const startValue = Number(primaryDataZoom.startValue)
-  const endValue = Number(primaryDataZoom.endValue)
-
-  let nextStartIndex = 0
-  let nextEndIndex = lastIndex
-
-  if (Number.isFinite(startValue) && Number.isFinite(endValue)) {
-    nextStartIndex = clampZoomIndex(startValue, lastIndex)
-    nextEndIndex = clampZoomIndex(endValue, lastIndex)
-  } else {
-    const startPercent = Number(primaryDataZoom.start ?? 0)
-    const endPercent = Number(primaryDataZoom.end ?? 100)
-
-    nextStartIndex = clampZoomIndex((startPercent / 100) * lastIndex, lastIndex)
-    nextEndIndex = clampZoomIndex((endPercent / 100) * lastIndex, lastIndex)
-  }
-
-  visibleZoomRange.value = {
-    startIndex: Math.min(nextStartIndex, nextEndIndex),
-    endIndex: Math.max(nextStartIndex, nextEndIndex)
-  }
-}
-
-const getVisibleDatasetValues = (dataset: number[]) => {
-  if (!dataset.length) return []
-
-  const lastIndex = dataset.length - 1
-  const startIndex = clampZoomIndex(visibleZoomRange.value.startIndex, lastIndex)
-  const endIndex = Math.max(startIndex, clampZoomIndex(visibleZoomRange.value.endIndex, lastIndex))
-
-  return dataset.slice(startIndex, endIndex + 1).filter((value) => Number.isFinite(value))
-}
-
-const normalizeSeriesData = (dataset: number[]) => {
-  if (!dataset.length) return []
-
-  const validValues = getVisibleDatasetValues(dataset)
-
-  if (!validValues.length) {
-    return dataset.map(() => NORMALIZED_AXIS_MIN)
-  }
-
-  const min = Math.min(...validValues)
-  const max = Math.max(...validValues)
-
-  if (min === max) {
-    return dataset.map(() => (min === 0 ? NORMALIZED_AXIS_MIN : NORMALIZED_AXIS_CENTER))
-  }
-
-  const usableHeight = NORMALIZED_AXIS_MAX - NORMALIZED_AXIS_MIN - NORMALIZED_AXIS_PADDING * 2
-
-  return dataset.map((value) =>
-    Number((NORMALIZED_AXIS_PADDING + ((value - min) / (max - min)) * usableHeight).toFixed(2))
-  )
-}
-
-const formatTrendAxisLabel = (value: number) => {
-  if (value === NORMALIZED_AXIS_MIN) return '低'
-  if (value === NORMALIZED_AXIS_CENTER) return '中'
-  if (value === NORMALIZED_AXIS_MAX) return '高'
-  return ''
-}
-
-const formatSeriesValue = (name: string, value: number) => {
-  const item = legendItemMap[name]
-  const safeValue = Number.isFinite(value) ? value : 0
-
-  if (!item) return safeValue.toFixed(2)
-
-  return `${safeValue.toFixed(item.decimals)} ${item.unit}`
-}
-
-const getSeries = () => {
-  const enableSampling = xAxisData.value.length > 120
-
-  return legendItems.map((item) => ({
-    name: item.name,
-    type: 'line',
-    smooth: false,
-    showSymbol: true,
-    symbol: 'circle',
-    symbolSize: 6,
-    connectNulls: true,
-    lineStyle: {
-      width: 2
-    },
-    emphasis: {
-      focus: 'series'
-    },
-    sampling: enableSampling ? 'lttb' : undefined,
-    progressive: 300,
-    progressiveThreshold: 1500,
-    data: normalizeSeriesData(chartData.value[item.key])
-  }))
-}
-
-const getChartOption = () => ({
-  animation: xAxisData.value.length <= 120,
-  animationDuration: 280,
-  animationDurationUpdate: 180,
-  grid: {
-    top: 72,
-    right: 24,
-    bottom: 68,
-    left: 48,
-    containLabel: true
-  },
-  tooltip: {
-    trigger: 'axis',
-    axisPointer: { type: 'line' },
-    formatter: (params: any) => {
-      const list = Array.isArray(params) ? params : [params]
-
-      if (!list.length) return ''
-
-      const content = list.map((item: any) => {
-        const legendItem = legendItemMap[item.seriesName]
-        const realValue = legendItem ? (chartData.value[legendItem.key][item.dataIndex] ?? 0) : 0
-
-        return `<div class="flex items-center justify-between mt-1 gap-1">
-            <span>${item.marker} ${item.seriesName}</span>
-            <span>${formatSeriesValue(item.seriesName, realValue)}</span>
-          </div>`
-      })
-
-      return `${list[0].axisValueLabel}<br>${content.join('')}`
-    }
-  },
-  legend: {
-    type: 'scroll',
-    top: 16,
-    data: legendItems.map((item) => item.name),
-    selected: selectedLegends.value,
-    show: true
-  },
-  xAxis: {
-    type: 'category',
-    boundaryGap: false,
-    data: xAxisData.value,
-    axisLabel: {
-      hideOverlap: true
-    }
-  },
-  dataZoom: [
-    {
-      type: 'inside',
-      xAxisIndex: 0,
-      filterMode: 'none',
-      throttle: 50,
-      startValue: visibleZoomRange.value.startIndex,
-      endValue: visibleZoomRange.value.endIndex
-    },
-    {
-      type: 'slider',
-      xAxisIndex: 0,
-      filterMode: 'none',
-      height: 18,
-      bottom: 8,
-      brushSelect: false,
-      showDetail: false,
-      moveHandleSize: 0,
-      throttle: 50,
-      startValue: visibleZoomRange.value.startIndex,
-      endValue: visibleZoomRange.value.endIndex
-    }
-  ],
-  yAxis: {
-    type: 'value',
-    min: NORMALIZED_AXIS_MIN,
-    max: NORMALIZED_AXIS_MAX,
-    splitNumber: 4,
-    name: '相对趋势',
-    nameGap: 16,
-    axisLabel: {
-      formatter: formatTrendAxisLabel
-    },
-    splitLine: {
-      lineStyle: {
-        type: 'dashed'
-      }
-    }
-  },
-  series: getSeries()
-})
-
-const initChart = () => {
-  if (!chartRef.value) return
-
-  if (chart && chartContainerEl === chartRef.value) {
-    return
-  }
-
-  chart?.dispose()
-  window.removeEventListener('resize', resizer)
-  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas', useDirtyRect: true })
-  chartContainerEl = chartRef.value
-  window.addEventListener('resize', resizer)
-
-  chart.off('legendselectchanged')
-  chart.on('legendselectchanged', (params: any) => {
-    selectedLegends.value = { ...params.selected }
-  })
-
-  chart.off('datazoom')
-  chart.on('datazoom', () => {
-    syncVisibleZoomRangeFromChart()
-    chart?.setOption(
-      {
-        series: getSeries()
-      },
-      { lazyUpdate: true }
-    )
-  })
-}
-
-const render = () => {
-  if (!chartRef.value) return
-
-  initChart()
-
-  ensureLegendSelection()
-
-  chart?.setOption(getChartOption(), { notMerge: true, lazyUpdate: true })
-}
-
-onUnmounted(() => {
-  window.removeEventListener('resize', resizer)
-  chart?.dispose()
-  chart = null
-  chartContainerEl = null
-})
+const refreshKey = ref(0)
 
 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 handleQuery = () => {
+  query.value.pageNo = 1
+  refreshKey.value += 1
 }
 
 const resetQuery = () => {
-  query.value = {
-    pageNo: 1,
-    pageSize: 10,
-    deptId: deptId,
-    contractName: '',
-    taskName: '',
-    createTime: [
-      ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
-    ]
-  }
-  handleQuery()
-}
-
-watch(
-  () => query.value.createTime,
-  (val) => {
-    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(() => {
+  query.value = createDefaultQuery()
+  deptName.value = '瑞恒兴域'
   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 IotRhDailyReportApi.exportRhDailyReportStatistics(query.value)
-
-  download.excel(res, '瑞恒日报统计数据.xlsx')
-}
-
-const exportAll = async () => {
-  if (tab.value === '看板') exportChart()
-  else exportData()
 }
-
-const message = useMessage()
-
-const unfilledDialogRef = ref()
-
-const openUnfilledDialog = () => {
-  // 检查是否选择了创建时间
-  if (!query.value.createTime || query.value.createTime.length === 0) {
-    message.warning('请先选择创建时间范围')
-    return
-  }
-
-  // 打开弹窗
-  unfilledDialogRef.value?.open()
-}
-
-const router = useRouter()
-
-const tolist = (id: number, non: boolean = false) => {
-  const { pageNo, pageSize, ...rest } = query.value
-
-  router.push({
-    path: '/iotdayilyreport/IotRhDailyReport',
-    query: {
-      ...rest,
-      deptId: id,
-      ...(non ? { nonProductFlag: 'Y' } : {})
-    }
-  })
-}
-
-const { ZmTable, ZmTableColumn } = useTableComponents()
 </script>
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[62px_128px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="grid grid-cols-[auto_1fr] grid-rows-[62px_48px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <DeptTreeSelect
       :deptId="id"
@@ -774,9 +62,6 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
       @node-click="handleDeptNodeClick"
       class="row-span-3"
     />
-    <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3">
-
-    </div> -->
     <el-form
       size="default"
       class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
@@ -820,147 +105,37 @@ const { ZmTable, ZmTableColumn } = useTableComponents()
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
       </el-form-item>
     </el-form>
-    <div class="grid grid-cols-8 gap-8">
-      <template v-for="info in totalWorkKeys" :key="info[0]">
-        <el-tooltip :disabled="info[0] !== 'totalGasInjection'" placement="top">
-          <template #content>
-            <div>
-              累计氮气注气量:{{
-                formatGasInjectionTooltipValue(totalWorkloadDetail.totalN2GasInjection)
-              }}
-              万方
-            </div>
-            <div>
-              累计天然气注气量:{{
-                formatGasInjectionTooltipValue(totalWorkloadDetail.totalNaturalGasInjection)
-              }}
-              万方
-            </div>
-          </template>
-          <div
-            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"
-              :class="{ 'cursor-help': info[0] === 'totalGasInjection' }"
-              :start-val="0"
-              :end-val="totalWork[info[0]]"
-              :decimals="info[4]"
-              @click="info[2] === '未填报' ? openUnfilledDialog() : ''"
-            >
-              <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
-            </count-to>
 
-            <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
-              {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
-            </div>
-          </div>
-        </el-tooltip>
-      </template>
-    </div>
-
-    <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2">
-      <div class="flex h-12 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>
-      <div class="flex-1 relative">
-        <el-auto-resizer class="absolute">
-          <template #default="{ height }">
-            <Motion
-              as="div"
-              :style="{ position: 'relative', overflow: 'hidden' }"
-              :animate="{ height: `${height}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: `${height}px` }">
-                    <zm-table
-                      v-if="currentTab === '表格'"
-                      :loading="listLoading"
-                      :data="list"
-                      :height="height"
-                      show-border
-                    >
-                      <template v-for="item in columns(type)" :key="item.prop">
-                        <zm-table-column
-                          v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
-                          :label="item.label"
-                          :prop="item.prop"
-                          :formatter="formatter"
-                          :action="item.action"
-                        />
-                        <zm-table-column
-                          v-else-if="item.prop === 'name'"
-                          :label="item.label"
-                          :prop="item.prop"
-                        >
-                          <template #default="{ row }">
-                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
-                              row.name
-                            }}</el-button>
-                          </template>
-                        </zm-table-column>
-                        <zm-table-column v-else :label="item.label" :prop="item.prop">
-                          <template #default="{ row }">
-                            <el-button
-                              v-if="row.nonProductiveTime > 0"
-                              text
-                              type="primary"
-                              @click.prevent="tolist(row.id, true)"
-                            >
-                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
-                            </el-button>
-                            <span v-else>
-                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
-                            </span>
-                          </template>
-                        </zm-table-column>
-                      </template>
-                    </zm-table>
-                    <div
-                      ref="chartRef"
-                      v-loading="chartLoading"
-                      v-else
-                      :style="{ width: `100%`, height: `${height}px` }"
-                    >
-                    </div>
-                  </div>
-                </Motion>
-              </AnimatePresence>
-            </Motion>
-          </template>
-        </el-auto-resizer>
-      </div>
-    </div>
+    <el-button-group class="justify-self-start self-center">
+      <el-button
+        size="default"
+        :type="activeTab === '日报统计' ? 'primary' : 'default'"
+        @click="activeTab = '日报统计'"
+      >
+        日报统计
+      </el-button>
+      <el-button
+        size="default"
+        :type="activeTab === '非生产时效' ? 'primary' : 'default'"
+        @click="activeTab = '非生产时效'"
+      >
+        非生产时效
+      </el-button>
+    </el-button-group>
+
+    <DailyStatistics
+      v-if="activeTab === '日报统计'"
+      :query="query"
+      :dept-name="deptName"
+      :refresh-key="refreshKey"
+    />
+    <NonProductionEfficiency
+      v-else
+      :query="query"
+      :dept-name="deptName"
+      :refresh-key="refreshKey"
+    />
   </div>
-
-  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" />
 </template>
 
 <style scoped>

+ 678 - 0
src/views/pms/iotrydailyreport/components/DailyStatistics.vue

@@ -0,0 +1,678 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+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 download from '@/utils/download'
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+  projectClassification: 1 | 2
+}
+
+const props = defineProps<{
+  query: Query
+  deptName: string
+  refreshKey: number
+}>()
+
+const totalWorkKeys: [string, string, string, string, number][] = [
+  ['totalFootage', 'M', '累计进尺', 'i-solar:ruler-bold text-sky', 2],
+  [
+    'utilizationRate',
+    '%',
+    '设备利用率',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  [
+    'totalPowerConsumption',
+    'MWh',
+    '累计用电量',
+    'i-material-symbols:electric-bolt-outline-rounded text-sky',
+    2
+  ],
+  [
+    'totalFuelConsumption',
+    '升',
+    '累计油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
+  [
+    'averageFuelConsumption',
+    '升',
+    '平均油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
+  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
+  [
+    'alreadyReported',
+    '个',
+    '已填报',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
+]
+
+const totalWork = ref({
+  totalCount: 0,
+  alreadyReported: 0,
+  notReported: 0,
+  totalFuelConsumption: 0,
+  totalPowerConsumption: 0,
+  totalFootage: 0,
+  averageFuelConsumption: 0,
+  utilizationRate: 0
+})
+
+const totalLoading = ref(false)
+
+const getTotal = useDebounceFn(async () => {
+  totalLoading.value = true
+
+  const { pageNo, pageSize, ...other } = props.query
+
+  try {
+    let res1: any[]
+    if (props.query.createTime && props.query.createTime.length === 2) {
+      res1 = await IotRyDailyReportApi.ryDailyReportStatistics({
+        createTime: props.query.createTime,
+        projectClassification: props.query.projectClassification
+      })
+
+      totalWork.value.totalCount = res1[0].count
+      totalWork.value.alreadyReported = res1[1].count
+      totalWork.value.notReported = res1[2].count
+    }
+
+    const res2 = await IotRyDailyReportApi.totalWorkload(other)
+
+    totalWork.value = {
+      ...totalWork.value,
+      totalFootage: 0,
+      ...res2,
+      totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
+      totalGasInjection: (res2.totalGasInjection || 0) / 10000,
+      totalFuelConsumption: res2.totalFuelConsumption || 0,
+      averageFuelConsumption: res2.averageFuelConsumption || 0,
+      utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
+    }
+  } finally {
+    totalLoading.value = false
+  }
+}, 1000)
+
+interface List {
+  id: number | null
+  name: string | null
+  type: '1' | '2' | '3'
+  cumulativeGasInjection: number | null
+  cumulativeWaterInjection: number | null
+  cumulativePowerConsumption: number | null
+  cumulativeFuelConsumption: number | null
+  transitTime: number | null
+  nonProductiveTime: number | null
+  averageFuelConsumption: number | null
+  utilizationRate: number | null
+}
+
+const list = ref<List[]>([])
+
+const type = ref('2')
+
+const columns = (type: string) => {
+  return [
+    {
+      label: type === '2' ? '项目部' : '队伍',
+      prop: 'name'
+    },
+    {
+      label: '累计进尺(M)',
+      prop: 'cumulativeFootage'
+    },
+    {
+      label: '累计用电量(MWh)',
+      prop: 'cumulativePowerConsumption'
+    },
+    {
+      label: '累计油耗(升)',
+      prop: 'cumulativeFuelConsumption'
+    },
+    {
+      label: '平均油耗(升)',
+      prop: 'averageFuelConsumption'
+    },
+    {
+      label: '平均时效(%)',
+      prop: 'transitTime'
+    },
+    {
+      label: '非生产时效(%)',
+      prop: 'nonProductiveTime'
+    },
+    {
+      label: '设备利用率(%)',
+      prop: 'utilizationRate',
+      action: true
+    }
+  ]
+}
+
+const listLoading = ref(false)
+
+const formatter = (row: List, column: any) => {
+  if (column.property === 'transitTime') {
+    return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
+  } else if (column.property === 'nonProductiveTime') {
+    return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
+  } else if (column.property === 'utilizationRate') {
+    return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
+  } else return row[column.property] ?? 0
+}
+
+const getList = useDebounceFn(async () => {
+  listLoading.value = true
+  try {
+    const res = await IotRyDailyReportApi.getIotRyDailyReportSummary(props.query)
+
+    const { list: reslist } = res
+
+    type.value = reslist[0]?.type || '2'
+
+    list.value = reslist.map(
+      ({ id, projectDeptId, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => ({
+        id: type === '2' ? projectDeptId : teamId,
+        name: type === '2' ? projectDeptName : teamName,
+        ...other,
+        cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
+        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
+        averageFuelConsumption: (other.averageFuelConsumption || 0).toFixed(2),
+        utilizationRate: other.utilizationRate || 0
+      })
+    )
+  } finally {
+    listLoading.value = false
+  }
+}, 1000)
+
+const tab = ref<'表格' | '看板'>('表格')
+
+const currentTab = 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
+let chartContainerEl: HTMLDivElement | null = null
+
+const xAxisData = ref<string[]>([])
+
+const legend = ref<string[][]>([
+  // ['累计油耗 (万升)', 'cumulativeFuelConsumption'],
+  ['油耗 (升)', 'cumulativeFuelConsumption'],
+  ['进尺 (M)', 'cumulativeFootage'],
+  ['用电量 (KWh)', 'cumulativePowerConsumption'],
+  // ['累计用电量 (MWh)', 'cumulativePowerConsumption'],
+  ['平均时效 (%)', 'transitTime'],
+  ['设备利用率 (%)', 'utilizationRate']
+])
+
+const chartData = ref<Record<string, number[]>>({
+  cumulativeFuelConsumption: [],
+  cumulativeFootage: [],
+  cumulativePowerConsumption: [],
+  transitTime: [],
+  utilizationRate: []
+})
+
+let chartLoading = ref(false)
+
+const getChart = useDebounceFn(async () => {
+  chartLoading.value = true
+
+  try {
+    // 创建查询参数,如果 createTime 为空则不传
+    const params: any = {
+      deptId: props.query.deptId,
+      projectClassification: props.query.projectClassification
+    }
+
+    // 只有 createTime 有值时才添加
+    if (props.query.createTime && props.query.createTime.length === 2) {
+      params.createTime = props.query.createTime
+    }
+
+    const res = await IotRyDailyReportApi.getIotRyDailyReportSummaryPolyline(props.query)
+
+    chartData.value = {
+      cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
+      // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
+      cumulativeFootage: res.map((item) => item.cumulativeFootage || 0),
+      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
+      // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
+      transitTime: res.map((item) => (item.transitTime || 0) * 100),
+      utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
+    }
+
+    xAxisData.value = res.map((item) => item.reportDate || '')
+  } finally {
+    chartLoading.value = false
+  }
+}, 1000)
+
+const resizer = () => {
+  chart?.resize()
+}
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizer)
+  chart?.dispose()
+  chart = null
+  chartContainerEl = null
+})
+
+const selectedLegends = ref<Record<string, boolean>>({})
+const intervalArr = ref<number[]>([])
+const maxInterval = ref(0)
+const minInterval = ref(0)
+
+const calcIntervals = () => {
+  let maxVal = -Infinity
+  let minVal = Infinity
+  let hasData = false
+
+  for (const [name, key] of legend.value) {
+    if (selectedLegends.value[name] !== false) {
+      const dataset = chartData.value[key] || []
+      if (dataset.length > 0) {
+        hasData = true
+        for (const val of dataset) {
+          if (val > maxVal) maxVal = val
+          if (val < minVal) minVal = val
+        }
+      }
+    }
+  }
+
+  if (!hasData) {
+    maxVal = 10000
+    minVal = 0
+  } else {
+    minVal = minVal > 0 ? 0 : minVal
+  }
+
+  const maxDigits = (Math.floor(maxVal) + '').length
+  const minDigits = minVal === 0 ? 0 : (Math.floor(Math.abs(minVal)) + '').length
+  const interval = Math.max(maxDigits, minDigits)
+
+  maxInterval.value = interval
+  minInterval.value = minDigits
+
+  const arr = [0]
+  for (let i = 1; i <= interval; i++) {
+    arr.push(Math.pow(10, i))
+  }
+  intervalArr.value = arr
+}
+
+const mapDataValue = (value: number) => {
+  if (value === 0) return 0
+
+  const isPositive = value > 0
+  const absItem = Math.abs(value)
+
+  if (!intervalArr.value.length) return value
+
+  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
+  const min_index = intervalArr.value.findIndex((v) => v === min_value)
+
+  const denominator =
+    min_index < intervalArr.value.length - 1
+      ? intervalArr.value[min_index + 1] - intervalArr.value[min_index]
+      : intervalArr.value[min_index] || 1
+
+  const new_value = (absItem - min_value) / denominator + min_index
+
+  return isPositive ? new_value : -new_value
+}
+
+const getSeries = () => {
+  return legend.value.map(([name, key]) => ({
+    name,
+    type: 'line',
+    smooth: true,
+    showSymbol: true,
+    data: chartData.value[key].map((value) => mapDataValue(value))
+  }))
+}
+
+const initChart = () => {
+  if (!chartRef.value) return
+
+  if (chart && chartContainerEl === chartRef.value) {
+    return
+  }
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+  chartContainerEl = chartRef.value
+  window.addEventListener('resize', resizer)
+
+  chart.on('legendselectchanged', (params: any) => {
+    selectedLegends.value = params.selected
+
+    calcIntervals()
+
+    chart?.setOption({
+      yAxis: {
+        min: -minInterval.value,
+        max: maxInterval.value
+      },
+      series: getSeries()
+    })
+  })
+}
+
+const render = () => {
+  if (!chartRef.value) return
+
+  initChart()
+
+  legend.value.forEach(([name]) => {
+    selectedLegends.value[name] = true
+  })
+
+  calcIntervals()
+
+  chart?.setOption(
+    {
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: { type: 'line' },
+        formatter: (params: any) => {
+          let d = `${params[0].axisValueLabel}<br>`
+          let item = params.map((el: any) => {
+            const realValue = chartData.value[legend.value[el.componentIndex][1]][el.dataIndex]
+            return `<div class="flex items-center justify-between mt-1 gap-1">
+            <span>${el.marker} ${el.seriesName}</span>
+            <span>${realValue.toFixed(2)} ${el.seriesName.split(' ')[1] || ''}</span>
+          </div>`
+          })
+          return d + item.join('')
+        }
+      },
+      legend: {
+        data: legend.value.map(([name]) => name),
+        selected: selectedLegends.value,
+        show: true
+      },
+      xAxis: {
+        type: 'category',
+        data: xAxisData.value
+      },
+      yAxis: {
+        type: 'value',
+        min: -minInterval.value,
+        max: maxInterval.value,
+        interval: 1,
+        axisLabel: {
+          formatter: (v: number) => {
+            const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
+            return num.toLocaleString()
+          }
+        }
+      },
+      series: getSeries()
+    },
+    true
+  )
+}
+
+const handleQuery = () => {
+  getChart().then(() => {
+    render()
+  })
+  getList()
+  getTotal()
+}
+
+watch(
+  () => [
+    props.refreshKey,
+    props.query.deptId,
+    props.query.contractName,
+    props.query.taskName,
+    props.query.createTime?.[0],
+    props.query.createTime?.[1],
+    props.query.projectClassification
+  ],
+  () => {
+    if (!props.query.createTime) {
+      totalWork.value.totalCount = 0
+      totalWork.value.notReported = 0
+      totalWork.value.alreadyReported = 0
+    }
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+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 IotRyDailyReportApi.exportRyDailyReportStatistics(props.query)
+
+  download.excel(res, '瑞鹰钻井日报统计数据.xlsx')
+}
+
+const exportAll = async () => {
+  if (tab.value === '看板') exportChart()
+  else exportData()
+}
+
+const message = useMessage()
+
+const unfilledDialogRef = ref()
+
+const openUnfilledDialog = () => {
+  // 检查是否选择了创建时间
+  if (!props.query.createTime || props.query.createTime.length === 0) {
+    message.warning('请先选择创建时间范围')
+    return
+  }
+
+  // 打开弹窗
+  unfilledDialogRef.value?.open()
+}
+
+const router = useRouter()
+
+const tolist = (id: number, non: boolean = false) => {
+  const { pageNo, pageSize, ...rest } = props.query
+
+  router.push({
+    path: '/iotdayilyreport/IotRyDailyReport',
+    query: {
+      ...rest,
+      deptId: id,
+      ...(non ? { nonProductFlag: 'Y' } : {})
+    }
+  })
+}
+</script>
+
+<template>
+  <div class="grid grid-rows-[128px_1fr] gap-4 h-full min-h-0">
+    <div class="grid grid-cols-8 gap-8">
+      <div
+        v-for="info in totalWorkKeys"
+        :key="info[0]"
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-4 flex flex-col items-center justify-center gap-2"
+      >
+        <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]"
+          @click="info[2] === '未填报' ? openUnfilledDialog() : ''"
+        >
+          <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
+        </count-to>
+        <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
+          {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
+        </div>
+      </div>
+    </div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2 min-h-0">
+      <div class="flex h-12 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">{{ `${props.deptName}-${tab}` }}</h3>
+        <el-button size="default" type="primary" @click="exportAll">导出</el-button>
+      </div>
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ height }">
+            <Motion
+              as="div"
+              :style="{ position: 'relative', overflow: 'hidden' }"
+              :animate="{ height: `${height}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: `${height}px` }">
+                    <zm-table
+                      v-if="currentTab === '表格'"
+                      :loading="listLoading"
+                      :data="list"
+                      :height="height"
+                      show-border
+                    >
+                      <template v-for="item in columns(type)" :key="item.prop">
+                        <zm-table-column
+                          v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
+                          :label="item.label"
+                          :prop="item.prop"
+                          :formatter="formatter"
+                          :action="item.action"
+                        />
+                        <zm-table-column
+                          v-else-if="item.prop === 'name'"
+                          :label="item.label"
+                          :prop="item.prop"
+                        >
+                          <template #default="{ row }">
+                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                              row.name
+                            }}</el-button>
+                          </template>
+                        </zm-table-column>
+                        <zm-table-column v-else :label="item.label" :prop="item.prop">
+                          <template #default="{ row }">
+                            <el-button
+                              v-if="row.nonProductiveTime > 0"
+                              text
+                              type="primary"
+                              @click.prevent="tolist(row.id, true)"
+                            >
+                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
+                            </el-button>
+                            <span v-else>
+                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
+                            </span>
+                          </template>
+                        </zm-table-column>
+                      </template>
+                    </zm-table>
+                    <div
+                      ref="chartRef"
+                      v-loading="chartLoading"
+                      :key="dayjs().valueOf()"
+                      v-else
+                      :style="{ width: `100%`, height: `${height}px` }"
+                    >
+                    </div>
+                  </div>
+                </Motion>
+              </AnimatePresence>
+            </Motion>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </div>
+  </div>
+
+  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="props.query" />
+</template>

+ 384 - 0
src/views/pms/iotrydailyreport/components/NonProductionEfficiency.vue

@@ -0,0 +1,384 @@
+<script setup lang="ts">
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import { useDebounceFn } from '@vueuse/core'
+import * as echarts from 'echarts'
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+  projectClassification: 1
+}
+
+const props = defineProps<{
+  query: Query
+  deptName: string
+  refreshKey: number
+}>()
+
+interface ListItem {
+  id?: number | null
+  name?: string | null
+  type?: string
+  teamId?: number
+  projectDeptId?: number
+  teamName?: string
+  projectDeptName?: string
+  accidentTime: number
+  repairTime: number
+  selfStopTime: number
+  complexityTime: number
+  relocationTime: number
+  rectificationTime: number
+  waitingStopTime: number
+  winterBreakTime: number
+  partyaDesign: number
+  partyaPrepare: number
+  partyaResource: number
+  otherNptTime: number
+  nptTotal: number
+  nptRate: number
+  calendarTime: number
+}
+
+const list = ref<ListItem[]>([])
+const loading = ref(false)
+const tab = ref<'表格' | '看板'>('表格')
+const chartRef = ref<HTMLDivElement | null>(null)
+let chart: echarts.ECharts | null = null
+const router = useRouter()
+
+const nonProductionTimeFields: [keyof ListItem, string][] = [
+  ['accidentTime', '工程质量'],
+  ['repairTime', '设备故障'],
+  ['selfStopTime', '设备保养'],
+  ['complexityTime', '技术受限'],
+  ['relocationTime', '生产配合'],
+  ['rectificationTime', '生产组织'],
+  ['waitingStopTime', '不可抗力'],
+  ['winterBreakTime', '待命'],
+  ['partyaDesign', '甲方设计'],
+  ['partyaPrepare', '甲方准备'],
+  ['partyaResource', '甲方资源'],
+  ['otherNptTime', '其它']
+]
+
+const getQueryWithoutPage = () => {
+  const { pageNo: _pageNo, pageSize: _pageSize, ...query } = props.query
+
+  void _pageNo
+  void _pageSize
+
+  return {
+    ...query,
+    projectClassification: 1
+  }
+}
+
+const normalizeListData = (data: any) => {
+  const rows = Array.isArray(data) ? data : data?.list || data?.records || []
+
+  return rows.map(({ projectDeptId, projectDeptName, teamId, teamName, type, ...other }) => ({
+    ...other,
+    type,
+    projectDeptId,
+    projectDeptName,
+    teamId,
+    teamName,
+    id: type === '2' ? projectDeptId : teamId,
+    name: type === '2' ? projectDeptName : teamName
+  }))
+}
+
+const loadList = useDebounceFn(async function () {
+  loading.value = true
+  try {
+    const data = await IotRyDailyReportApi.nptStatistics(getQueryWithoutPage())
+
+    list.value = normalizeListData(data)
+    if (tab.value === '看板') {
+      nextTick(renderChart)
+    }
+  } finally {
+    loading.value = false
+  }
+}, 500)
+
+function handleQuery() {
+  loadList()
+}
+
+watch(
+  () => [
+    props.refreshKey,
+    props.query.deptId,
+    props.query.contractName,
+    props.query.taskName,
+    props.query.createTime?.[0],
+    props.query.createTime?.[1],
+    props.query.projectClassification
+  ],
+  () => {
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+const handleSelectTab = (val: '表格' | '看板') => {
+  tab.value = val
+
+  if (val === '看板') {
+    nextTick(renderChart)
+  } else {
+    chart?.dispose()
+    chart = null
+  }
+}
+
+const formatNumber = (value: unknown) => {
+  const num = Number(value || 0)
+  return Number.isInteger(num) ? `${num}` : num.toFixed(2)
+}
+
+const getFieldTotal = (field: keyof ListItem) => {
+  return list.value.reduce((total, row) => total + Number(row[field] || 0), 0)
+}
+
+const getSummaryRate = () => {
+  const nptTotal = getFieldTotal('nptTotal')
+  const calendarTime = getFieldTotal('calendarTime')
+
+  if (!calendarTime) return '0.00%'
+
+  return `${((nptTotal / calendarTime) * 100).toFixed(2)}%`
+}
+
+const getSummaries = ({ columns }: { columns: Array<{ property?: keyof ListItem | string }> }) => {
+  const nonSummaryFields = ['id', 'name', 'teamName', 'projectDeptName']
+
+  return columns.map((column, index) => {
+    if (index === 0) return '合计'
+
+    const property = column.property as keyof ListItem | undefined
+    if (!property) return ''
+
+    if (property === 'nptRate') return getSummaryRate()
+    if (nonSummaryFields.includes(property)) return ''
+
+    return formatNumber(getFieldTotal(property))
+  })
+}
+
+const formatTeamName = (row: ListItem) => {
+  return row.name || '-'
+}
+
+const formatRate = (row: ListItem) => {
+  return `${(Number(row.nptRate || 0) * 100).toFixed(2)}%`
+}
+
+const handleRowClick = (row: ListItem) => {
+  if (!row.id) return
+
+  router.push({
+    name: 'IotRyDailyReport',
+    query: {
+      deptId: row.id,
+      createTime: props.query.createTime,
+      projectClassification: 1
+    }
+  })
+}
+
+const getClickableRowClassName = () => 'summary-clickable-row'
+
+const getPieData = () => {
+  return list.value
+    .map((row) => ({
+      name: formatTeamName(row),
+      value: Number(row.nptTotal || 0)
+    }))
+    .filter((item) => item.value > 0)
+}
+
+const resizeChart = () => {
+  chart?.resize()
+}
+
+const renderChart = () => {
+  if (!chartRef.value) return
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+
+  window.removeEventListener('resize', resizeChart)
+  window.addEventListener('resize', resizeChart)
+
+  const pieData = getPieData()
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}<br/>NPT合计: {c} H<br/>占比: {d}%'
+    },
+    legend: {
+      type: 'scroll',
+      orient: 'vertical',
+      right: 24,
+      top: 32,
+      bottom: 24
+    },
+    graphic:
+      pieData.length === 0
+        ? {
+            type: 'text',
+            left: 'center',
+            top: 'middle',
+            style: {
+              text: '暂无NPT数据',
+              fill: '#909399',
+              fontSize: 14
+            }
+          }
+        : undefined,
+    series: [
+      {
+        name: 'NPT合计',
+        type: 'pie',
+        radius: ['42%', '68%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: true,
+        itemStyle: {
+          borderRadius: 4,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: {
+          formatter: '{b}: {d}%'
+        },
+        data: pieData
+      }
+    ]
+  })
+}
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  chart?.dispose()
+})
+
+const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
+</script>
+
+<template>
+  <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2 h-full min-h-0">
+    <div class="flex h-12 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> -->
+      <div class="w-80px"></div>
+    </div>
+
+    <div class="flex-1 relative min-h-0">
+      <el-auto-resizer class="absolute">
+        <template #default="{ width, height }">
+          <zm-table
+            v-if="tab === '表格'"
+            :data="list"
+            :loading="loading"
+            :width="width"
+            :max-height="height"
+            :height="height"
+            show-border
+            show-summary
+            :summary-method="getSummaries"
+            :row-class-name="getClickableRowClassName"
+            @row-click="handleRowClick"
+          >
+            <zm-table-column
+              prop="name"
+              label="队伍"
+              min-width="120"
+              fixed="left"
+              cover-formatter
+              :real-value="formatTeamName"
+            />
+            <zm-table-column
+              v-for="[prop, label] in nonProductionTimeFields"
+              :key="prop"
+              :prop="prop"
+              :label="label"
+              min-width="92"
+              cover-formatter
+              :real-value="(row: ListItem) => formatNumber(row[prop])"
+            />
+            <zm-table-column label="npt合计" is-parent>
+              <zm-table-column
+                prop="nptTotal"
+                label="时长(H)"
+                min-width="92"
+                cover-formatter
+                :real-value="(row: ListItem) => formatNumber(row.nptTotal)"
+              />
+              <zm-table-column
+                prop="nptRate"
+                label="占比"
+                min-width="92"
+                cover-formatter
+                :real-value="formatRate"
+              />
+            </zm-table-column>
+            <zm-table-column
+              prop="calendarTime"
+              label="自然时间"
+              min-width="92"
+              cover-formatter
+              action
+              :real-value="(row: ListItem) => formatNumber(row.calendarTime)"
+            />
+          </zm-table>
+          <div
+            v-else
+            ref="chartRef"
+            v-loading="loading"
+            class="npt-board-container"
+            :style="{ width: `${width}px`, height: `${height}px` }"
+          >
+          </div>
+        </template>
+      </el-auto-resizer>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.npt-cell-highlight) {
+  background-color: #fff566 !important;
+}
+
+:deep(.summary-clickable-row) {
+  cursor: pointer;
+}
+
+.npt-board-container {
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 4px;
+}
+</style>

+ 676 - 0
src/views/pms/iotrydailyreport/components/XjDailyStatistics.vue

@@ -0,0 +1,676 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+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 download from '@/utils/download'
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+  projectClassification: 1 | 2
+}
+
+const props = defineProps<{
+  query: Query
+  deptName: string
+  refreshKey: number
+}>()
+
+const totalWorkKeys: [string, string, string, string, number][] = [
+  ['constructionWells', '个', '累计施工井数', 'i-mdi:progress-wrench text-sky', 0],
+  ['completedWells', '个', '累计完工井数', 'i-mdi:wrench-check-outline text-emerald', 0],
+  [
+    'utilizationRate',
+    '%',
+    '设备利用率',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  [
+    'totalPowerConsumption',
+    'MWh',
+    '累计用电量',
+    'i-material-symbols:electric-bolt-outline-rounded text-sky',
+    2
+  ],
+  [
+    'totalFuelConsumption',
+    '升',
+    '累计油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
+  [
+    'averageFuelConsumption',
+    '升',
+    '平均油耗',
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
+  ],
+  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
+  [
+    'alreadyReported',
+    '个',
+    '已填报',
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
+  ],
+  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
+]
+
+const totalWork = ref({
+  totalCount: 0,
+  alreadyReported: 0,
+  notReported: 0,
+  totalFuelConsumption: 0,
+  totalPowerConsumption: 0,
+  constructionWells: 0,
+  completedWells: 0,
+  averageFuelConsumption: 0,
+  utilizationRate: 0
+})
+
+const totalLoading = ref(false)
+
+const getTotal = useDebounceFn(async () => {
+  totalLoading.value = true
+
+  const { pageNo, pageSize, ...other } = props.query
+
+  try {
+    let res1: any[]
+    if (props.query.createTime && props.query.createTime.length === 2) {
+      res1 = await IotRyDailyReportApi.ryDailyReportStatistics({
+        createTime: props.query.createTime,
+        projectClassification: props.query.projectClassification
+      })
+
+      totalWork.value.totalCount = res1[0].count
+      totalWork.value.alreadyReported = res1[1].count
+      totalWork.value.notReported = res1[2].count
+    }
+
+    const res2 = await IotRyDailyReportApi.totalWorkload(other)
+
+    totalWork.value = {
+      ...totalWork.value,
+      constructionWells: 0,
+      completedWells: 0,
+      ...res2,
+      totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
+      totalFuelConsumption: res2.totalFuelConsumption || 0,
+      averageFuelConsumption: res2.averageFuelConsumption || 0,
+      utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
+    }
+  } finally {
+    totalLoading.value = false
+  }
+}, 1000)
+
+interface List {
+  id: number | null
+  name: string | null
+  type: '1' | '2' | '3'
+  cumulativeGasInjection: number | null
+  cumulativeWaterInjection: number | null
+  cumulativePowerConsumption: number | null
+  cumulativeFuelConsumption: number | null
+  transitTime: number | null
+  nonProductiveTime: number | null
+  averageFuelConsumption: number | null
+  utilizationRate: number | null
+}
+
+const list = ref<List[]>([])
+
+const type = ref('2')
+
+const columns = (type: string) => {
+  return [
+    {
+      label: type === '2' ? '项目部' : '队伍',
+      prop: 'name'
+    },
+    {
+      label: '累计施工井数',
+      prop: 'cumulativeConstructWells'
+    },
+    {
+      label: '累计完工井数',
+      prop: 'cumulativeCompletedWells'
+    },
+    {
+      label: '累计用电量(MWh)',
+      prop: 'cumulativePowerConsumption'
+    },
+    {
+      label: '累计油耗(升)',
+      prop: 'cumulativeFuelConsumption'
+    },
+    {
+      label: '平均油耗(升)',
+      prop: 'averageFuelConsumption'
+    },
+    {
+      label: '平均时效(%)',
+      prop: 'transitTime'
+    },
+    {
+      label: '非生产时效(%)',
+      prop: 'nonProductiveTime'
+    },
+    {
+      label: '设备利用率(%)',
+      prop: 'utilizationRate',
+      action: true
+    }
+  ]
+}
+
+const listLoading = ref(false)
+
+const formatter = (row: List, column: any) => {
+  if (column.property === 'transitTime') {
+    return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
+  } else if (column.property === 'nonProductiveTime') {
+    return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
+  } else if (column.property === 'utilizationRate') {
+    return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
+  } else return row[column.property] ?? 0
+}
+
+const getList = useDebounceFn(async () => {
+  listLoading.value = true
+  try {
+    const res = await IotRyDailyReportApi.getIotRyDailyReportSummary(props.query)
+
+    const { list: reslist } = res
+
+    type.value = reslist[0]?.type || '2'
+
+    list.value = reslist.map(
+      ({ id, projectDeptId, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => ({
+        id: type === '2' ? projectDeptId : teamId,
+        name: type === '2' ? projectDeptName : teamName,
+        ...other,
+        cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
+        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
+        averageFuelConsumption: (other.averageFuelConsumption || 0).toFixed(2),
+        utilizationRate: other.utilizationRate || 0
+      })
+    )
+  } finally {
+    listLoading.value = false
+  }
+}, 1000)
+
+const tab = ref<'表格' | '看板'>('表格')
+
+const currentTab = 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
+let chartContainerEl: HTMLDivElement | null = null
+
+const xAxisData = ref<string[]>([])
+
+const legend = ref<string[][]>([
+  ['施工井数 (个)', 'cumulativeConstructWells'],
+  ['完工井数 (个)', 'cumulativeCompletedWells'],
+  // ['累计油耗 (万升)', 'cumulativeFuelConsumption'],
+  ['油耗 (升)', 'cumulativeFuelConsumption'],
+  // ['累计用电量 (MWh)', 'cumulativePowerConsumption'],
+  ['用电量 (KWh)', 'cumulativePowerConsumption'],
+  ['平均时效 (%)', 'transitTime'],
+  ['设备利用率 (%)', 'utilizationRate']
+])
+
+const chartData = ref<Record<string, number[]>>({
+  cumulativeFuelConsumption: [],
+  cumulativeConstructWells: [],
+  cumulativeCompletedWells: [],
+  cumulativePowerConsumption: [],
+  transitTime: [],
+  utilizationRate: []
+})
+
+let chartLoading = ref(false)
+
+const getChart = useDebounceFn(async () => {
+  chartLoading.value = true
+
+  try {
+    const res = await IotRyDailyReportApi.getIotRyDailyReportSummaryPolyline(props.query)
+
+    chartData.value = {
+      cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
+      // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
+      cumulativeConstructWells: res.map((item) => item.cumulativeConstructWells || 0),
+      cumulativeCompletedWells: res.map((item) => item.cumulativeCompletedWells || 0),
+      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
+      // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
+      transitTime: res.map((item) => (item.transitTime || 0) * 100),
+      utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
+    }
+
+    xAxisData.value = res.map((item) => item.reportDate || '')
+  } finally {
+    chartLoading.value = false
+  }
+}, 1000)
+
+const resizer = () => {
+  chart?.resize()
+}
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizer)
+  chart?.dispose()
+  chart = null
+  chartContainerEl = null
+})
+
+const selectedLegends = ref<Record<string, boolean>>({})
+const intervalArr = ref<number[]>([])
+const maxInterval = ref(0)
+const minInterval = ref(0)
+
+const calcIntervals = () => {
+  let maxVal = -Infinity
+  let minVal = Infinity
+  let hasData = false
+
+  for (const [name, key] of legend.value) {
+    if (selectedLegends.value[name] !== false) {
+      const dataset = chartData.value[key] || []
+      if (dataset.length > 0) {
+        hasData = true
+        for (const val of dataset) {
+          if (val > maxVal) maxVal = val
+          if (val < minVal) minVal = val
+        }
+      }
+    }
+  }
+
+  if (!hasData) {
+    maxVal = 10000
+    minVal = 0
+  } else {
+    minVal = minVal > 0 ? 0 : minVal
+  }
+
+  const maxDigits = (Math.floor(maxVal) + '').length
+  const minDigits = minVal === 0 ? 0 : (Math.floor(Math.abs(minVal)) + '').length
+  const interval = Math.max(maxDigits, minDigits)
+
+  maxInterval.value = interval
+  minInterval.value = minDigits
+
+  const arr = [0]
+  for (let i = 1; i <= interval; i++) {
+    arr.push(Math.pow(10, i))
+  }
+  intervalArr.value = arr
+}
+
+const mapDataValue = (value: number) => {
+  if (value === 0) return 0
+
+  const isPositive = value > 0
+  const absItem = Math.abs(value)
+
+  if (!intervalArr.value.length) return value
+
+  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
+  const min_index = intervalArr.value.findIndex((v) => v === min_value)
+
+  const denominator =
+    min_index < intervalArr.value.length - 1
+      ? intervalArr.value[min_index + 1] - intervalArr.value[min_index]
+      : intervalArr.value[min_index] || 1
+
+  const new_value = (absItem - min_value) / denominator + min_index
+
+  return isPositive ? new_value : -new_value
+}
+
+const getSeries = () => {
+  return legend.value.map(([name, key]) => ({
+    name,
+    type: 'line',
+    smooth: true,
+    showSymbol: true,
+    data: chartData.value[key].map((value) => mapDataValue(value))
+  }))
+}
+
+const initChart = () => {
+  if (!chartRef.value) return
+
+  if (chart && chartContainerEl === chartRef.value) {
+    return
+  }
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+  chartContainerEl = chartRef.value
+  window.addEventListener('resize', resizer)
+
+  chart.on('legendselectchanged', (params: any) => {
+    selectedLegends.value = params.selected
+
+    calcIntervals()
+
+    chart?.setOption({
+      yAxis: {
+        min: -minInterval.value,
+        max: maxInterval.value
+      },
+      series: getSeries()
+    })
+  })
+}
+
+const render = () => {
+  if (!chartRef.value) return
+
+  initChart()
+
+  legend.value.forEach(([name]) => {
+    selectedLegends.value[name] = true
+  })
+
+  calcIntervals()
+
+  chart?.setOption(
+    {
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: { type: 'line' },
+        formatter: (params: any) => {
+          let d = `${params[0].axisValueLabel}<br>`
+          let item = params.map((el: any) => {
+            const realValue = chartData.value[legend.value[el.componentIndex][1]][el.dataIndex]
+            return `<div class="flex items-center justify-between mt-1 gap-1">
+            <span>${el.marker} ${el.seriesName}</span>
+            <span>${realValue.toFixed(2)} ${el.seriesName.split(' ')[1] || ''}</span>
+          </div>`
+          })
+          return d + item.join('')
+        }
+      },
+      legend: {
+        data: legend.value.map(([name]) => name),
+        selected: selectedLegends.value,
+        show: true
+      },
+      xAxis: {
+        type: 'category',
+        data: xAxisData.value
+      },
+      yAxis: {
+        type: 'value',
+        min: -minInterval.value,
+        max: maxInterval.value,
+        interval: 1,
+        axisLabel: {
+          formatter: (v: number) => {
+            const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
+            return num.toLocaleString()
+          }
+        }
+      },
+      series: getSeries()
+    },
+    true
+  )
+}
+
+const handleQuery = () => {
+  getChart().then(() => {
+    render()
+  })
+  getList()
+  getTotal()
+}
+
+watch(
+  () => [
+    props.refreshKey,
+    props.query.deptId,
+    props.query.contractName,
+    props.query.taskName,
+    props.query.createTime?.[0],
+    props.query.createTime?.[1],
+    props.query.projectClassification
+  ],
+  () => {
+    if (!props.query.createTime) {
+      totalWork.value.totalCount = 0
+      totalWork.value.notReported = 0
+      totalWork.value.alreadyReported = 0
+    }
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+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 IotRyDailyReportApi.exportRyDailyReportStatistics(props.query)
+
+  download.excel(res, '瑞鹰修井日报统计数据.xlsx')
+}
+
+const exportAll = async () => {
+  if (tab.value === '看板') exportChart()
+  else exportData()
+}
+
+const message = useMessage()
+
+const unfilledDialogRef = ref()
+
+const openUnfilledDialog = () => {
+  // 检查是否选择了创建时间
+  if (!props.query.createTime || props.query.createTime.length === 0) {
+    message.warning('请先选择创建时间范围')
+    return
+  }
+
+  // 打开弹窗
+  unfilledDialogRef.value?.open()
+}
+
+const router = useRouter()
+
+const tolist = (id: number) => {
+  const { pageNo, pageSize, ...rest } = props.query
+
+  router.push({
+    name: 'IotRyXjDailyReport',
+    query: {
+      ...rest,
+      deptId: id,
+      projectClassification: 2
+    }
+  })
+}
+</script>
+
+<template>
+  <div class="grid grid-rows-[128px_1fr] gap-4 h-full min-h-0">
+    <div class="grid grid-cols-9 gap-8">
+      <div
+        v-for="info in totalWorkKeys"
+        :key="info[0]"
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-4 flex flex-col items-center justify-center gap-2"
+      >
+        <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]"
+          @click="info[2] === '未填报' ? openUnfilledDialog() : ''"
+        >
+          <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
+        </count-to>
+        <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
+          {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
+        </div>
+      </div>
+    </div>
+
+    <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2 min-h-0">
+      <div class="flex h-12 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">{{ `${props.deptName}-${tab}` }}</h3>
+        <el-button size="default" type="primary" @click="exportAll">导出</el-button>
+      </div>
+      <div class="flex-1 relative min-h-0">
+        <el-auto-resizer class="absolute">
+          <template #default="{ height }">
+            <Motion
+              as="div"
+              :style="{ position: 'relative', overflow: 'hidden' }"
+              :animate="{ height: `${height}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: `${height}px` }">
+                    <zm-table
+                      v-if="currentTab === '表格'"
+                      :loading="listLoading"
+                      :data="list"
+                      :height="height"
+                      show-border
+                    >
+                      <template v-for="item in columns(type)" :key="item.prop">
+                        <zm-table-column
+                          v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
+                          :label="item.label"
+                          :prop="item.prop"
+                          :formatter="formatter"
+                          :action="item.action"
+                        />
+                        <zm-table-column
+                          v-else-if="item.prop === 'name'"
+                          :label="item.label"
+                          :prop="item.prop"
+                        >
+                          <template #default="{ row }">
+                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                              row.name
+                            }}</el-button>
+                          </template>
+                        </zm-table-column>
+                        <zm-table-column v-else :label="item.label" :prop="item.prop">
+                          <template #default="{ row }">
+                            <el-button
+                              v-if="row.nonProductiveTime > 0"
+                              text
+                              type="primary"
+                              @click.prevent="tolist(row.id)"
+                            >
+                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
+                            </el-button>
+                            <span v-else>
+                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
+                            </span>
+                          </template>
+                        </zm-table-column>
+                      </template>
+                    </zm-table>
+                    <div
+                      ref="chartRef"
+                      v-loading="chartLoading"
+                      :key="dayjs().valueOf()"
+                      v-else
+                      :style="{ width: `100%`, height: `${height}px` }"
+                    >
+                    </div>
+                  </div>
+                </Motion>
+              </AnimatePresence>
+            </Motion>
+          </template>
+        </el-auto-resizer>
+      </div>
+    </div>
+  </div>
+
+  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="props.query" />
+</template>

+ 384 - 0
src/views/pms/iotrydailyreport/components/XjNonProductionEfficiency.vue

@@ -0,0 +1,384 @@
+<script setup lang="ts">
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+import { useTableComponents } from '@/components/ZmTable/useTableComponents'
+import { useDebounceFn } from '@vueuse/core'
+import * as echarts from 'echarts'
+
+interface Query {
+  pageNo: number
+  pageSize: number
+  deptId: number
+  contractName?: string
+  taskName?: string
+  createTime: string[]
+  projectClassification: 2
+}
+
+const props = defineProps<{
+  query: Query
+  deptName: string
+  refreshKey: number
+}>()
+
+interface ListItem {
+  id?: number | null
+  name?: string | null
+  type?: string
+  teamId?: number
+  projectDeptId?: number
+  teamName?: string
+  projectDeptName?: string
+  accidentTime: number
+  repairTime: number
+  selfStopTime: number
+  complexityTime: number
+  relocationTime: number
+  rectificationTime: number
+  waitingStopTime: number
+  winterBreakTime: number
+  partyaDesign: number
+  partyaPrepare: number
+  partyaResource: number
+  otherNptTime: number
+  nptTotal: number
+  nptRate: number
+  calendarTime: number
+}
+
+const list = ref<ListItem[]>([])
+const loading = ref(false)
+const tab = ref<'表格' | '看板'>('表格')
+const chartRef = ref<HTMLDivElement | null>(null)
+let chart: echarts.ECharts | null = null
+const router = useRouter()
+
+const nonProductionTimeFields: [keyof ListItem, string][] = [
+  ['accidentTime', '工程质量'],
+  ['repairTime', '设备故障'],
+  ['selfStopTime', '设备保养'],
+  ['complexityTime', '技术受限'],
+  ['relocationTime', '生产配合'],
+  ['rectificationTime', '生产组织'],
+  ['waitingStopTime', '不可抗力'],
+  ['winterBreakTime', '待命'],
+  ['partyaDesign', '甲方设计'],
+  ['partyaPrepare', '甲方准备'],
+  ['partyaResource', '甲方资源'],
+  ['otherNptTime', '其它']
+]
+
+const getQueryWithoutPage = () => {
+  const { pageNo: _pageNo, pageSize: _pageSize, ...query } = props.query
+
+  void _pageNo
+  void _pageSize
+
+  return {
+    ...query,
+    projectClassification: 2
+  }
+}
+
+const normalizeListData = (data: any) => {
+  const rows = Array.isArray(data) ? data : data?.list || data?.records || []
+
+  return rows.map(({ projectDeptId, projectDeptName, teamId, teamName, type, ...other }) => ({
+    ...other,
+    type,
+    projectDeptId,
+    projectDeptName,
+    teamId,
+    teamName,
+    id: type === '2' ? projectDeptId : teamId,
+    name: type === '2' ? projectDeptName : teamName
+  }))
+}
+
+const loadList = useDebounceFn(async function () {
+  loading.value = true
+  try {
+    const data = await IotRyDailyReportApi.nptStatistics(getQueryWithoutPage())
+
+    list.value = normalizeListData(data)
+    if (tab.value === '看板') {
+      nextTick(renderChart)
+    }
+  } finally {
+    loading.value = false
+  }
+}, 500)
+
+function handleQuery() {
+  loadList()
+}
+
+watch(
+  () => [
+    props.refreshKey,
+    props.query.deptId,
+    props.query.contractName,
+    props.query.taskName,
+    props.query.createTime?.[0],
+    props.query.createTime?.[1],
+    props.query.projectClassification
+  ],
+  () => {
+    handleQuery()
+  },
+  { immediate: true }
+)
+
+const handleSelectTab = (val: '表格' | '看板') => {
+  tab.value = val
+
+  if (val === '看板') {
+    nextTick(renderChart)
+  } else {
+    chart?.dispose()
+    chart = null
+  }
+}
+
+const formatNumber = (value: unknown) => {
+  const num = Number(value || 0)
+  return Number.isInteger(num) ? `${num}` : num.toFixed(2)
+}
+
+const getFieldTotal = (field: keyof ListItem) => {
+  return list.value.reduce((total, row) => total + Number(row[field] || 0), 0)
+}
+
+const getSummaryRate = () => {
+  const nptTotal = getFieldTotal('nptTotal')
+  const calendarTime = getFieldTotal('calendarTime')
+
+  if (!calendarTime) return '0.00%'
+
+  return `${((nptTotal / calendarTime) * 100).toFixed(2)}%`
+}
+
+const getSummaries = ({ columns }: { columns: Array<{ property?: keyof ListItem | string }> }) => {
+  const nonSummaryFields = ['id', 'name', 'teamName', 'projectDeptName']
+
+  return columns.map((column, index) => {
+    if (index === 0) return '合计'
+
+    const property = column.property as keyof ListItem | undefined
+    if (!property) return ''
+
+    if (property === 'nptRate') return getSummaryRate()
+    if (nonSummaryFields.includes(property)) return ''
+
+    return formatNumber(getFieldTotal(property))
+  })
+}
+
+const formatTeamName = (row: ListItem) => {
+  return row.name || '-'
+}
+
+const formatRate = (row: ListItem) => {
+  return `${(Number(row.nptRate || 0) * 100).toFixed(2)}%`
+}
+
+const handleRowClick = (row: ListItem) => {
+  if (!row.id) return
+
+  router.push({
+    name: 'IotRyXjDailyReport',
+    query: {
+      deptId: row.id,
+      createTime: props.query.createTime,
+      projectClassification: 2
+    }
+  })
+}
+
+const getClickableRowClassName = () => 'summary-clickable-row'
+
+const getPieData = () => {
+  return list.value
+    .map((row) => ({
+      name: formatTeamName(row),
+      value: Number(row.nptTotal || 0)
+    }))
+    .filter((item) => item.value > 0)
+}
+
+const resizeChart = () => {
+  chart?.resize()
+}
+
+const renderChart = () => {
+  if (!chartRef.value) return
+
+  chart?.dispose()
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+
+  window.removeEventListener('resize', resizeChart)
+  window.addEventListener('resize', resizeChart)
+
+  const pieData = getPieData()
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}<br/>NPT合计: {c} H<br/>占比: {d}%'
+    },
+    legend: {
+      type: 'scroll',
+      orient: 'vertical',
+      right: 24,
+      top: 32,
+      bottom: 24
+    },
+    graphic:
+      pieData.length === 0
+        ? {
+            type: 'text',
+            left: 'center',
+            top: 'middle',
+            style: {
+              text: '暂无NPT数据',
+              fill: '#909399',
+              fontSize: 14
+            }
+          }
+        : undefined,
+    series: [
+      {
+        name: 'NPT合计',
+        type: 'pie',
+        radius: ['42%', '68%'],
+        center: ['50%', '50%'],
+        avoidLabelOverlap: true,
+        itemStyle: {
+          borderRadius: 4,
+          borderColor: '#fff',
+          borderWidth: 2
+        },
+        label: {
+          formatter: '{b}: {d}%'
+        },
+        data: pieData
+      }
+    ]
+  })
+}
+
+onUnmounted(() => {
+  window.removeEventListener('resize', resizeChart)
+  chart?.dispose()
+})
+
+const { ZmTable, ZmTableColumn } = useTableComponents<ListItem>()
+</script>
+
+<template>
+  <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2 h-full min-h-0">
+    <div class="flex h-12 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> -->
+      <div class="w-80px"></div>
+    </div>
+
+    <div class="flex-1 relative min-h-0">
+      <el-auto-resizer class="absolute">
+        <template #default="{ width, height }">
+          <zm-table
+            v-if="tab === '表格'"
+            :data="list"
+            :loading="loading"
+            :width="width"
+            :max-height="height"
+            :height="height"
+            show-border
+            show-summary
+            :summary-method="getSummaries"
+            :row-class-name="getClickableRowClassName"
+            @row-click="handleRowClick"
+          >
+            <zm-table-column
+              prop="name"
+              label="队伍"
+              min-width="120"
+              fixed="left"
+              cover-formatter
+              :real-value="formatTeamName"
+            />
+            <zm-table-column
+              v-for="[prop, label] in nonProductionTimeFields"
+              :key="prop"
+              :prop="prop"
+              :label="label"
+              min-width="92"
+              cover-formatter
+              :real-value="(row: ListItem) => formatNumber(row[prop])"
+            />
+            <zm-table-column label="npt合计" is-parent>
+              <zm-table-column
+                prop="nptTotal"
+                label="时长(H)"
+                min-width="92"
+                cover-formatter
+                :real-value="(row: ListItem) => formatNumber(row.nptTotal)"
+              />
+              <zm-table-column
+                prop="nptRate"
+                label="占比"
+                min-width="92"
+                cover-formatter
+                :real-value="formatRate"
+              />
+            </zm-table-column>
+            <zm-table-column
+              prop="calendarTime"
+              label="自然时间"
+              min-width="92"
+              cover-formatter
+              action
+              :real-value="(row: ListItem) => formatNumber(row.calendarTime)"
+            />
+          </zm-table>
+          <div
+            v-else
+            ref="chartRef"
+            v-loading="loading"
+            class="npt-board-container"
+            :style="{ width: `${width}px`, height: `${height}px` }"
+          >
+          </div>
+        </template>
+      </el-auto-resizer>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+:deep(.npt-cell-highlight) {
+  background-color: #fff566 !important;
+}
+
+:deep(.summary-clickable-row) {
+  cursor: pointer;
+}
+
+.npt-board-container {
+  background: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 4px;
+}
+</style>

+ 27 - 0
src/views/pms/iotrydailyreport/index.vue

@@ -44,6 +44,26 @@ const initQuery: Query = {
 
 const query = ref<Query>({ ...initQuery })
 
+const getRouteCreateTime = () => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    return createTime.filter((item): item is string => typeof item === 'string')
+  }
+
+  return typeof createTime === 'string'
+    ? [createTime]
+    : [...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
+}
+
+const syncQueryFromRoute = () => {
+  query.value.deptId = route.query.deptId ? Number(route.query.deptId) : id
+  query.value.createTime = getRouteCreateTime()
+  query.value.projectClassification = route.query.projectClassification
+    ? String(route.query.projectClassification)
+    : '1'
+}
+
 const list = ref<any[]>([])
 const total = ref(0)
 
@@ -97,6 +117,13 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => [route.query.deptId, route.query.createTime, route.query.projectClassification],
+  () => {
+    syncQueryFromRoute()
+  }
+)
+
 const exportLoading = ref(false)
 
 async function handleExport() {

+ 45 - 664
src/views/pms/iotrydailyreport/summary.vue

@@ -1,17 +1,9 @@
 <script setup lang="ts">
 import dayjs from 'dayjs'
-import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
-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 download from '@/utils/download'
-
 import { useUserStore } from '@/store/modules/user'
+import DailyStatistics from './components/DailyStatistics.vue'
+import NonProductionEfficiency from './components/NonProductionEfficiency.vue'
 
 const deptId = useUserStore().getUser.deptId
 
@@ -22,566 +14,48 @@ interface Query {
   contractName?: string
   taskName?: string
   createTime: string[]
-  projectClassification: 1 | 2
+  projectClassification: 1
 }
 
 const id = deptId
 
-const query = ref<Query>({
+const createDefaultQuery = (): Query => ({
   pageNo: 1,
   pageSize: 10,
   deptId: deptId,
+  contractName: '',
+  taskName: '',
   createTime: [
     ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
   ],
   projectClassification: 1
 })
 
-const totalWorkKeys: [string, string, string, string, number][] = [
-  ['totalFootage', 'M', '累计进尺', 'i-solar:ruler-bold text-sky', 2],
-  [
-    'utilizationRate',
-    '%',
-    '设备利用率',
-    'i-material-symbols:check-circle-outline-rounded text-emerald',
-    0
-  ],
-  [
-    'totalPowerConsumption',
-    'MWh',
-    '累计用电量',
-    'i-material-symbols:electric-bolt-outline-rounded text-sky',
-    2
-  ],
-  [
-    'totalFuelConsumption',
-    '升',
-    '累计油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky',
-    2
-  ],
-  [
-    'averageFuelConsumption',
-    '升',
-    '平均油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky',
-    2
-  ],
-  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
-  [
-    'alreadyReported',
-    '个',
-    '已填报',
-    'i-material-symbols:check-circle-outline-rounded text-emerald',
-    0
-  ],
-  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
-]
-
-const totalWork = ref({
-  totalCount: 0,
-  alreadyReported: 0,
-  notReported: 0,
-  totalFuelConsumption: 0,
-  totalPowerConsumption: 0,
-  totalFootage: 0,
-  averageFuelConsumption: 0,
-  utilizationRate: 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 IotRyDailyReportApi.ryDailyReportStatistics({
-        createTime: query.value.createTime,
-        projectClassification: query.value.projectClassification
-      })
-
-      totalWork.value.totalCount = res1[0].count
-      totalWork.value.alreadyReported = res1[1].count
-      totalWork.value.notReported = res1[2].count
-    }
-
-    const res2 = await IotRyDailyReportApi.totalWorkload(other)
-
-    totalWork.value = {
-      ...totalWork.value,
-      totalFootage: 0,
-      ...res2,
-      totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
-      totalGasInjection: (res2.totalGasInjection || 0) / 10000,
-      totalFuelConsumption: res2.totalFuelConsumption || 0,
-      averageFuelConsumption: res2.averageFuelConsumption || 0,
-      utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
-    }
-  } finally {
-    totalLoading.value = false
-  }
-}, 1000)
-
-interface List {
-  id: number | null
-  name: string | null
-  type: '1' | '2' | '3'
-  cumulativeGasInjection: number | null
-  cumulativeWaterInjection: number | null
-  cumulativePowerConsumption: number | null
-  cumulativeFuelConsumption: number | null
-  transitTime: number | null
-  nonProductiveTime: number | null
-  averageFuelConsumption: number | null
-  utilizationRate: number | null
-}
-
-const list = ref<List[]>([])
-
-const type = ref('2')
-
-const columns = (type: string) => {
-  return [
-    {
-      label: type === '2' ? '项目部' : '队伍',
-      prop: 'name'
-    },
-    {
-      label: '累计进尺(M)',
-      prop: 'cumulativeFootage'
-    },
-    {
-      label: '累计用电量(MWh)',
-      prop: 'cumulativePowerConsumption'
-    },
-    {
-      label: '累计油耗(升)',
-      prop: 'cumulativeFuelConsumption'
-    },
-    {
-      label: '平均油耗(升)',
-      prop: 'averageFuelConsumption'
-    },
-    {
-      label: '平均时效(%)',
-      prop: 'transitTime'
-    },
-    {
-      label: '非生产时效(%)',
-      prop: 'nonProductiveTime'
-    },
-    {
-      label: '设备利用率(%)',
-      prop: 'utilizationRate',
-      action: true
-    }
-  ]
-}
-
-const listLoading = ref(false)
-
-const formatter = (row: List, column: any) => {
-  if (column.property === 'transitTime') {
-    return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
-  } else if (column.property === 'nonProductiveTime') {
-    return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
-  } else if (column.property === 'utilizationRate') {
-    return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
-  } else return row[column.property] ?? 0
-}
-
-const getList = useDebounceFn(async () => {
-  listLoading.value = true
-  try {
-    const res = await IotRyDailyReportApi.getIotRyDailyReportSummary(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 }) => ({
-        id: type === '2' ? projectDeptId : teamId,
-        name: type === '2' ? projectDeptName : teamName,
-        ...other,
-        cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
-        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
-        averageFuelConsumption: (other.averageFuelConsumption || 0).toFixed(2),
-        utilizationRate: other.utilizationRate || 0
-      })
-    )
-  } finally {
-    listLoading.value = false
-  }
-}, 1000)
-
-const tab = ref<'表格' | '看板'>('表格')
-
-const currentTab = ref<'表格' | '看板'>('表格')
-
+const query = ref<Query>(createDefaultQuery())
+const activeTab = 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
-let chartContainerEl: HTMLDivElement | null = null
-
-const xAxisData = ref<string[]>([])
-
-const legend = ref<string[][]>([
-  // ['累计油耗 (万升)', 'cumulativeFuelConsumption'],
-  ['油耗 (升)', 'cumulativeFuelConsumption'],
-  ['进尺 (M)', 'cumulativeFootage'],
-  ['用电量 (KWh)', 'cumulativePowerConsumption'],
-  // ['累计用电量 (MWh)', 'cumulativePowerConsumption'],
-  ['平均时效 (%)', 'transitTime'],
-  ['设备利用率 (%)', 'utilizationRate']
-])
-
-const chartData = ref<Record<string, number[]>>({
-  cumulativeFuelConsumption: [],
-  cumulativeFootage: [],
-  cumulativePowerConsumption: [],
-  transitTime: [],
-  utilizationRate: []
-})
-
-let chartLoading = ref(false)
-
-const getChart = useDebounceFn(async () => {
-  chartLoading.value = true
-
-  try {
-    // 创建查询参数,如果 createTime 为空则不传
-    const params: any = {
-      deptId: query.value.deptId,
-      projectClassification: query.value.projectClassification
-    }
-
-    // 只有 createTime 有值时才添加
-    if (query.value.createTime && query.value.createTime.length === 2) {
-      params.createTime = query.value.createTime
-    }
-
-    const res = await IotRyDailyReportApi.getIotRyDailyReportSummaryPolyline(query.value)
-
-    chartData.value = {
-      cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
-      // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
-      cumulativeFootage: res.map((item) => item.cumulativeFootage || 0),
-      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
-      // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
-      transitTime: res.map((item) => (item.transitTime || 0) * 100),
-      utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
-    }
-
-    xAxisData.value = res.map((item) => item.reportDate || '')
-  } finally {
-    chartLoading.value = false
-  }
-}, 1000)
-
-const resizer = () => {
-  chart?.resize()
-}
-
-onUnmounted(() => {
-  window.removeEventListener('resize', resizer)
-  chart?.dispose()
-  chart = null
-  chartContainerEl = null
-})
-
-const selectedLegends = ref<Record<string, boolean>>({})
-const intervalArr = ref<number[]>([])
-const maxInterval = ref(0)
-const minInterval = ref(0)
-
-const calcIntervals = () => {
-  let maxVal = -Infinity
-  let minVal = Infinity
-  let hasData = false
-
-  for (const [name, key] of legend.value) {
-    if (selectedLegends.value[name] !== false) {
-      const dataset = chartData.value[key] || []
-      if (dataset.length > 0) {
-        hasData = true
-        for (const val of dataset) {
-          if (val > maxVal) maxVal = val
-          if (val < minVal) minVal = val
-        }
-      }
-    }
-  }
-
-  if (!hasData) {
-    maxVal = 10000
-    minVal = 0
-  } else {
-    minVal = minVal > 0 ? 0 : minVal
-  }
-
-  const maxDigits = (Math.floor(maxVal) + '').length
-  const minDigits = minVal === 0 ? 0 : (Math.floor(Math.abs(minVal)) + '').length
-  const interval = Math.max(maxDigits, minDigits)
-
-  maxInterval.value = interval
-  minInterval.value = minDigits
-
-  const arr = [0]
-  for (let i = 1; i <= interval; i++) {
-    arr.push(Math.pow(10, i))
-  }
-  intervalArr.value = arr
-}
-
-const mapDataValue = (value: number) => {
-  if (value === 0) return 0
-
-  const isPositive = value > 0
-  const absItem = Math.abs(value)
-
-  if (!intervalArr.value.length) return value
-
-  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
-  const min_index = intervalArr.value.findIndex((v) => v === min_value)
-
-  const denominator =
-    min_index < intervalArr.value.length - 1
-      ? intervalArr.value[min_index + 1] - intervalArr.value[min_index]
-      : intervalArr.value[min_index] || 1
-
-  const new_value = (absItem - min_value) / denominator + min_index
-
-  return isPositive ? new_value : -new_value
-}
-
-const getSeries = () => {
-  return legend.value.map(([name, key]) => ({
-    name,
-    type: 'line',
-    smooth: true,
-    showSymbol: true,
-    data: chartData.value[key].map((value) => mapDataValue(value))
-  }))
-}
-
-const initChart = () => {
-  if (!chartRef.value) return
-
-  if (chart && chartContainerEl === chartRef.value) {
-    return
-  }
-
-  chart?.dispose()
-  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
-  chartContainerEl = chartRef.value
-  window.addEventListener('resize', resizer)
-
-  chart.on('legendselectchanged', (params: any) => {
-    selectedLegends.value = params.selected
-
-    calcIntervals()
-
-    chart?.setOption({
-      yAxis: {
-        min: -minInterval.value,
-        max: maxInterval.value
-      },
-      series: getSeries()
-    })
-  })
-}
-
-const render = () => {
-  if (!chartRef.value) return
-
-  initChart()
-
-  legend.value.forEach(([name]) => {
-    selectedLegends.value[name] = true
-  })
-
-  calcIntervals()
-
-  chart?.setOption(
-    {
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: { type: 'line' },
-        formatter: (params: any) => {
-          let d = `${params[0].axisValueLabel}<br>`
-          let item = params.map((el: any) => {
-            const realValue = chartData.value[legend.value[el.componentIndex][1]][el.dataIndex]
-            return `<div class="flex items-center justify-between mt-1 gap-1">
-            <span>${el.marker} ${el.seriesName}</span>
-            <span>${realValue.toFixed(2)} ${el.seriesName.split(' ')[1] || ''}</span>
-          </div>`
-          })
-          return d + item.join('')
-        }
-      },
-      legend: {
-        data: legend.value.map(([name]) => name),
-        selected: selectedLegends.value,
-        show: true
-      },
-      xAxis: {
-        type: 'category',
-        data: xAxisData.value
-      },
-      yAxis: {
-        type: 'value',
-        min: -minInterval.value,
-        max: maxInterval.value,
-        interval: 1,
-        axisLabel: {
-          formatter: (v: number) => {
-            const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
-            return num.toLocaleString()
-          }
-        }
-      },
-      series: getSeries()
-    },
-    true
-  )
-}
+const refreshKey = ref(0)
 
 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 handleQuery = () => {
+  query.value.pageNo = 1
+  refreshKey.value += 1
 }
 
 const resetQuery = () => {
-  query.value = {
-    pageNo: 1,
-    pageSize: 10,
-    deptId: deptId,
-    createTime: [
-      ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
-    ],
-    projectClassification: 1
-  }
+  query.value = createDefaultQuery()
+  deptName.value = '瑞鹰国际钻井'
   handleQuery()
 }
-
-watch(
-  () => query.value.createTime,
-  (val) => {
-    if (!val) {
-      totalWork.value.totalCount = 0
-      totalWork.value.notReported = 0
-      totalWork.value.alreadyReported = 0
-    }
-    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 IotRyDailyReportApi.exportRyDailyReportStatistics(query.value)
-
-  download.excel(res, '瑞鹰钻井日报统计数据.xlsx')
-}
-
-const exportAll = async () => {
-  if (tab.value === '看板') exportChart()
-  else exportData()
-}
-
-const message = useMessage()
-
-const unfilledDialogRef = ref()
-
-const openUnfilledDialog = () => {
-  // 检查是否选择了创建时间
-  if (!query.value.createTime || query.value.createTime.length === 0) {
-    message.warning('请先选择创建时间范围')
-    return
-  }
-
-  // 打开弹窗
-  unfilledDialogRef.value?.open()
-}
-
-const router = useRouter()
-
-const tolist = (id: number, non: boolean = false) => {
-  const { pageNo, pageSize, ...rest } = query.value
-
-  router.push({
-    path: '/iotdayilyreport/IotRyDailyReport',
-    query: {
-      ...rest,
-      deptId: id,
-      ...(non ? { nonProductFlag: 'Y' } : {})
-    }
-  })
-}
 </script>
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[62px_128px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="grid grid-cols-[auto_1fr] grid-rows-[62px_48px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <DeptTreeSelect
       :deptId="id"
@@ -590,7 +64,6 @@ const tolist = (id: number, non: boolean = false) => {
       @node-click="handleDeptNodeClick"
       class="row-span-3"
     />
-    <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3"> </div> -->
     <el-form
       size="default"
       class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
@@ -622,8 +95,8 @@ const tolist = (id: number, non: boolean = false) => {
             start-placeholder="开始日期"
             end-placeholder="结束日期"
             :shortcuts="rangeShortcuts"
-            class="!w-220px"
             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-220px"
           />
         </el-form-item>
       </div>
@@ -634,129 +107,37 @@ const tolist = (id: number, non: boolean = false) => {
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
       </el-form-item>
     </el-form>
-    <div class="grid grid-cols-8 gap-8">
-      <div
-        v-for="info in totalWorkKeys"
-        :key="info[0]"
-        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-4 flex flex-col items-center justify-center gap-2"
-      >
-        <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]"
-          @click="info[2] === '未填报' ? openUnfilledDialog() : ''"
-        >
-          <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
-        </count-to>
-        <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
-          {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
-        </div>
-      </div>
-    </div>
 
-    <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 gap-2">
-      <div class="flex h-12 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>
-      <div class="flex-1 relative">
-        <el-auto-resizer class="absolute">
-          <template #default="{ height }">
-            <Motion
-              as="div"
-              :style="{ position: 'relative', overflow: 'hidden' }"
-              :animate="{ height: `${height}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: `${height}px` }">
-                    <zm-table
-                      v-if="currentTab === '表格'"
-                      :loading="listLoading"
-                      :data="list"
-                      :height="height"
-                      show-border
-                    >
-                      <template v-for="item in columns(type)" :key="item.prop">
-                        <zm-table-column
-                          v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
-                          :label="item.label"
-                          :prop="item.prop"
-                          :formatter="formatter"
-                          :action="item.action"
-                        />
-                        <zm-table-column
-                          v-else-if="item.prop === 'name'"
-                          :label="item.label"
-                          :prop="item.prop"
-                        >
-                          <template #default="{ row }">
-                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
-                              row.name
-                            }}</el-button>
-                          </template>
-                        </zm-table-column>
-                        <zm-table-column v-else :label="item.label" :prop="item.prop">
-                          <template #default="{ row }">
-                            <el-button
-                              v-if="row.nonProductiveTime > 0"
-                              text
-                              type="primary"
-                              @click.prevent="tolist(row.id, true)"
-                            >
-                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
-                            </el-button>
-                            <span v-else>
-                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
-                            </span>
-                          </template>
-                        </zm-table-column>
-                      </template>
-                    </zm-table>
-                    <div
-                      ref="chartRef"
-                      v-loading="chartLoading"
-                      :key="dayjs().valueOf()"
-                      v-else
-                      :style="{ width: `100%`, height: `${height}px` }"
-                    >
-                    </div>
-                  </div>
-                </Motion>
-              </AnimatePresence>
-            </Motion>
-          </template>
-        </el-auto-resizer>
-      </div>
-    </div>
+    <el-button-group class="justify-self-start self-center">
+      <el-button
+        size="default"
+        :type="activeTab === '日报统计' ? 'primary' : 'default'"
+        @click="activeTab = '日报统计'"
+      >
+        日报统计
+      </el-button>
+      <el-button
+        size="default"
+        :type="activeTab === '非生产时效' ? 'primary' : 'default'"
+        @click="activeTab = '非生产时效'"
+      >
+        非生产时效
+      </el-button>
+    </el-button-group>
+
+    <DailyStatistics
+      v-if="activeTab === '日报统计'"
+      :query="query"
+      :dept-name="deptName"
+      :refresh-key="refreshKey"
+    />
+    <NonProductionEfficiency
+      v-else
+      :query="query"
+      :dept-name="deptName"
+      :refresh-key="refreshKey"
+    />
   </div>
-
-  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" />
 </template>
 
 <style scoped>

+ 27 - 0
src/views/pms/iotrydailyreport/xjindex.vue

@@ -44,6 +44,26 @@ const initQuery: Query = {
 
 const query = ref<Query>({ ...initQuery })
 
+const getRouteCreateTime = () => {
+  const createTime = route.query.createTime
+
+  if (Array.isArray(createTime)) {
+    return createTime.filter((item): item is string => typeof item === 'string')
+  }
+
+  return typeof createTime === 'string'
+    ? [createTime]
+    : [...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
+}
+
+const syncQueryFromRoute = () => {
+  query.value.deptId = route.query.deptId ? Number(route.query.deptId) : id
+  query.value.createTime = getRouteCreateTime()
+  query.value.projectClassification = route.query.projectClassification
+    ? String(route.query.projectClassification)
+    : '2'
+}
+
 const list = ref<any[]>([])
 const total = ref(0)
 
@@ -97,6 +117,13 @@ watch(
   { immediate: true }
 )
 
+watch(
+  () => [route.query.deptId, route.query.createTime, route.query.projectClassification],
+  () => {
+    syncQueryFromRoute()
+  }
+)
+
 const exportLoading = ref(false)
 
 async function handleExport() {

+ 45 - 663
src/views/pms/iotrydailyreport/xsummary.vue

@@ -1,17 +1,9 @@
 <script setup lang="ts">
 import dayjs from 'dayjs'
-import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
-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 download from '@/utils/download'
-
 import { useUserStore } from '@/store/modules/user'
+import XjDailyStatistics from './components/XjDailyStatistics.vue'
+import XjNonProductionEfficiency from './components/XjNonProductionEfficiency.vue'
 
 const deptId = useUserStore().getUser.deptId
 
@@ -22,565 +14,48 @@ interface Query {
   contractName?: string
   taskName?: string
   createTime: string[]
-  projectClassification: 1 | 2
+  projectClassification: 2
 }
 
 const id = deptId
 
-const query = ref<Query>({
+const createDefaultQuery = (): Query => ({
   pageNo: 1,
   pageSize: 10,
   deptId: deptId,
+  contractName: '',
+  taskName: '',
   createTime: [
     ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
   ],
   projectClassification: 2
 })
 
-const totalWorkKeys: [string, string, string, string, number][] = [
-  ['constructionWells', '个', '累计施工井数', 'i-mdi:progress-wrench text-sky', 0],
-  ['completedWells', '个', '累计完工井数', 'i-mdi:wrench-check-outline text-emerald', 0],
-  [
-    'utilizationRate',
-    '%',
-    '设备利用率',
-    'i-material-symbols:check-circle-outline-rounded text-emerald',
-    0
-  ],
-  [
-    'totalPowerConsumption',
-    'MWh',
-    '累计用电量',
-    'i-material-symbols:electric-bolt-outline-rounded text-sky',
-    2
-  ],
-  [
-    'totalFuelConsumption',
-    '升',
-    '累计油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky',
-    2
-  ],
-  [
-    'averageFuelConsumption',
-    '升',
-    '平均油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky',
-    2
-  ],
-  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
-  [
-    'alreadyReported',
-    '个',
-    '已填报',
-    'i-material-symbols:check-circle-outline-rounded text-emerald',
-    0
-  ],
-  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0]
-]
-
-const totalWork = ref({
-  totalCount: 0,
-  alreadyReported: 0,
-  notReported: 0,
-  totalFuelConsumption: 0,
-  totalPowerConsumption: 0,
-  constructionWells: 0,
-  completedWells: 0,
-  averageFuelConsumption: 0,
-  utilizationRate: 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 IotRyDailyReportApi.ryDailyReportStatistics({
-        createTime: query.value.createTime,
-        projectClassification: query.value.projectClassification
-      })
-
-      totalWork.value.totalCount = res1[0].count
-      totalWork.value.alreadyReported = res1[1].count
-      totalWork.value.notReported = res1[2].count
-    }
-
-    const res2 = await IotRyDailyReportApi.totalWorkload(other)
-
-    totalWork.value = {
-      ...totalWork.value,
-      constructionWells: 0,
-      completedWells: 0,
-      ...res2,
-      totalPowerConsumption: (res2.totalPowerConsumption || 0) / 1000,
-      totalFuelConsumption: res2.totalFuelConsumption || 0,
-      averageFuelConsumption: res2.averageFuelConsumption || 0,
-      utilizationRate: Number(((res2.utilizationRate || 0) * 100).toFixed(2))
-    }
-  } finally {
-    totalLoading.value = false
-  }
-}, 1000)
-
-interface List {
-  id: number | null
-  name: string | null
-  type: '1' | '2' | '3'
-  cumulativeGasInjection: number | null
-  cumulativeWaterInjection: number | null
-  cumulativePowerConsumption: number | null
-  cumulativeFuelConsumption: number | null
-  transitTime: number | null
-  nonProductiveTime: number | null
-  averageFuelConsumption: number | null
-  utilizationRate: number | null
-}
-
-const list = ref<List[]>([])
-
-const type = ref('2')
-
-const columns = (type: string) => {
-  return [
-    {
-      label: type === '2' ? '项目部' : '队伍',
-      prop: 'name'
-    },
-    {
-      label: '累计施工井数',
-      prop: 'cumulativeConstructWells'
-    },
-    {
-      label: '累计完工井数',
-      prop: 'cumulativeCompletedWells'
-    },
-    {
-      label: '累计用电量(MWh)',
-      prop: 'cumulativePowerConsumption'
-    },
-    {
-      label: '累计油耗(升)',
-      prop: 'cumulativeFuelConsumption'
-    },
-    {
-      label: '平均油耗(升)',
-      prop: 'averageFuelConsumption'
-    },
-    {
-      label: '平均时效(%)',
-      prop: 'transitTime'
-    },
-    {
-      label: '非生产时效(%)',
-      prop: 'nonProductiveTime'
-    },
-    {
-      label: '设备利用率(%)',
-      prop: 'utilizationRate',
-      action: true
-    }
-  ]
-}
-
-const listLoading = ref(false)
-
-const formatter = (row: List, column: any) => {
-  if (column.property === 'transitTime') {
-    return (Number(row.transitTime ?? 0) * 100).toFixed(2) + '%'
-  } else if (column.property === 'nonProductiveTime') {
-    return (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%'
-  } else if (column.property === 'utilizationRate') {
-    return (Number(row.utilizationRate ?? 0) * 100).toFixed(2) + '%'
-  } else return row[column.property] ?? 0
-}
-
-const getList = useDebounceFn(async () => {
-  listLoading.value = true
-  try {
-    const res = await IotRyDailyReportApi.getIotRyDailyReportSummary(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 }) => ({
-        id: type === '2' ? projectDeptId : teamId,
-        name: type === '2' ? projectDeptName : teamName,
-        ...other,
-        cumulativePowerConsumption: ((other.cumulativePowerConsumption || 0) / 1000).toFixed(2),
-        cumulativeFuelConsumption: (other.cumulativeFuelConsumption || 0).toFixed(2),
-        averageFuelConsumption: (other.averageFuelConsumption || 0).toFixed(2),
-        utilizationRate: other.utilizationRate || 0
-      })
-    )
-  } finally {
-    listLoading.value = false
-  }
-}, 1000)
-
-const tab = ref<'表格' | '看板'>('表格')
-
-const currentTab = ref<'表格' | '看板'>('表格')
-
+const query = ref<Query>(createDefaultQuery())
+const activeTab = 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
-let chartContainerEl: HTMLDivElement | null = null
-
-const xAxisData = ref<string[]>([])
-
-const legend = ref<string[][]>([
-  ['施工井数 (个)', 'cumulativeConstructWells'],
-  ['完工井数 (个)', 'cumulativeCompletedWells'],
-  // ['累计油耗 (万升)', 'cumulativeFuelConsumption'],
-  ['油耗 (升)', 'cumulativeFuelConsumption'],
-  // ['累计用电量 (MWh)', 'cumulativePowerConsumption'],
-  ['用电量 (KWh)', 'cumulativePowerConsumption'],
-  ['平均时效 (%)', 'transitTime'],
-  ['设备利用率 (%)', 'utilizationRate']
-])
-
-const chartData = ref<Record<string, number[]>>({
-  cumulativeFuelConsumption: [],
-  cumulativeConstructWells: [],
-  cumulativeCompletedWells: [],
-  cumulativePowerConsumption: [],
-  transitTime: [],
-  utilizationRate: []
-})
-
-let chartLoading = ref(false)
-
-const getChart = useDebounceFn(async () => {
-  chartLoading.value = true
-
-  try {
-    const res = await IotRyDailyReportApi.getIotRyDailyReportSummaryPolyline(query.value)
-
-    chartData.value = {
-      cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
-      // cumulativeFuelConsumption: res.map((item) => (item.cumulativeFuelConsumption || 0) / 10000),
-      cumulativeConstructWells: res.map((item) => item.cumulativeConstructWells || 0),
-      cumulativeCompletedWells: res.map((item) => item.cumulativeCompletedWells || 0),
-      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
-      // cumulativePowerConsumption: res.map((item) => (item.cumulativePowerConsumption || 0) / 1000),
-      transitTime: res.map((item) => (item.transitTime || 0) * 100),
-      utilizationRate: res.map((item) => Number(((item.utilizationRate || 0) * 100).toFixed(2)))
-    }
-
-    xAxisData.value = res.map((item) => item.reportDate || '')
-  } finally {
-    chartLoading.value = false
-  }
-}, 1000)
-
-const resizer = () => {
-  chart?.resize()
-}
-
-onUnmounted(() => {
-  window.removeEventListener('resize', resizer)
-  chart?.dispose()
-  chart = null
-  chartContainerEl = null
-})
-
-const selectedLegends = ref<Record<string, boolean>>({})
-const intervalArr = ref<number[]>([])
-const maxInterval = ref(0)
-const minInterval = ref(0)
-
-const calcIntervals = () => {
-  let maxVal = -Infinity
-  let minVal = Infinity
-  let hasData = false
-
-  for (const [name, key] of legend.value) {
-    if (selectedLegends.value[name] !== false) {
-      const dataset = chartData.value[key] || []
-      if (dataset.length > 0) {
-        hasData = true
-        for (const val of dataset) {
-          if (val > maxVal) maxVal = val
-          if (val < minVal) minVal = val
-        }
-      }
-    }
-  }
-
-  if (!hasData) {
-    maxVal = 10000
-    minVal = 0
-  } else {
-    minVal = minVal > 0 ? 0 : minVal
-  }
-
-  const maxDigits = (Math.floor(maxVal) + '').length
-  const minDigits = minVal === 0 ? 0 : (Math.floor(Math.abs(minVal)) + '').length
-  const interval = Math.max(maxDigits, minDigits)
-
-  maxInterval.value = interval
-  minInterval.value = minDigits
-
-  const arr = [0]
-  for (let i = 1; i <= interval; i++) {
-    arr.push(Math.pow(10, i))
-  }
-  intervalArr.value = arr
-}
-
-const mapDataValue = (value: number) => {
-  if (value === 0) return 0
-
-  const isPositive = value > 0
-  const absItem = Math.abs(value)
-
-  if (!intervalArr.value.length) return value
-
-  const min_value = Math.max(...intervalArr.value.filter((v) => v <= absItem))
-  const min_index = intervalArr.value.findIndex((v) => v === min_value)
-
-  const denominator =
-    min_index < intervalArr.value.length - 1
-      ? intervalArr.value[min_index + 1] - intervalArr.value[min_index]
-      : intervalArr.value[min_index] || 1
-
-  const new_value = (absItem - min_value) / denominator + min_index
-
-  return isPositive ? new_value : -new_value
-}
-
-const getSeries = () => {
-  return legend.value.map(([name, key]) => ({
-    name,
-    type: 'line',
-    smooth: true,
-    showSymbol: true,
-    data: chartData.value[key].map((value) => mapDataValue(value))
-  }))
-}
-
-const initChart = () => {
-  if (!chartRef.value) return
-
-  if (chart && chartContainerEl === chartRef.value) {
-    return
-  }
-
-  chart?.dispose()
-  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
-  chartContainerEl = chartRef.value
-  window.addEventListener('resize', resizer)
-
-  chart.on('legendselectchanged', (params: any) => {
-    selectedLegends.value = params.selected
-
-    calcIntervals()
-
-    chart?.setOption({
-      yAxis: {
-        min: -minInterval.value,
-        max: maxInterval.value
-      },
-      series: getSeries()
-    })
-  })
-}
-
-const render = () => {
-  if (!chartRef.value) return
-
-  initChart()
-
-  legend.value.forEach(([name]) => {
-    selectedLegends.value[name] = true
-  })
-
-  calcIntervals()
-
-  chart?.setOption(
-    {
-      tooltip: {
-        trigger: 'axis',
-        axisPointer: { type: 'line' },
-        formatter: (params: any) => {
-          let d = `${params[0].axisValueLabel}<br>`
-          let item = params.map((el: any) => {
-            const realValue = chartData.value[legend.value[el.componentIndex][1]][el.dataIndex]
-            return `<div class="flex items-center justify-between mt-1 gap-1">
-            <span>${el.marker} ${el.seriesName}</span>
-            <span>${realValue.toFixed(2)} ${el.seriesName.split(' ')[1] || ''}</span>
-          </div>`
-          })
-          return d + item.join('')
-        }
-      },
-      legend: {
-        data: legend.value.map(([name]) => name),
-        selected: selectedLegends.value,
-        show: true
-      },
-      xAxis: {
-        type: 'category',
-        data: xAxisData.value
-      },
-      yAxis: {
-        type: 'value',
-        min: -minInterval.value,
-        max: maxInterval.value,
-        interval: 1,
-        axisLabel: {
-          formatter: (v: number) => {
-            const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
-            return num.toLocaleString()
-          }
-        }
-      },
-      series: getSeries()
-    },
-    true
-  )
-}
+const refreshKey = ref(0)
 
 const handleDeptNodeClick = (node: any) => {
   deptName.value = node.name
-  // query.value.deptId = node.id
   handleQuery()
 }
 
-const handleQuery = (setPage = true) => {
-  if (setPage) {
-    query.value.pageNo = 1
-  }
-  getChart().then(() => {
-    render()
-  })
-  getList()
-  getTotal()
+const handleQuery = () => {
+  query.value.pageNo = 1
+  refreshKey.value += 1
 }
 
 const resetQuery = () => {
-  query.value = {
-    pageNo: 1,
-    pageSize: 10,
-    deptId: deptId,
-    createTime: [
-      ...rangeShortcuts[1].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))
-    ],
-    projectClassification: 1
-  }
+  query.value = createDefaultQuery()
+  deptName.value = '瑞鹰国际修井'
   handleQuery()
 }
-
-watch(
-  () => query.value.createTime,
-  (val) => {
-    if (!val) {
-      totalWork.value.totalCount = 0
-      totalWork.value.notReported = 0
-      totalWork.value.alreadyReported = 0
-    }
-    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 IotRyDailyReportApi.exportRyDailyReportStatistics(query.value)
-
-  download.excel(res, '瑞鹰修井日报统计数据.xlsx')
-}
-
-const exportAll = async () => {
-  if (tab.value === '看板') exportChart()
-  else exportData()
-}
-
-const message = useMessage()
-
-const unfilledDialogRef = ref()
-
-const openUnfilledDialog = () => {
-  // 检查是否选择了创建时间
-  if (!query.value.createTime || query.value.createTime.length === 0) {
-    message.warning('请先选择创建时间范围')
-    return
-  }
-
-  // 打开弹窗
-  unfilledDialogRef.value?.open()
-}
-
-const router = useRouter()
-
-const tolist = (id: number, non: boolean = false) => {
-  const { pageNo, pageSize, ...rest } = query.value
-
-  router.push({
-    path: '/iotdayilyreport/IotRyXjDailyReport',
-    query: {
-      ...rest,
-      deptId: id,
-      ...(non ? { nonProductFlag: 'Y' } : {})
-    }
-  })
-}
 </script>
 
 <template>
   <div
-    class="grid grid-cols-[auto_1fr] grid-rows-[62px_128px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
+    class="grid grid-cols-[auto_1fr] grid-rows-[62px_48px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
   >
     <DeptTreeSelect
       :deptId="id"
@@ -589,7 +64,6 @@ const tolist = (id: number, non: boolean = false) => {
       @node-click="handleDeptNodeClick"
       class="row-span-3"
     />
-    <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-3"> </div> -->
     <el-form
       size="default"
       class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 gap-8 flex items-center justify-between"
@@ -621,8 +95,8 @@ const tolist = (id: number, non: boolean = false) => {
             start-placeholder="开始日期"
             end-placeholder="结束日期"
             :shortcuts="rangeShortcuts"
-            class="!w-220px"
             :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-220px"
           />
         </el-form-item>
       </div>
@@ -633,129 +107,37 @@ const tolist = (id: number, non: boolean = false) => {
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
       </el-form-item>
     </el-form>
-    <div class="grid grid-cols-9 gap-8">
-      <div
-        v-for="info in totalWorkKeys"
-        :key="info[0]"
-        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow p-4 flex flex-col items-center justify-center gap-2"
-      >
-        <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]"
-          @click="info[2] === '未填报' ? openUnfilledDialog() : ''"
-        >
-          <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
-        </count-to>
-        <div class="text-sm font-medium text-[var(--el-text-color-regular)] whitespace-nowrap">
-          {{ info[1] ? info[2] + '(' + info[1] + ')' : info[2] }}
-        </div>
-      </div>
-    </div>
 
-    <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow flex flex-col p-4 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>
-      <div class="flex-1 relative">
-        <el-auto-resizer class="absolute">
-          <template #default="{ height }">
-            <Motion
-              as="div"
-              :style="{ position: 'relative', overflow: 'hidden' }"
-              :animate="{ height: `${height}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: `${height}px` }">
-                    <zm-table
-                      v-if="currentTab === '表格'"
-                      :loading="listLoading"
-                      :data="list"
-                      :height="height"
-                      show-border
-                    >
-                      <template v-for="item in columns(type)" :key="item.prop">
-                        <zm-table-column
-                          v-if="item.prop !== 'name' && item.prop !== 'nonProductiveTime'"
-                          :label="item.label"
-                          :prop="item.prop"
-                          :formatter="formatter"
-                          :action="item.action"
-                        />
-                        <zm-table-column
-                          v-else-if="item.prop === 'name'"
-                          :label="item.label"
-                          :prop="item.prop"
-                        >
-                          <template #default="{ row }">
-                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
-                              row.name
-                            }}</el-button>
-                          </template>
-                        </zm-table-column>
-                        <zm-table-column v-else :label="item.label" :prop="item.prop">
-                          <template #default="{ row }">
-                            <el-button
-                              v-if="row.nonProductiveTime > 0"
-                              text
-                              type="primary"
-                              @click.prevent="tolist(row.id, true)"
-                            >
-                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
-                            </el-button>
-                            <span v-else>
-                              {{ (Number(row.nonProductiveTime ?? 0) * 100).toFixed(2) + '%' }}
-                            </span>
-                          </template>
-                        </zm-table-column>
-                      </template>
-                    </zm-table>
-                    <div
-                      ref="chartRef"
-                      v-loading="chartLoading"
-                      :key="dayjs().valueOf()"
-                      v-else
-                      :style="{ width: `100%`, height: `${height}px` }"
-                    >
-                    </div>
-                  </div>
-                </Motion>
-              </AnimatePresence>
-            </Motion>
-          </template>
-        </el-auto-resizer>
-      </div>
-    </div>
+    <el-button-group class="justify-self-start self-center">
+      <el-button
+        size="default"
+        :type="activeTab === '日报统计' ? 'primary' : 'default'"
+        @click="activeTab = '日报统计'"
+      >
+        日报统计
+      </el-button>
+      <el-button
+        size="default"
+        :type="activeTab === '非生产时效' ? 'primary' : 'default'"
+        @click="activeTab = '非生产时效'"
+      >
+        非生产时效
+      </el-button>
+    </el-button-group>
+
+    <XjDailyStatistics
+      v-if="activeTab === '日报统计'"
+      :query="query"
+      :dept-name="deptName"
+      :refresh-key="refreshKey"
+    />
+    <XjNonProductionEfficiency
+      v-else
+      :query="query"
+      :dept-name="deptName"
+      :refresh-key="refreshKey"
+    />
   </div>
-
-  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" />
 </template>
 
 <style scoped>