Przeglądaj źródła

✨ feat(日报汇总): 添加看板等一些完善功能

Zimo 5 dni temu
rodzic
commit
2a9cf2eafe

+ 7 - 0
src/api/pms/iotrhdailyreport/index.ts

@@ -61,6 +61,13 @@ export const IotRhDailyReportApi = {
     return await request.get({ url: `/pms/iot-rh-daily-report/rhDailyReportStatistics`, params })
   },
 
+  exportRhDailyReportStatistics: async (params: any) => {
+    return await request.download({
+      url: `/pms/iot-rh-daily-report/exportStatistics`,
+      params
+    })
+  },
+
   // 按照日期查询瑞恒日报统计数据 未填报队伍明细
   rhUnReportDetails: async (params: any) => {
     return await request.get({ url: `/pms/iot-rh-daily-report/rhUnReportDetails`, params })

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

@@ -54,6 +54,21 @@ export interface IotRyDailyReportVO {
 
 // 瑞鹰日报 API
 export const IotRyDailyReportApi = {
+  exportRyDailyReportStatistics: async (params: any) => {
+    return await request.download({
+      url: `/pms/iot-ry-daily-report/exportStatistics`,
+      params
+    })
+  },
+
+  ryUnReportDetails: async (params: any) => {
+    return await request.get({ url: `/pms/iot-ry-daily-report/ryUnReportDetails`, params })
+  },
+
+  getIotRyDailyReportSummaryPolyline: async (params: any) => {
+    return await request.get({ url: `/pms/iot-ry-daily-report/polylineStatistics`, params })
+  },
+
   // 查询瑞鹰日报分页
   getIotRyDailyReportPage: async (params: any) => {
     return await request.get({ url: `/pms/iot-ry-daily-report/page`, params })

+ 20 - 6
src/utils/formatTime.ts

@@ -1,11 +1,15 @@
 import dayjs from 'dayjs'
+import quarter from 'dayjs/plugin/quarterOfYear'
+
+dayjs.extend(quarter)
+
 import type { TableColumnCtx } from 'element-plus'
 
 /**
  * 日期快捷选项适用于 el-date-picker
  */
 
-export  const rangeShortcuts = [
+export const rangeShortcuts = [
   {
     text: '今天',
     value: () => {
@@ -23,14 +27,16 @@ export  const rangeShortcuts = [
   {
     text: '本周',
     value: () => {
-      return [dayjs().startOf('week').toDate(), dayjs().endOf('week').toDate()]
+      return [dayjs().subtract(6, 'day').startOf('day').toDate(), dayjs().endOf('day').toDate()]
     }
   },
   {
     text: '上周',
     value: () => {
-      const lastWeek = dayjs().subtract(1, 'week')
-      return [lastWeek.startOf('week').toDate(), lastWeek.endOf('week').toDate()]
+      return [
+        dayjs().subtract(13, 'day').startOf('day').toDate(),
+        dayjs().subtract(7, 'day').endOf('day').toDate()
+      ]
     }
   },
   {
@@ -291,7 +297,11 @@ export function formatPast2(ms: number): string {
  * @param column 字段
  * @param cellValue 字段值
  */
-export function dateFormatter(_row: any, _column: TableColumnCtx<any> | null, cellValue: any): string {
+export function dateFormatter(
+  _row: any,
+  _column: TableColumnCtx<any> | null,
+  cellValue: any
+): string {
   return cellValue ? formatDate(cellValue) : ''
 }
 
@@ -302,7 +312,11 @@ export function dateFormatter(_row: any, _column: TableColumnCtx<any> | null, ce
  * @param column 字段
  * @param cellValue 字段值
  */
-export function dateFormatter2(_row: any, _column: TableColumnCtx<any> |null, cellValue: any): string {
+export function dateFormatter2(
+  _row: any,
+  _column: TableColumnCtx<any> | null,
+  cellValue: any
+): string {
   return cellValue ? formatDate(cellValue, 'YYYY-MM-DD') : ''
 }
 

+ 12 - 2
src/views/pms/iotrhdailyreport/index.vue

@@ -483,7 +483,7 @@ const selectedRowData = ref<Record<string, any> | null>(null)
 const loading = ref(true) // 列表的加载中
 const list = ref<IotRhDailyReportVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+let queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -787,6 +787,7 @@ const getStatistics = async () => {
 const getList = async () => {
   loading.value = true
   try {
+    console.log('22 :>> ', 11)
     const data = await IotRhDailyReportApi.getIotRhDailyReportPage(queryParams)
     list.value = data.list
     total.value = data.total
@@ -912,6 +913,8 @@ const handleQuery = () => {
   getList()
 }
 
+const route = useRoute()
+
 /** 重置按钮操作 */
 const resetQuery = () => {
   queryFormRef.value.resetFields()
@@ -983,7 +986,14 @@ let resizeObserver: ResizeObserver | null = null
 
 /** 初始化 **/
 onMounted(() => {
-  getList()
+  if (Object.keys(route.query).length > 0) {
+    queryParams = {
+      ...queryParams,
+      ...route.query,
+      deptId: Number(route.query.deptId) as any
+    }
+    handleQuery()
+  } else getList()
   // 创建 ResizeObserver 监听表格容器尺寸变化
   if (tableContainerRef.value?.$el) {
     resizeObserver = new ResizeObserver(() => {

+ 265 - 63
src/views/pms/iotrhdailyreport/summary.vue

@@ -4,10 +4,13 @@ import dayjs from 'dayjs'
 import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
 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'
 
 interface Query {
   pageNo: number
@@ -29,34 +32,38 @@ const query = ref<Query>({
   ]
 })
 
-const totalWorkKeys = [
-  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky'],
+const totalWorkKeys: [string, string, string, string, number][] = [
+  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
   [
     'alreadyReported',
     '个',
     '已填报',
-    'i-material-symbols:check-circle-outline-rounded text-emerald'
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
   ],
-  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose'],
+  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0],
   [
     'totalFuelConsumption',
     '吨',
     '累计油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky'
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
   ],
   [
     'totalPowerConsumption',
     'KWH',
     '累计用电量',
-    'i-material-symbols:electric-bolt-outline-rounded text-sky'
+    'i-material-symbols:electric-bolt-outline-rounded text-sky',
+    2
   ],
   [
     'totalWaterInjection',
     '方',
     '累计注水量',
-    'i-material-symbols:water-drop-outline-rounded text-sky'
+    'i-material-symbols:water-drop-outline-rounded text-sky',
+    2
   ],
-  ['totalGasInjection', '万方', '累计注气量', 'i-material-symbols:cloud-outline text-sky']
+  ['totalGasInjection', '万方', '累计注气量', 'i-material-symbols:cloud-outline text-sky', 4]
 ]
 
 const totalWork = ref({
@@ -112,8 +119,6 @@ interface List {
   transitTime: number | null
 }
 
-const total = ref<number>(1000)
-
 const list = ref<List[]>([])
 
 const type = ref('2')
@@ -152,24 +157,21 @@ const formatter = (row: List, column: any) => {
 const getList = useDebounceFn(async () => {
   listLoading.value = true
   try {
-    const res = (await IotRhDailyReportApi.getIotRhDailyReportSummary(query.value)) as {
-      total: number
-      list: any[]
-    }
+    const res = await IotRhDailyReportApi.getIotRhDailyReportSummary(query.value)
 
-    const { total: resTotal, list: resList } = res
+    const { list: reslist } = res
 
-    total.value = resTotal
+    type.value = reslist[0]?.type || '2'
 
-    type.value = resList[0]?.type || '2'
-
-    list.value = resList.map(
-      ({ id, projectDeptIa, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => ({
-        id: type === '2' ? projectDeptIa : teamId,
-        name: type === '2' ? projectDeptName : teamName,
-        ...other,
-        cumulativeGasInjection: (other.cumulativeGasInjection || 0) / 10000
-      })
+    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
+        }
+      }
     )
   } finally {
     listLoading.value = false
@@ -184,33 +186,149 @@ const deptName = ref('瑞恒兴域')
 
 const direction = ref<'left' | 'right'>('right')
 
-watch(
-  () => tab.value,
-  (val) => {
-    direction.value = val === '看板' ? 'right' : 'left'
-    nextTick(() => {
-      currentTab.value = val
+const handleSelectTab = (val: '表格' | '看板') => {
+  tab.value = val
+  direction.value = val === '看板' ? 'right' : 'left'
+  nextTick(() => {
+    currentTab.value = val
+    setTimeout(() => {
+      render()
     })
-  },
-  {
-    immediate: true
-  }
-)
+  })
+}
 
 const chartRef = ref<HTMLDivElement | null>(null)
 let chart: echarts.ECharts | null = null
 
+const xAxisData = ref<string[]>([])
+
+const legend = ref<string[][]>([
+  ['累计油耗 (吨)', 'cumulativeFuelConsumption'],
+  ['累计注气量 (万方)', 'cumulativeGasInjection'],
+  ['累计用电量 (KWH)', 'cumulativePowerConsumption'],
+  ['累计注水量 (方)', 'cumulativeWaterInjection'],
+  ['平均时效 (%)', 'transitTime']
+])
+
+const chartData = ref<Record<string, number[]>>({
+  cumulativeFuelConsumption: [],
+  cumulativeGasInjection: [],
+  cumulativePowerConsumption: [],
+  cumulativeWaterInjection: [],
+  transitTime: []
+})
+
 let chartLoading = ref(false)
 
 const getChart = useDebounceFn(async () => {
   chartLoading.value = true
 
-  // try {
-  //   const res =
-  // } finally {
-  // }
+  try {
+    const res = await IotRhDailyReportApi.getIotRhDailyReportSummaryPolyline(query.value)
+
+    chartData.value = {
+      cumulativeFuelConsumption: res.map((item) => item.cumulativeFuelConsumption || 0),
+      cumulativeGasInjection: res.map((item) => (item.cumulativeGasInjection || 0) / 10000),
+      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
+      cumulativeWaterInjection: res.map((item) => item.cumulativeWaterInjection || 0),
+      transitTime: res.map((item) => (item.transitTime || 0) * 100)
+    }
+
+    xAxisData.value = res.map((item) => item.reportDate || '')
+  } finally {
+    chartLoading.value = false
+  }
 }, 1000)
 
+const render = () => {
+  if (!chartRef.value) return
+
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+
+  const values: number[] = []
+
+  for (const [_name, key] of legend.value) {
+    values.push(...(chartData.value[key] || []))
+  }
+
+  const maxVal = values.length === 0 ? 10000 : Math.max(...values)
+  const minVal = values.length === 0 ? 0 : Math.min(...values)
+
+  const maxDigits = (Math.floor(maxVal) + '').length
+  const minDigits = (Math.floor(Math.abs(minVal)) + '').length
+  const interval = Math.max(maxDigits, minDigits)
+
+  const maxInterval = interval
+  const minInterval = minDigits
+
+  const intervalArr = [0]
+  for (let i = 1; i <= interval; i++) {
+    intervalArr.push(Math.pow(10, i))
+  }
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'line'
+      },
+      formatter: (params) => {
+        let d = `${params[0].axisValueLabel}<br>`
+        let item = params.map((el) => {
+          return `<div class="flex items-center justify-between mt-1 gap-1">
+            <span>${el.marker} ${el.seriesName}</span>
+            <span>${chartData.value[legend.value[el.componentIndex][1]][el.dataIndex].toFixed(2)} ${el.seriesName.split(' ')[1]}</span>
+          </div>`
+        })
+
+        return d + item.join('')
+      }
+    },
+    legend: {
+      data: legend.value.map(([name]) => name),
+      show: true
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxisData.value
+    },
+    yAxis: {
+      type: 'value',
+      min: minInterval,
+      max: maxInterval,
+      interval: 1,
+      axisLabel: {
+        formatter: (v) => {
+          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
+
+          return num.toLocaleString()
+        }
+      }
+    },
+    series: legend.value.map(([name, key]) => ({
+      name,
+      type: 'line',
+      smooth: true,
+      showSymbol: true,
+      data: chartData.value[key].map((value) => {
+        // return value
+        if (value === 0) return 0
+
+        const isPositive = value > 0
+        const absItem = Math.abs(value)
+
+        const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
+        const min_index = intervalArr.findIndex((v) => v === min_value)
+
+        const new_value =
+          (absItem - min_value) / (intervalArr[min_index + 1] - intervalArr[min_index]) + min_index
+
+        return isPositive ? new_value : -new_value
+      })
+    }))
+  })
+}
+
 const handleDeptNodeClick = (node: any) => {
   deptName.value = node.name
   query.value.deptId = node.id
@@ -221,8 +339,11 @@ const handleQuery = (setPage = true) => {
   if (setPage) {
     query.value.pageNo = 1
   }
-  getTotal()
+  getChart().then(() => {
+    render()
+  })
   getList()
+  getTotal()
 }
 
 const resetQuery = () => {
@@ -245,6 +366,73 @@ watch(
 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 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) => {
+  const { pageNo, pageSize, ...rest } = query.value
+
+  router.push({
+    path: '/iotdayilyreport/IotRhDailyReport',
+    query: {
+      ...rest,
+      deptId: id
+    }
+  })
+}
 </script>
 
 <template>
@@ -302,7 +490,13 @@ onMounted(() => {
           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]]">
+          <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-xs font-medium text-[var(--el-text-color-regular)]">{{ info[1] }}</div>
@@ -317,30 +511,30 @@ onMounted(() => {
             <el-button
               size="default"
               :type="tab === '表格' ? 'primary' : 'default'"
-              @click="tab = '表格'"
+              @click="handleSelectTab('表格')"
               >表格
             </el-button>
             <el-button
               size="default"
               :type="tab === '看板' ? 'primary' : 'default'"
-              @click="tab = '看板'"
+              @click="handleSelectTab('看板')"
               >看板
             </el-button>
           </el-button-group>
           <h3 class="text-xl font-medium">{{ `${deptName}-${tab}` }}</h3>
-          <el-button size="default" type="primary">导出</el-button>
+          <el-button size="default" type="primary" @click="exportAll">导出</el-button>
         </div>
         <el-auto-resizer>
           <template #default="{ height, width }">
             <Motion
               as="div"
               :style="{ position: 'relative', overflow: 'hidden' }"
-              :animate="{ height: `${height + 1}px`, width: `${width}px` }"
+              :animate="{ height: `${height}px`, width: `${width}px` }"
               :transition="{ type: 'spring', stiffness: 200, damping: 25, duration: 0.3 }"
             >
               <AnimatePresence :initial="false" mode="sync">
                 <Motion
-                  :key="tab"
+                  :key="currentTab"
                   as="div"
                   :initial="{ x: direction === 'left' ? '-100%' : '100%', opacity: 0 }"
                   :animate="{ x: '0%', opacity: 1 }"
@@ -358,17 +552,32 @@ onMounted(() => {
                       :max-height="height"
                       show-overflow-tooltip
                     >
-                      <el-table-column
-                        v-for="item in columns(type)"
-                        :key="item.prop"
-                        :label="item.label"
-                        :prop="item.prop"
-                        align="center"
-                        :formatter="formatter"
-                      />
+                      <template v-for="item in columns(type)" :key="item.prop">
+                        <el-table-column
+                          v-if="item.prop !== 'name'"
+                          :label="item.label"
+                          :prop="item.prop"
+                          align="center"
+                          :formatter="formatter"
+                        />
+                        <el-table-column
+                          v-else
+                          :label="item.label"
+                          :prop="item.prop"
+                          align="center"
+                        >
+                          <template #default="{ row }">
+                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                              row.name
+                            }}</el-button>
+                          </template>
+                        </el-table-column>
+                      </template>
                     </el-table>
                     <div
                       ref="chartRef"
+                      v-loading="chartLoading"
+                      :key="dayjs().valueOf()"
                       v-else
                       :style="{ width: `${width}px`, height: `${height}px` }"
                     >
@@ -378,18 +587,11 @@ onMounted(() => {
               </AnimatePresence>
             </Motion>
           </template>
-
-          <!-- <Pagination
-          class="mt-8"
-          :total="total"
-          v-model:page="query.pageNo"
-          v-model:limit="query.pageSize"
-          @pagination="getList"
-        /> -->
         </el-auto-resizer>
       </div>
     </div>
   </div>
+  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" />
 </template>
 
 <style scoped>

+ 259 - 0
src/views/pms/iotrydailyreport/UnfilledReportDialog.vue

@@ -0,0 +1,259 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    title="未填报详情"
+    width="80%"
+    top="5vh"
+    :close-on-click-modal="false"
+    @closed="handleClosed"
+  >
+    <!-- 搜索条件区域 -->
+    <ContentWrap class="mb-15px">
+      <el-form :model="searchParams" ref="searchFormRef" :inline="true" label-width="100px">
+        <el-form-item label="创建时间" prop="createTime">
+          <el-date-picker
+            v-model="searchParams.createTime"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+            class="!w-320px"
+            @change="handleSearch"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button @click="handleSearch">
+            <Icon icon="ep:search" class="mr-5px" /> 搜索
+          </el-button>
+          <el-button @click="resetSearch">
+            <Icon icon="ep:refresh" class="mr-5px" /> 重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </ContentWrap>
+
+    <!-- 列表区域 -->
+    <ContentWrap>
+      <div class="table-container">
+        <el-table
+          v-loading="loading"
+          :data="list"
+          :stripe="true"
+          style="width: 100%"
+          :cell-style="cellStyle"
+          empty-text="暂无未填报数据"
+          table-layout="fixed"
+        >
+          <el-table-column label="日期" align="center" prop="reportDate" width="120">
+            <template #default="scope">
+              <span class="date-content">{{ scope.row.reportDate }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="部门名称" prop="deptNames" min-width="300">
+            <template #default="scope">
+              <div class="dept-names-content">
+                {{ scope.row.deptNames || '-' }}
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
+      <!-- 分页
+      <Pagination
+        :total="total"
+        v-model:page="searchParams.pageNo"
+        v-model:limit="searchParams.pageSize"
+        @pagination="getList"
+      /> -->
+    </ContentWrap>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, watch, nextTick } from 'vue'
+import { IotRyDailyReportApi } from '@/api/pms/iotrydailyreport'
+
+const { t } = useI18n()
+const message = useMessage()
+
+// 弹窗显示控制
+const dialogVisible = ref(false)
+
+// 搜索参数
+const searchParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  createTime: []
+})
+
+// 列表数据
+const list = ref<any[]>([])
+const total = ref(0)
+const loading = ref(false)
+
+// 接收父组件传递的查询参数
+const props = defineProps<{
+  queryParams: any
+}>()
+
+// 搜索表单引用
+const searchFormRef = ref()
+
+// 打开弹窗
+const open = () => {
+  // 复制父组件的查询参数
+  if (props.queryParams.createTime && props.queryParams.createTime.length > 0) {
+    searchParams.createTime = [...props.queryParams.createTime]
+  }
+
+  dialogVisible.value = true
+  // 获取数据
+  nextTick(() => {
+    getList()
+  })
+}
+
+// 获取列表数据
+const getList = async () => {
+  // 检查时间范围
+  if (!searchParams.createTime || searchParams.createTime.length === 0) {
+    list.value = []
+    total.value = 0
+    return
+  }
+
+  loading.value = true
+  try {
+    const res = await IotRyDailyReportApi.ryUnReportDetails({
+      createTime: searchParams.createTime,
+      projectClassification: props.queryParams.projectClassification
+    })
+
+    // 处理返回数据
+    if (res && Array.isArray(res)) {
+      list.value = res
+      total.value = res.length
+    } else {
+      list.value = []
+      total.value = 0
+    }
+  } catch (error) {
+    console.error('获取未填报数据失败', error)
+    message.error('获取未填报数据失败')
+    list.value = []
+    total.value = 0
+  } finally {
+    loading.value = false
+  }
+}
+
+// 搜索
+const handleSearch = () => {
+  searchParams.pageNo = 1
+  getList()
+}
+
+// 重置搜索
+const resetSearch = () => {
+  searchFormRef.value?.resetFields()
+  handleSearch()
+}
+
+// 单元格样式
+const cellStyle = ({ row, column, rowIndex, columnIndex }: any) => {
+  // 为所有列设置基本样式
+  const baseStyle = {
+    padding: '8px 4px'
+  }
+
+  if (column.property === 'deptNames') {
+    return {
+      ...baseStyle,
+      whiteSpace: 'normal',
+      wordBreak: 'break-all',
+      lineHeight: '1.5'
+    }
+  }
+
+  return baseStyle
+}
+
+// 弹窗关闭处理
+const handleClosed = () => {
+  list.value = []
+  total.value = 0
+  searchParams.pageNo = 1
+}
+
+// 暴露方法给父组件
+defineExpose({
+  open
+})
+
+// 监听父组件查询参数变化
+watch(
+  () => props.queryParams.createTime,
+  (newVal) => {
+    if (newVal && newVal.length > 0) {
+      searchParams.createTime = [...newVal]
+    }
+  },
+  { deep: true }
+)
+</script>
+
+<style scoped>
+/* 表格容器确保正确布局 */
+.table-container {
+  width: 100%;
+  overflow-x: auto;
+}
+
+.date-content {
+  white-space: nowrap;
+}
+
+.dept-names-content {
+  white-space: normal;
+  word-break: break-all;
+  line-height: 1.5;
+  padding: 8px 4px;
+}
+
+/* 深度样式修改确保表格正确显示 */
+:deep(.el-table) {
+  table-layout: fixed;
+}
+
+:deep(.el-table .el-table__cell) {
+  box-sizing: border-box;
+}
+
+:deep(.el-table .cell) {
+  white-space: normal;
+  word-break: break-all;
+  line-height: 1.5;
+  padding: 8px 4px;
+}
+
+:deep(.el-table td.el-table__cell) {
+  padding: 8px 4px;
+  border-bottom: 1px solid var(--el-table-border-color);
+}
+
+:deep(.el-table th.el-table__cell) {
+  padding: 8px 4px;
+  background-color: var(--el-table-header-bg-color);
+}
+
+/* 确保列宽正确分配 */
+:deep(.el-table__body colgroup col:nth-child(1)) {
+  width: 120px;
+}
+
+:deep(.el-table__body colgroup col:nth-child(2)) {
+  width: auto;
+}
+</style>

+ 18 - 10
src/views/pms/iotrydailyreport/index.vue

@@ -251,9 +251,9 @@
                 resizable
               >
                 <template #default="scope">
-                <span :class="{'fuel-warning': shouldShowFuelWarning(scope.row)}">
-                  {{ scope.row.dailyFuel }}
-                </span>
+                  <span :class="{ 'fuel-warning': shouldShowFuelWarning(scope.row) }">
+                    {{ scope.row.dailyFuel }}
+                  </span>
                 </template>
               </el-table-column>
             </el-table-column>
@@ -539,7 +539,7 @@ const rootDeptId = ref(158)
 const loading = ref(true) // 列表的加载中
 const list = ref<IotRyDailyReportVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+let queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -856,7 +856,6 @@ const cellStyle = ({
   rowIndex: number
   columnIndex: number
 }) => {
-
   // 处理当日油耗预警
   if (column.property === 'dailyFuel') {
     if (shouldShowFuelWarning(row)) {
@@ -864,7 +863,7 @@ const cellStyle = ({
         color: 'red',
         fontWeight: 'bold',
         backgroundColor: '#fff5f5' // 可选:添加背景色突出显示
-      };
+      }
     }
   }
 
@@ -914,9 +913,9 @@ const getList = async () => {
 
 // 在 cellStyle 函数附近添加油耗预警判断函数
 const shouldShowFuelWarning = (row: any): boolean => {
-  const dailyFuel = parseFloat(row.dailyFuel);
-  return !isNaN(dailyFuel) && dailyFuel > 15;
-};
+  const dailyFuel = parseFloat(row.dailyFuel)
+  return !isNaN(dailyFuel) && dailyFuel > 15
+}
 
 // 计算列宽度
 
@@ -994,9 +993,18 @@ const handleExport = async () => {
 // 声明 ResizeObserver 实例
 let resizeObserver: ResizeObserver | null = null
 
+const route = useRoute()
+
 /** 初始化 **/
 onMounted(() => {
-  getList()
+  if (Object.keys(route.query).length > 0) {
+    queryParams = {
+      ...queryParams,
+      ...route.query,
+      deptId: Number(route.query.deptId) as any
+    }
+    handleQuery()
+  } else getList()
   // 创建 ResizeObserver 监听表格容器尺寸变化
   if (tableContainerRef.value?.$el) {
     resizeObserver = new ResizeObserver(() => {

+ 333 - 49
src/views/pms/iotrydailyreport/summary.vue

@@ -4,8 +4,13 @@ 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'
 
 interface Query {
   pageNo: number
@@ -29,28 +34,31 @@ const query = ref<Query>({
   projectClassification: 1
 })
 
-const totalWorkKeys = [
-  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky'],
+const totalWorkKeys: [string, string, string, string, number][] = [
+  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
   [
     'alreadyReported',
     '个',
     '已填报',
-    'i-material-symbols:check-circle-outline-rounded text-emerald'
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
   ],
-  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose'],
+  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0],
   [
     'totalFuelConsumption',
     '吨',
     '累计油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky'
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
   ],
   [
     'totalPowerConsumption',
     'KWH',
     '累计用电量',
-    'i-material-symbols:electric-bolt-outline-rounded text-sky'
+    'i-material-symbols:electric-bolt-outline-rounded text-sky',
+    2
   ],
-  ['totalFootage', 'M', '累计进尺', 'i-solar:ruler-bold text-sky']
+  ['totalFootage', 'M', '累计进尺', 'i-solar:ruler-bold text-sky', 2]
 ]
 
 const totalWork = ref({
@@ -105,8 +113,6 @@ interface List {
   transitTime: number | null
 }
 
-const total = ref<number>(1000)
-
 const list = ref<List[]>([])
 
 const type = ref('2')
@@ -141,20 +147,15 @@ const formatter = (row: List, column: any) => {
 const getList = useDebounceFn(async () => {
   listLoading.value = true
   try {
-    const res = (await IotRyDailyReportApi.getIotRyDailyReportSummary(query.value)) as {
-      total: number
-      list: any[]
-    }
+    const res = await IotRyDailyReportApi.getIotRyDailyReportSummary(query.value)
 
-    const { total: resTotal, list: resList } = res
+    const { list: reslist } = res
 
-    total.value = resTotal
+    type.value = reslist[0]?.type || '2'
 
-    type.value = resList[0]?.type || '2'
-
-    list.value = resList.map(
-      ({ id, projectDeptIa, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => ({
-        id: type === '2' ? projectDeptIa : teamId,
+    list.value = reslist.map(
+      ({ id, projectDeptId, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => ({
+        id: type === '2' ? projectDeptId : teamId,
         name: type === '2' ? projectDeptName : teamName,
         ...other
       })
@@ -164,6 +165,154 @@ const getList = useDebounceFn(async () => {
   }
 }, 1000)
 
+const tab = ref<'表格' | '看板'>('表格')
+
+const currentTab = ref<'表格' | '看板'>('表格')
+
+const deptName = ref('瑞恒兴域')
+
+const direction = ref<'left' | 'right'>('right')
+
+const handleSelectTab = (val: '表格' | '看板') => {
+  tab.value = val
+  direction.value = val === '看板' ? 'right' : 'left'
+  nextTick(() => {
+    currentTab.value = val
+    setTimeout(() => {
+      render()
+    })
+  })
+}
+
+const chartRef = ref<HTMLDivElement | null>(null)
+let chart: echarts.ECharts | null = null
+
+const xAxisData = ref<string[]>([])
+
+const legend = ref<string[][]>([
+  ['累计油耗 (吨)', 'cumulativeFuelConsumption'],
+  ['累计进尺 (M)', 'cumulativeFootage'],
+  ['累计用电量 (KWH)', 'cumulativePowerConsumption'],
+  ['平均时效 (%)', 'transitTime']
+])
+
+const chartData = ref<Record<string, number[]>>({
+  cumulativeFuelConsumption: [],
+  cumulativeFootage: [],
+  cumulativePowerConsumption: [],
+  transitTime: []
+})
+
+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),
+      cumulativeFootage: res.map((item) => item.cumulativeFootage || 0),
+      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
+      transitTime: res.map((item) => (item.transitTime || 0) * 100)
+    }
+
+    xAxisData.value = res.map((item) => item.reportDate || '')
+  } finally {
+    chartLoading.value = false
+  }
+}, 1000)
+
+const render = () => {
+  if (!chartRef.value) return
+
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+
+  const values: number[] = []
+
+  for (const [_name, key] of legend.value) {
+    values.push(...(chartData.value[key] || []))
+  }
+
+  const maxVal = values.length === 0 ? 10000 : Math.max(...values)
+  const minVal = values.length === 0 ? 0 : Math.min(...values)
+
+  const maxDigits = (Math.floor(maxVal) + '').length
+  const minDigits = (Math.floor(Math.abs(minVal)) + '').length
+  const interval = Math.max(maxDigits, minDigits)
+
+  const maxInterval = interval
+  const minInterval = minDigits
+
+  const intervalArr = [0]
+  for (let i = 1; i <= interval; i++) {
+    intervalArr.push(Math.pow(10, i))
+  }
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'line'
+      },
+      formatter: (params) => {
+        let d = `${params[0].axisValueLabel}<br>`
+        let item = params.map((el) => {
+          return `<div class="flex items-center justify-between mt-1 gap-1">
+            <span>${el.marker} ${el.seriesName}</span>
+            <span>${chartData.value[legend.value[el.componentIndex][1]][el.dataIndex].toFixed(2)} ${el.seriesName.split(' ')[1]}</span>
+          </div>`
+        })
+
+        return d + item.join('')
+      }
+    },
+    legend: {
+      data: legend.value.map(([name]) => name),
+      show: true
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxisData.value
+    },
+    yAxis: {
+      type: 'value',
+      min: minInterval,
+      max: maxInterval,
+      interval: 1,
+      axisLabel: {
+        formatter: (v) => {
+          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
+
+          return num.toLocaleString()
+        }
+      }
+    },
+    series: legend.value.map(([name, key]) => ({
+      name,
+      type: 'line',
+      smooth: true,
+      showSymbol: true,
+      data: chartData.value[key].map((value) => {
+        // return value
+        if (value === 0) return 0
+
+        const isPositive = value > 0
+        const absItem = Math.abs(value)
+
+        const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
+        const min_index = intervalArr.findIndex((v) => v === min_value)
+
+        const new_value =
+          (absItem - min_value) / (intervalArr[min_index + 1] - intervalArr[min_index]) + min_index
+
+        return isPositive ? new_value : -new_value
+      })
+    }))
+  })
+}
+
 const handleDeptNodeClick = (node: any) => {
   query.value.deptId = node.id
   handleQuery()
@@ -173,8 +322,11 @@ const handleQuery = (setPage = true) => {
   if (setPage) {
     query.value.pageNo = 1
   }
-  getTotal()
+  getChart().then(() => {
+    render()
+  })
   getList()
+  getTotal()
 }
 
 const resetQuery = () => {
@@ -196,6 +348,73 @@ watch(
 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) => {
+  const { pageNo, pageSize, ...rest } = query.value
+
+  router.push({
+    path: '/iotdayilyreport/IotRyDailyReport',
+    query: {
+      ...rest,
+      deptId: id
+    }
+  })
+}
 </script>
 
 <template>
@@ -253,43 +472,108 @@ onMounted(() => {
           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]]">
+          <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-xs font-medium text-[var(--el-text-color-regular)]">{{ info[1] }}</div>
           <div class="text-sm font-medium text-[var(--el-text-color-regular)]">{{ info[2] }}</div>
         </div>
       </div>
-      <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow pt-4 px-8">
-        <el-table
-          v-loading="listLoading"
-          :data="list"
-          :stripe="true"
-          :style="{ width: '100%' }"
-          max-height="600"
-          class="min-h-143"
-          show-overflow-tooltip
-        >
-          <el-table-column
-            v-for="item in columns(type)"
-            :key="item.prop"
-            :label="item.label"
-            :prop="item.prop"
-            align="center"
-            :formatter="formatter"
-          />
-        </el-table>
-
-        <!-- <Pagination
-          class="mt-8"
-          :total="total"
-          v-model:page="query.pageNo"
-          v-model:limit="query.pageSize"
-          @pagination="getList"
-        /> -->
+      <div
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-4 grid grid-rows-[48px_1fr] gap-2"
+      >
+        <div class="flex items-center justify-between">
+          <el-button-group>
+            <el-button
+              size="default"
+              :type="tab === '表格' ? 'primary' : 'default'"
+              @click="handleSelectTab('表格')"
+              >表格
+            </el-button>
+            <el-button
+              size="default"
+              :type="tab === '看板' ? 'primary' : 'default'"
+              @click="handleSelectTab('看板')"
+              >看板
+            </el-button>
+          </el-button-group>
+          <h3 class="text-xl font-medium">{{ `${deptName}-${tab}` }}</h3>
+          <el-button size="default" type="primary" @click="exportAll">导出</el-button>
+        </div>
+        <el-auto-resizer>
+          <template #default="{ height, width }">
+            <Motion
+              as="div"
+              :style="{ position: 'relative', overflow: 'hidden' }"
+              :animate="{ height: `${height}px`, width: `${width}px` }"
+              :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: `${width}px`, height: `${height}px` }">
+                    <el-table
+                      v-if="currentTab === '表格'"
+                      v-loading="listLoading"
+                      :data="list"
+                      :stripe="true"
+                      :width="width"
+                      :max-height="height"
+                      show-overflow-tooltip
+                    >
+                      <template v-for="item in columns(type)" :key="item.prop">
+                        <el-table-column
+                          v-if="item.prop !== 'name'"
+                          :label="item.label"
+                          :prop="item.prop"
+                          align="center"
+                          :formatter="formatter"
+                        />
+                        <el-table-column
+                          v-else
+                          :label="item.label"
+                          :prop="item.prop"
+                          align="center"
+                        >
+                          <template #default="{ row }">
+                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                              row.name
+                            }}</el-button>
+                          </template>
+                        </el-table-column>
+                      </template>
+                    </el-table>
+                    <div
+                      ref="chartRef"
+                      v-loading="chartLoading"
+                      :key="dayjs().valueOf()"
+                      v-else
+                      :style="{ width: `${width}px`, height: `${height}px` }"
+                    >
+                    </div>
+                  </div>
+                </Motion>
+              </AnimatePresence>
+            </Motion>
+          </template>
+        </el-auto-resizer>
       </div>
     </div>
   </div>
+  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" />
 </template>
 
 <style scoped>

+ 13 - 4
src/views/pms/iotrydailyreport/xjindex.vue

@@ -497,7 +497,7 @@ const selectedRowData = ref<Record<string, any> | null>(null)
 const loading = ref(true) // 列表的加载中
 const list = ref<IotRyDailyReportVO[]>([]) // 列表的数据
 const total = ref(0) // 列表的总页数
-const queryParams = reactive({
+let queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   deptId: undefined,
@@ -802,14 +802,14 @@ const cellStyle = ({
 }) => {
   // 当日油耗预警逻辑
   if (column.property === 'dailyFuel') {
-    const dailyFuel = parseFloat(row.dailyFuel) || 0;
+    const dailyFuel = parseFloat(row.dailyFuel) || 0
     if (dailyFuel > 5) {
       return {
         backgroundColor: '#e6f8ff', // 浅黄色背景
         color: '#0a35c4', // 橙色文字
         fontWeight: 'bold',
         border: '1px solid #ffd591' // 可选:添加边框突出显示
-      };
+      }
     }
   }
 
@@ -1006,9 +1006,18 @@ const handleExport = async () => {
 // 声明 ResizeObserver 实例
 let resizeObserver: ResizeObserver | null = null
 
+const route = useRoute()
+
 /** 初始化 **/
 onMounted(() => {
-  getList()
+  if (Object.keys(route.query).length > 0) {
+    queryParams = {
+      ...queryParams,
+      ...route.query,
+      deptId: Number(route.query.deptId) as any
+    }
+    handleQuery()
+  } else getList()
   // 创建 ResizeObserver 监听表格容器尺寸变化
   if (tableContainerRef.value?.$el) {
     resizeObserver = new ResizeObserver(() => {

+ 337 - 50
src/views/pms/iotrydailyreport/xsummary.vue

@@ -4,8 +4,13 @@ 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'
 
 interface Query {
   pageNo: number
@@ -29,29 +34,32 @@ const query = ref<Query>({
   projectClassification: 2
 })
 
-const totalWorkKeys = [
-  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky'],
+const totalWorkKeys: [string, string, string, string, number][] = [
+  ['totalCount', '个', '总数', 'i-tabler:report-analytics text-sky', 0],
   [
     'alreadyReported',
     '个',
     '已填报',
-    'i-material-symbols:check-circle-outline-rounded text-emerald'
+    'i-material-symbols:check-circle-outline-rounded text-emerald',
+    0
   ],
-  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose'],
+  ['notReported', '个', '未填报', 'i-material-symbols:cancel-outline-rounded text-rose', 0],
   [
     'totalFuelConsumption',
     '吨',
     '累计油耗',
-    'i-material-symbols:directions-car-outline-rounded text-sky'
+    'i-material-symbols:directions-car-outline-rounded text-sky',
+    2
   ],
   [
     'totalPowerConsumption',
     'KWH',
     '累计用电量',
-    'i-material-symbols:electric-bolt-outline-rounded text-sky'
+    'i-material-symbols:electric-bolt-outline-rounded text-sky',
+    2
   ],
-  ['constructionWells', '个', '累计施工井数', 'i-mdi:progress-wrench text-sky'],
-  ['completedWells', '个', '累计完工井数', 'i-mdi:wrench-check-outline text-emerald']
+  ['constructionWells', '个', '累计施工井数', 'i-mdi:progress-wrench text-sky', 0],
+  ['completedWells', '个', '累计完工井数', 'i-mdi:wrench-check-outline text-emerald', 0]
 ]
 
 const totalWork = ref({
@@ -106,8 +114,6 @@ interface List {
   transitTime: number | null
 }
 
-const total = ref<number>(1000)
-
 const list = ref<List[]>([])
 
 const type = ref('2')
@@ -146,20 +152,15 @@ const formatter = (row: List, column: any) => {
 const getList = useDebounceFn(async () => {
   listLoading.value = true
   try {
-    const res = (await IotRyDailyReportApi.getIotRyDailyReportSummary(query.value)) as {
-      total: number
-      list: any[]
-    }
+    const res = await IotRyDailyReportApi.getIotRyDailyReportSummary(query.value)
 
-    const { total: resTotal, list: resList } = res
+    const { list: reslist } = res
 
-    total.value = resTotal
+    type.value = reslist[0]?.type || '2'
 
-    type.value = resList[0]?.type || '2'
-
-    list.value = resList.map(
-      ({ id, projectDeptIa, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => ({
-        id: type === '2' ? projectDeptIa : teamId,
+    list.value = reslist.map(
+      ({ id, projectDeptId, projectDeptName, teamId, teamName, sort, taskId, type, ...other }) => ({
+        id: type === '2' ? projectDeptId : teamId,
         name: type === '2' ? projectDeptName : teamName,
         ...other
       })
@@ -169,6 +170,157 @@ const getList = useDebounceFn(async () => {
   }
 }, 1000)
 
+const tab = ref<'表格' | '看板'>('表格')
+
+const currentTab = ref<'表格' | '看板'>('表格')
+
+const deptName = ref('瑞恒兴域')
+
+const direction = ref<'left' | 'right'>('right')
+
+const handleSelectTab = (val: '表格' | '看板') => {
+  tab.value = val
+  direction.value = val === '看板' ? 'right' : 'left'
+  nextTick(() => {
+    currentTab.value = val
+    setTimeout(() => {
+      render()
+    })
+  })
+}
+
+const chartRef = ref<HTMLDivElement | null>(null)
+let chart: echarts.ECharts | null = null
+
+const xAxisData = ref<string[]>([])
+
+const legend = ref<string[][]>([
+  ['累计施工井数 (个)', 'cumulativeConstructWells'],
+  ['累计完工井数 (个)', 'cumulativeCompletedWells'],
+  ['累计油耗 (吨)', 'cumulativeFuelConsumption'],
+  ['累计用电量 (KWH)', 'cumulativePowerConsumption'],
+  ['平均时效 (%)', 'transitTime']
+])
+
+const chartData = ref<Record<string, number[]>>({
+  cumulativeFuelConsumption: [],
+  cumulativeConstructWells: [],
+  cumulativeCompletedWells: [],
+  cumulativePowerConsumption: [],
+  transitTime: []
+})
+
+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),
+      cumulativeConstructWells: res.map((item) => item.cumulativeConstructWells || 0),
+      cumulativeCompletedWells: res.map((item) => item.cumulativeCompletedWells || 0),
+      cumulativePowerConsumption: res.map((item) => item.cumulativePowerConsumption || 0),
+      transitTime: res.map((item) => (item.transitTime || 0) * 100)
+    }
+
+    xAxisData.value = res.map((item) => item.reportDate || '')
+  } finally {
+    chartLoading.value = false
+  }
+}, 1000)
+
+const render = () => {
+  if (!chartRef.value) return
+
+  chart = echarts.init(chartRef.value, undefined, { renderer: 'canvas' })
+
+  const values: number[] = []
+
+  for (const [_name, key] of legend.value) {
+    values.push(...(chartData.value[key] || []))
+  }
+
+  const maxVal = values.length === 0 ? 10000 : Math.max(...values)
+  const minVal = values.length === 0 ? 0 : Math.min(...values)
+
+  const maxDigits = (Math.floor(maxVal) + '').length
+  const minDigits = (Math.floor(Math.abs(minVal)) + '').length
+  const interval = Math.max(maxDigits, minDigits)
+
+  const maxInterval = interval
+  const minInterval = minDigits
+
+  const intervalArr = [0]
+  for (let i = 1; i <= interval; i++) {
+    intervalArr.push(Math.pow(10, i))
+  }
+
+  chart.setOption({
+    tooltip: {
+      trigger: 'axis',
+      axisPointer: {
+        type: 'line'
+      },
+      formatter: (params) => {
+        let d = `${params[0].axisValueLabel}<br>`
+        let item = params.map((el) => {
+          return `<div class="flex items-center justify-between mt-1 gap-1">
+            <span>${el.marker} ${el.seriesName}</span>
+            <span>${chartData.value[legend.value[el.componentIndex][1]][el.dataIndex].toFixed(2)} ${el.seriesName.split(' ')[1]}</span>
+          </div>`
+        })
+
+        return d + item.join('')
+      }
+    },
+    legend: {
+      data: legend.value.map(([name]) => name),
+      show: true
+    },
+    xAxis: {
+      type: 'category',
+      data: xAxisData.value
+    },
+    yAxis: {
+      type: 'value',
+      min: minInterval,
+      max: maxInterval,
+      interval: 1,
+      axisLabel: {
+        formatter: (v) => {
+          const num = v === 0 ? 0 : v > 0 ? Math.pow(10, v) : -Math.pow(10, -v)
+
+          return num.toLocaleString()
+        }
+      }
+    },
+    series: legend.value.map(([name, key]) => ({
+      name,
+      type: 'line',
+      smooth: true,
+      showSymbol: true,
+      data: chartData.value[key].map((value) => {
+        // return value
+        if (value === 0) return 0
+
+        const isPositive = value > 0
+        const absItem = Math.abs(value)
+
+        const min_value = Math.max(...intervalArr.filter((v) => v <= absItem))
+        const min_index = intervalArr.findIndex((v) => v === min_value)
+
+        const new_value =
+          (absItem - min_value) / (intervalArr[min_index + 1] - intervalArr[min_index]) + min_index
+
+        return isPositive ? new_value : -new_value
+      })
+    }))
+  })
+}
+
 const handleDeptNodeClick = (node: any) => {
   query.value.deptId = node.id
   handleQuery()
@@ -178,8 +330,11 @@ const handleQuery = (setPage = true) => {
   if (setPage) {
     query.value.pageNo = 1
   }
-  getTotal()
+  getChart().then(() => {
+    render()
+  })
   getList()
+  getTotal()
 }
 
 const resetQuery = () => {
@@ -201,6 +356,73 @@ watch(
 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) => {
+  const { pageNo, pageSize, ...rest } = query.value
+
+  router.push({
+    path: '/iotdayilyreport/IotRyXjDailyReport',
+    query: {
+      ...rest,
+      deptId: id
+    }
+  })
+}
 </script>
 
 <template>
@@ -258,43 +480,108 @@ onMounted(() => {
           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]]">
+          <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-xs font-medium text-[var(--el-text-color-regular)]">{{ info[1] }}</div>
           <div class="text-sm font-medium text-[var(--el-text-color-regular)]">{{ info[2] }}</div>
         </div>
       </div>
-      <div class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow pt-4 px-8">
-        <el-table
-          v-loading="listLoading"
-          :data="list"
-          :stripe="true"
-          :style="{ width: '100%' }"
-          max-height="600"
-          class="min-h-143"
-          show-overflow-tooltip
-        >
-          <el-table-column
-            v-for="item in columns(type)"
-            :key="item.prop"
-            :label="item.label"
-            :prop="item.prop"
-            align="center"
-            :formatter="formatter"
-          />
-        </el-table>
-
-        <!-- <Pagination
-          class="mt-8"
-          :total="total"
-          v-model:page="query.pageNo"
-          v-model:limit="query.pageSize"
-          @pagination="getList"
-        /> -->
+      <div
+        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 py-4 grid grid-rows-[48px_1fr] gap-2"
+      >
+        <div class="flex items-center justify-between">
+          <el-button-group>
+            <el-button
+              size="default"
+              :type="tab === '表格' ? 'primary' : 'default'"
+              @click="handleSelectTab('表格')"
+              >表格
+            </el-button>
+            <el-button
+              size="default"
+              :type="tab === '看板' ? 'primary' : 'default'"
+              @click="handleSelectTab('看板')"
+              >看板
+            </el-button>
+          </el-button-group>
+          <h3 class="text-xl font-medium">{{ `${deptName}-${tab}` }}</h3>
+          <el-button size="default" type="primary" @click="exportAll">导出</el-button>
+        </div>
+        <el-auto-resizer>
+          <template #default="{ height, width }">
+            <Motion
+              as="div"
+              :style="{ position: 'relative', overflow: 'hidden' }"
+              :animate="{ height: `${height}px`, width: `${width}px` }"
+              :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: `${width}px`, height: `${height}px` }">
+                    <el-table
+                      v-if="currentTab === '表格'"
+                      v-loading="listLoading"
+                      :data="list"
+                      :stripe="true"
+                      :width="width"
+                      :max-height="height"
+                      show-overflow-tooltip
+                    >
+                      <template v-for="item in columns(type)" :key="item.prop">
+                        <el-table-column
+                          v-if="item.prop !== 'name'"
+                          :label="item.label"
+                          :prop="item.prop"
+                          align="center"
+                          :formatter="formatter"
+                        />
+                        <el-table-column
+                          v-else
+                          :label="item.label"
+                          :prop="item.prop"
+                          align="center"
+                        >
+                          <template #default="{ row }">
+                            <el-button text type="primary" @click.prevent="tolist(row.id)">{{
+                              row.name
+                            }}</el-button>
+                          </template>
+                        </el-table-column>
+                      </template>
+                    </el-table>
+                    <div
+                      ref="chartRef"
+                      v-loading="chartLoading"
+                      :key="dayjs().valueOf()"
+                      v-else
+                      :style="{ width: `${width}px`, height: `${height}px` }"
+                    >
+                    </div>
+                  </div>
+                </Motion>
+              </AnimatePresence>
+            </Motion>
+          </template>
+        </el-auto-resizer>
       </div>
     </div>
   </div>
+  <UnfilledReportDialog ref="unfilledDialogRef" :query-params="query" />
 </template>
 
 <style scoped>