Просмотр исходного кода

!764 针对 iothome 页面上次codereview的优化
Merge pull request !764 from alwayssuper/feature/iot

芋道源码 11 месяцев назад
Родитель
Сommit
108782ba80

+ 0 - 19
pnpm-lock.yaml

@@ -1397,42 +1397,36 @@ packages:
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-arm-musl@2.5.0':
     resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==, tarball: https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz}
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-linux-arm64-glibc@2.5.0':
     resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==, tarball: https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-arm64-musl@2.5.0':
     resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==, tarball: https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-linux-x64-glibc@2.5.0':
     resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==, tarball: https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@parcel/watcher-linux-x64-musl@2.5.0':
     resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==, tarball: https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@parcel/watcher-win32-arm64@2.5.0':
     resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==, tarball: https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz}
@@ -1536,55 +1530,46 @@ packages:
     resolution: {integrity: sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz}
     cpu: [arm]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm-musleabihf@4.27.4':
     resolution: {integrity: sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz}
     cpu: [arm]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-arm64-gnu@4.27.4':
     resolution: {integrity: sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-arm64-musl@4.27.4':
     resolution: {integrity: sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-linux-powerpc64le-gnu@4.27.4':
     resolution: {integrity: sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz}
     cpu: [ppc64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-riscv64-gnu@4.27.4':
     resolution: {integrity: sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz}
     cpu: [riscv64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-s390x-gnu@4.27.4':
     resolution: {integrity: sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz}
     cpu: [s390x]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-gnu@4.27.4':
     resolution: {integrity: sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@rollup/rollup-linux-x64-musl@4.27.4':
     resolution: {integrity: sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@rollup/rollup-win32-arm64-msvc@4.27.4':
     resolution: {integrity: sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==, tarball: https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz}
@@ -1630,28 +1615,24 @@ packages:
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
-    libc: [glibc]
 
   '@swc/core-linux-arm64-musl@1.9.3':
     resolution: {integrity: sha512-tzVH480RY6RbMl/QRgh5HK3zn1ZTFsThuxDGo6Iuk1MdwIbdFYUY034heWUTI4u3Db97ArKh0hNL0xhO3+PZdg==, tarball: https://registry.npmmirror.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.9.3.tgz}
     engines: {node: '>=10'}
     cpu: [arm64]
     os: [linux]
-    libc: [musl]
 
   '@swc/core-linux-x64-gnu@1.9.3':
     resolution: {integrity: sha512-ivXXBRDXDc9k4cdv10R21ccBmGebVOwKXT/UdH1PhxUn9m/h8erAWjz5pcELwjiMf27WokqPgaWVfaclDbgE+w==, tarball: https://registry.npmmirror.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.9.3.tgz}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
-    libc: [glibc]
 
   '@swc/core-linux-x64-musl@1.9.3':
     resolution: {integrity: sha512-ILsGMgfnOz1HwdDz+ZgEuomIwkP1PHT6maigZxaCIuC6OPEhKE8uYna22uU63XvYcLQvZYDzpR3ms47WQPuNEg==, tarball: https://registry.npmmirror.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.9.3.tgz}
     engines: {node: '>=10'}
     cpu: [x64]
     os: [linux]
-    libc: [musl]
 
   '@swc/core-win32-arm64-msvc@1.9.3':
     resolution: {integrity: sha512-e+XmltDVIHieUnNJHtspn6B+PCcFOMYXNJB1GqoCcyinkEIQNwC8KtWgMqUucUbEWJkPc35NHy9k8aCXRmw9Kg==, tarball: https://registry.npmmirror.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.9.3.tgz}

+ 8 - 2
src/api/iot/statistics/index.ts

@@ -16,10 +16,16 @@ export interface IotStatisticsSummaryRespVO {
   productCategoryDeviceCounts: Record<string, number>
 }
 
+/** 时间戳-数值的键值对类型 */
+interface TimeValueItem {
+  [key: string]: number
+}
+
 /** IoT 消息统计数据类型 */
 export interface IotStatisticsDeviceMessageSummaryRespVO {
-  upstreamCounts: Record<number, number>
-  downstreamCounts: Record<number, number>
+  statType: number
+  upstreamCounts: TimeValueItem[]
+  downstreamCounts: TimeValueItem[]
 }
 
 // IoT 数据统计 API

+ 27 - 0
src/utils/formatTime.ts

@@ -330,3 +330,30 @@ export function getDateRange(
     dayjs(endDate).endOf('d').format('YYYY-MM-DD HH:mm:ss')
   ]
 }
+
+/**
+ * 获取指定小时前的时间戳
+ * @param hours 小时数
+ * @returns 返回指定小时前的时间戳(毫秒)
+ */
+export function getHoursAgo(hours: number): number {
+  return dayjs().subtract(hours, 'hour').valueOf()
+}
+
+/**
+ * 获取标准时间范围的时间戳
+ * @param range 时间范围,支持 '8h' | '24h' | '7d'
+ * @returns 返回开始时间戳(毫秒)
+ */
+export function getTimeRangeStart(range: '8h' | '24h' | '7d'): number {
+  switch (range) {
+    case '8h':
+      return getHoursAgo(8)
+    case '24h':
+      return getHoursAgo(24)
+    case '7d':
+      return dayjs().subtract(7, 'day').valueOf()
+    default:
+      return dayjs().valueOf()
+  }
+}

+ 50 - 0
src/views/iot/home/components/ComparisonCard.vue

@@ -0,0 +1,50 @@
+<template>
+  <el-card class="stat-card" shadow="never" :loading="loading">
+    <div class="flex flex-col">
+      <div class="flex justify-between items-center mb-1">
+        <span class="text-gray-500 text-base font-medium">{{ title }}</span>
+        <Icon :icon="icon" :class="`text-[32px] ${iconColor}`" />
+      </div>
+      <span class="text-3xl font-bold text-gray-700">
+        <span v-if="value === -1">--</span>
+        <span v-else>{{ value }}</span>
+      </span>
+      <el-divider class="my-2" />
+      <div class="flex justify-between items-center text-gray-400 text-sm">
+        <span>今日新增</span>
+        <span class="text-green-500" v-if="todayCount !== -1">+{{ todayCount }}</span>
+        <span v-else>--</span>
+      </div>
+    </div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+/** 统计卡片组件 */
+defineOptions({ name: 'ComparisonCard' })
+
+const props = defineProps({
+  title: propTypes.string.def('').isRequired,
+  value: propTypes.number.def(0).isRequired,
+  todayCount: propTypes.number.def(0).isRequired,
+  icon: propTypes.string.def('').isRequired,
+  iconColor: propTypes.string.def(''),
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.stat-card {
+  transition: all 0.3s;
+
+  &:hover {
+    transform: translateY(-5px);
+    box-shadow: 0 5px 15px rgb(0 0 0 / 8%);
+  }
+}
+</style>

+ 127 - 0
src/views/iot/home/components/DeviceCountCard.vue

@@ -0,0 +1,127 @@
+<template>
+  <el-card class="chart-card" shadow="never" :loading="loading">
+    <template #header>
+      <div class="flex items-center">
+        <span class="text-base font-medium text-gray-600">设备数量统计</span>
+      </div>
+    </template>
+    <div v-if="loading && !hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="加载中..." />
+    </div>
+    <div v-else-if="!hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="暂无数据" />
+    </div>
+    <div v-else ref="deviceCountChartRef" class="h-[240px]"></div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { PieChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { TooltipComponent, LegendComponent } from 'echarts/components'
+import { LabelLayout } from 'echarts/features'
+import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
+import type { PropType } from 'vue'
+
+/** 设备数量统计卡片 */
+defineOptions({ name: 'DeviceCountCard' })
+
+const props = defineProps({
+  statsData: {
+    type: Object as PropType<IotStatisticsSummaryRespVO>,
+    required: true
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const deviceCountChartRef = ref()
+
+// 是否有数据
+const hasData = computed(() => {
+  if (!props.statsData) return false
+  
+  const categories = Object.entries(props.statsData.productCategoryDeviceCounts || {})
+  return categories.length > 0 && props.statsData.deviceCount !== -1
+})
+
+// 初始化图表
+const initChart = () => {
+  // 如果没有数据,则不初始化图表
+  if (!hasData.value) return
+  
+  // 确保 DOM 元素存在且已渲染
+  if (!deviceCountChartRef.value) {
+    console.warn('图表DOM元素不存在')
+    return
+  }
+  
+  echarts.use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer, LabelLayout])
+  
+  try {
+    const chart = echarts.init(deviceCountChartRef.value)
+    chart.setOption({
+      tooltip: {
+        trigger: 'item'
+      },
+      legend: {
+        top: '5%',
+        right: '10%',
+        align: 'left',
+        orient: 'vertical',
+        icon: 'circle'
+      },
+      series: [
+        {
+          name: 'Access From',
+          type: 'pie',
+          radius: ['50%', '80%'],
+          avoidLabelOverlap: false,
+          center: ['30%', '50%'],
+          label: {
+            show: false,
+            position: 'outside'
+          },
+          emphasis: {
+            label: {
+              show: true,
+              fontSize: 20,
+              fontWeight: 'bold'
+            }
+          },
+          labelLine: {
+            show: false
+          },
+          data: Object.entries(props.statsData.productCategoryDeviceCounts).map(([name, value]) => ({
+            name,
+            value
+          }))
+        }
+      ]
+    })
+    return chart
+  } catch (error) {
+    console.error('初始化图表失败:', error)
+    return null
+  }
+}
+
+// 监听数据变化
+watch(() => props.statsData, () => {
+  // 使用 nextTick 确保 DOM 已更新
+  nextTick(() => {
+    initChart()
+  })
+}, { deep: true })
+
+// 组件挂载时初始化图表
+onMounted(() => {
+  // 使用 nextTick 确保 DOM 已更新
+  nextTick(() => {
+    initChart()
+  })
+})
+</script>

+ 162 - 0
src/views/iot/home/components/DeviceStateCountCard.vue

@@ -0,0 +1,162 @@
+<template>
+  <el-card class="chart-card" shadow="never" :loading="loading">
+    <template #header>
+      <div class="flex items-center">
+        <span class="text-base font-medium text-gray-600">设备状态统计</span>
+      </div>
+    </template>
+    <div v-if="loading && !hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="加载中..." />
+    </div>
+    <div v-else-if="!hasData" class="h-[240px] flex justify-center items-center">
+      <el-empty description="暂无数据" />
+    </div>
+    <el-row v-else class="h-[240px]">
+      <el-col :span="8" class="flex flex-col items-center">
+        <div ref="deviceOnlineCountChartRef" class="h-[160px] w-full"></div>
+        <div class="text-center mt-2">
+          <span class="text-sm text-gray-600">在线设备</span>
+        </div>
+      </el-col>
+      <el-col :span="8" class="flex flex-col items-center">
+        <div ref="deviceOfflineChartRef" class="h-[160px] w-full"></div>
+        <div class="text-center mt-2">
+          <span class="text-sm text-gray-600">离线设备</span>
+        </div>
+      </el-col>
+      <el-col :span="8" class="flex flex-col items-center">
+        <div ref="deviceActiveChartRef" class="h-[160px] w-full"></div>
+        <div class="text-center mt-2">
+          <span class="text-sm text-gray-600">待激活设备</span>
+        </div>
+      </el-col>
+    </el-row>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { GaugeChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
+import type { PropType } from 'vue'
+
+/** 设备状态统计卡片 */
+defineOptions({ name: 'DeviceStateCountCard' })
+
+const props = defineProps({
+  statsData: {
+    type: Object as PropType<IotStatisticsSummaryRespVO>,
+    required: true
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const deviceOnlineCountChartRef = ref()
+const deviceOfflineChartRef = ref()
+const deviceActiveChartRef = ref()
+
+// 是否有数据
+const hasData = computed(() => {
+  if (!props.statsData) return false
+  return props.statsData.deviceCount !== -1
+})
+
+// 初始化仪表盘图表
+const initGaugeChart = (el: any, value: number, color: string) => {
+  // 确保 DOM 元素存在且已渲染
+  if (!el) {
+    console.warn('图表DOM元素不存在')
+    return
+  }
+  
+  echarts.use([GaugeChart, CanvasRenderer])
+  
+  try {
+    const chart = echarts.init(el)
+    chart.setOption({
+      series: [
+        {
+          type: 'gauge',
+          startAngle: 360,
+          endAngle: 0,
+          min: 0,
+          max: props.statsData.deviceCount || 100, // 使用设备总数作为最大值
+          progress: {
+            show: true,
+            width: 12,
+            itemStyle: {
+              color: color
+            }
+          },
+          axisLine: {
+            lineStyle: {
+              width: 12,
+              color: [[1, '#E5E7EB']]
+            }
+          },
+          axisTick: { show: false },
+          splitLine: { show: false },
+          axisLabel: { show: false },
+          pointer: { show: false },
+          anchor: { show: false },
+          title: { show: false },
+          detail: {
+            valueAnimation: true,
+            fontSize: 24,
+            fontWeight: 'bold',
+            fontFamily: 'Inter, sans-serif',
+            color: color,
+            offsetCenter: [0, '0'],
+            formatter: (value: number) => {
+              return `${value} 个`
+            }
+          },
+          data: [{ value: value }]
+        }
+      ]
+    })
+    return chart
+  } catch (error) {
+    console.error('初始化图表失败:', error)
+    return null
+  }
+}
+
+// 初始化所有图表
+const initCharts = () => {
+  // 如果没有数据,则不初始化图表
+  if (!hasData.value) return
+  
+  // 使用 nextTick 确保 DOM 已更新
+  nextTick(() => {
+    // 在线设备统计
+    if (deviceOnlineCountChartRef.value) {
+      initGaugeChart(deviceOnlineCountChartRef.value, props.statsData.deviceOnlineCount, '#0d9')
+    }
+    
+    // 离线设备统计
+    if (deviceOfflineChartRef.value) {
+      initGaugeChart(deviceOfflineChartRef.value, props.statsData.deviceOfflineCount, '#f50')
+    }
+    
+    // 待激活设备统计
+    if (deviceActiveChartRef.value) {
+      initGaugeChart(deviceActiveChartRef.value, props.statsData.deviceInactiveCount, '#05b')
+    }
+  })
+}
+
+// 监听数据变化
+watch(() => props.statsData, () => {
+  initCharts()
+}, { deep: true })
+
+// 组件挂载时初始化图表
+onMounted(() => {
+  initCharts()
+})
+</script>

+ 325 - 0
src/views/iot/home/components/MessageTrendCard.vue

@@ -0,0 +1,325 @@
+<template>
+  <el-card class="chart-card" shadow="never" :loading="loading">
+    <template #header>
+      <div class="flex items-center justify-between">
+        <span class="text-base font-medium text-gray-600">
+          上下行消息量统计
+          <span class="text-sm text-gray-400 ml-2">
+            {{ props.messageStats.statType === 1 ? '(按天)' : '(按小时)' }}
+          </span>
+        </span>
+        <div class="flex items-center space-x-2">
+          <el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
+            <el-radio-button label="8h">最近8小时</el-radio-button>
+            <el-radio-button label="24h">最近24小时</el-radio-button>
+            <el-radio-button label="7d">近一周</el-radio-button>
+          </el-radio-group>
+          <el-date-picker
+            v-model="dateRange"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+            @change="handleDateRangeChange"
+          />
+        </div>
+      </div>
+    </template>
+    <div v-if="loading && !hasData" class="h-[300px] flex justify-center items-center">
+      <el-empty description="加载中..." />
+    </div>
+    <div v-else-if="!hasData" class="h-[300px] flex justify-center items-center">
+      <el-empty description="暂无数据" />
+    </div>
+    <div v-else ref="messageChartRef" class="h-[300px]"></div>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { LineChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
+import { UniversalTransition } from 'echarts/features'
+import { IotStatisticsDeviceMessageSummaryRespVO } from '@/api/iot/statistics'
+import { formatDate, getTimeRangeStart } from '@/utils/formatTime'
+import type { PropType } from 'vue'
+import dayjs from 'dayjs'
+
+/** 消息趋势统计卡片 */
+defineOptions({ name: 'MessageTrendCard' })
+
+const props = defineProps({
+  messageStats: {
+    type: Object as PropType<IotStatisticsDeviceMessageSummaryRespVO>,
+    required: true
+  },
+  loading: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits(['timeRangeChange'])
+
+const timeRange = ref('7d')
+const dateRange = ref<any>(null)
+const messageChartRef = ref()
+
+// 是否有数据
+const hasData = computed(() => {
+  if (!props.messageStats) return false
+  
+  const upstreamCounts = Array.isArray(props.messageStats.upstreamCounts) 
+    ? props.messageStats.upstreamCounts 
+    : []
+  
+  const downstreamCounts = Array.isArray(props.messageStats.downstreamCounts) 
+    ? props.messageStats.downstreamCounts 
+    : []
+    
+  return upstreamCounts.length > 0 || downstreamCounts.length > 0
+})
+// TODO @super:这个的计算,看看能不能结合 dayjs 简化。因为 1h、24h、7d 感觉是比较标准的。如果没有,抽到 utils/formatTime.ts 作为一个工具方法
+// 处理快捷时间范围选择
+const handleTimeRangeChange = (range: string) => {
+  const now = dayjs().valueOf()
+  const startTime = getTimeRangeStart(range as '8h' | '24h' | '7d')
+
+  dateRange.value = null
+  emit('timeRangeChange', { startTime, endTime: now })
+}
+
+// 处理自定义日期范围选择
+const handleDateRangeChange = (value: [Date, Date] | null) => {
+  if (value) {
+    timeRange.value = ''
+    emit('timeRangeChange', {
+      startTime: value[0].getTime(),
+      endTime: value[1].getTime()
+    })
+  }
+}
+
+// 初始化图表
+const initChart = () => {
+  echarts.use([
+    LineChart,
+    CanvasRenderer,
+    GridComponent,
+    LegendComponent,
+    TooltipComponent,
+    UniversalTransition
+  ])
+
+  // 检查是否有数据可以绘制
+  if (!hasData.value) return
+  
+  // 确保 DOM 元素存在且已渲染
+  if (!messageChartRef.value) {
+    console.warn('图表DOM元素不存在')
+    return
+  }
+
+
+  // 检查数据格式并转换
+  const upstreamCounts = Array.isArray(props.messageStats.upstreamCounts) 
+    ? props.messageStats.upstreamCounts 
+    : Object.entries(props.messageStats.upstreamCounts || {}).map(([key, value]) => ({ [key]: value }))
+  
+  const downstreamCounts = Array.isArray(props.messageStats.downstreamCounts) 
+    ? props.messageStats.downstreamCounts 
+    : Object.entries(props.messageStats.downstreamCounts || {}).map(([key, value]) => ({ [key]: value }))
+
+  // 获取所有时间戳并排序
+  let timestamps: number[] = []
+  
+  try {
+    // 尝试从数组中提取时间戳
+    if (Array.isArray(upstreamCounts) && upstreamCounts.length > 0) {
+      timestamps = Array.from(
+        new Set([
+          ...upstreamCounts.map(item => Number(Object.keys(item)[0])),
+          ...downstreamCounts.map(item => Number(Object.keys(item)[0]))
+        ])
+      ).sort((a, b) => a - b)
+    } else {
+      // 如果数组为空或不是数组,尝试从对象中提取时间戳
+      const upKeys = Object.keys(props.messageStats.upstreamCounts || {}).map(Number)
+      const downKeys = Object.keys(props.messageStats.downstreamCounts || {}).map(Number)
+      timestamps = Array.from(new Set([...upKeys, ...downKeys])).sort((a, b) => a - b)
+    }
+  } catch (error) {
+    console.error('提取时间戳出错:', error)
+    timestamps = []
+  }
+
+  console.log('时间戳:', timestamps)
+
+  // 准备数据 - 根据 statType 确定时间格式
+  const xdata = timestamps.map((ts) => {
+    // 根据 statType 选择合适的格式
+    if (props.messageStats.statType === 1) {
+      // 日级别统计 - 使用 YYYY-MM-DD 格式
+      return formatDate(dayjs(ts).toDate(), 'YYYY-MM-DD')
+    } else {
+      // 小时级别统计 - 使用 YYYY-MM-DD HH:mm 格式
+      return formatDate(dayjs(ts).toDate(), 'YYYY-MM-DD HH:mm')
+    }
+  })
+  
+  let upData: number[] = []
+  let downData: number[] = []
+  
+  try {
+    // 尝试从数组中提取数据
+    if (Array.isArray(upstreamCounts) && upstreamCounts.length > 0) {
+      upData = timestamps.map((ts) => {
+        const item = upstreamCounts.find(count => 
+          Number(Object.keys(count)[0]) === ts
+        )
+        return item ? Number(Object.values(item)[0]) : 0
+      })
+      
+      downData = timestamps.map((ts) => {
+        const item = downstreamCounts.find(count => 
+          Number(Object.keys(count)[0]) === ts
+        )
+        return item ? Number(Object.values(item)[0]) : 0
+      })
+    } else {
+      // 如果数组为空或不是数组,尝试从对象中提取数据
+      const upstreamObj = props.messageStats.upstreamCounts || {}
+      const downstreamObj = props.messageStats.downstreamCounts || {}
+      upData = timestamps.map((ts) => Number(upstreamObj[ts as keyof typeof upstreamObj] || 0))
+      downData = timestamps.map((ts) => Number(downstreamObj[ts as keyof typeof downstreamObj] || 0))
+    }
+  } catch (error) {
+    console.error('提取数据出错:', error)
+    upData = []
+    downData = []
+  }
+
+
+  // 配置图表
+  try {
+    const chart = echarts.init(messageChartRef.value)
+    
+    chart.setOption({
+      tooltip: {
+        trigger: 'axis',
+        backgroundColor: 'rgba(255, 255, 255, 0.9)',
+        borderColor: '#E5E7EB',
+        textStyle: {
+          color: '#374151'
+        }
+      },
+      legend: {
+        data: ['上行消息量', '下行消息量'],
+        textStyle: {
+          color: '#374151',
+          fontWeight: 500
+        }
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        boundaryGap: false,
+        data: xdata,
+        axisLine: {
+          lineStyle: {
+            color: '#E5E7EB'
+          }
+        },
+        axisLabel: {
+          color: '#6B7280'
+        }
+      },
+      yAxis: {
+        type: 'value',
+        axisLine: {
+          lineStyle: {
+            color: '#E5E7EB'
+          }
+        },
+        axisLabel: {
+          color: '#6B7280'
+        },
+        splitLine: {
+          lineStyle: {
+            color: '#F3F4F6'
+          }
+        }
+      },
+      series: [
+        {
+          name: '上行消息量',
+          type: 'line',
+          smooth: true,
+          data: upData,
+          itemStyle: {
+            color: '#3B82F6'
+          },
+          lineStyle: {
+            width: 2
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
+              { offset: 1, color: 'rgba(59, 130, 246, 0)' }
+            ])
+          }
+        },
+        {
+          name: '下行消息量',
+          type: 'line',
+          smooth: true,
+          data: downData,
+          itemStyle: {
+            color: '#10B981'
+          },
+          lineStyle: {
+            width: 2
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
+              { offset: 1, color: 'rgba(16, 185, 129, 0)' }
+            ])
+          }
+        }
+      ]
+    })
+    return chart
+  } catch (error) {
+    console.error('初始化图表失败:', error)
+    return null
+  }
+}
+
+// 监听数据变化
+watch(
+  () => props.messageStats,
+  () => {
+    // 使用 nextTick 确保 DOM 已更新
+    nextTick(() => {
+      initChart()
+    })
+  },
+  { deep: true }
+)
+
+// 组件挂载时初始化图表
+onMounted(() => {
+  // 使用 nextTick 确保 DOM 已更新
+  nextTick(() => {
+    initChart()
+  })
+})
+</script>

+ 76 - 436
src/views/iot/home/index.vue

@@ -2,145 +2,65 @@
   <!-- 第一行:统计卡片行 -->
   <el-row :gutter="16" class="mb-4">
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">分类数量</span>
-            <Icon icon="ep:menu" class="text-[32px] text-blue-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">
-            {{ statsData.productCategoryCount }}
-          </span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.productCategoryTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="分类数量"
+        :value="statsData.productCategoryCount"
+        :todayCount="statsData.productCategoryTodayCount"
+        icon="ep:menu"
+        iconColor="text-blue-400"
+        :loading="loading"
+      />
     </el-col>
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">产品数量</span>
-            <Icon icon="ep:box" class="text-[32px] text-orange-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">{{ statsData.productCount }}</span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.productTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="产品数量"
+        :value="statsData.productCount"
+        :todayCount="statsData.productTodayCount"
+        icon="ep:box"
+        iconColor="text-orange-400"
+        :loading="loading"
+      />
     </el-col>
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">设备数量</span>
-            <Icon icon="ep:cpu" class="text-[32px] text-purple-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">{{ statsData.deviceCount }}</span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.deviceTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="设备数量"
+        :value="statsData.deviceCount"
+        :todayCount="statsData.deviceTodayCount"
+        icon="ep:cpu"
+        iconColor="text-purple-400"
+        :loading="loading"
+      />
     </el-col>
     <el-col :span="6">
-      <el-card class="stat-card" shadow="never">
-        <div class="flex flex-col">
-          <div class="flex justify-between items-center mb-1">
-            <span class="text-gray-500 text-base font-medium">设备消息数</span>
-            <Icon icon="ep:message" class="text-[32px] text-teal-400" />
-          </div>
-          <span class="text-3xl font-bold text-gray-700">
-            {{ statsData.deviceMessageCount }}
-          </span>
-          <el-divider class="my-2" />
-          <div class="flex justify-between items-center text-gray-400 text-sm">
-            <span>今日新增</span>
-            <span class="text-green-500">+{{ statsData.deviceMessageTodayCount }}</span>
-          </div>
-        </div>
-      </el-card>
+      <ComparisonCard
+        title="设备消息数"
+        :value="statsData.deviceMessageCount"
+        :todayCount="statsData.deviceMessageTodayCount"
+        icon="ep:message"
+        iconColor="text-teal-400"
+        :loading="loading"
+      />
     </el-col>
   </el-row>
 
   <!-- 第二行:图表行 -->
   <el-row :gutter="16" class="mb-4">
     <el-col :span="12">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center">
-            <span class="text-base font-medium text-gray-600">设备数量统计</span>
-          </div>
-        </template>
-        <div ref="deviceCountChartRef" class="h-[240px]"></div>
-      </el-card>
+      <DeviceCountCard :statsData="statsData" :loading="loading" />
     </el-col>
     <el-col :span="12">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center">
-            <span class="text-base font-medium text-gray-600">设备状态统计</span>
-          </div>
-        </template>
-        <el-row class="h-[240px]">
-          <el-col :span="8" class="flex flex-col items-center">
-            <div ref="deviceOnlineCountChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">在线设备</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="flex flex-col items-center">
-            <div ref="deviceOfflineChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">离线设备</span>
-            </div>
-          </el-col>
-          <el-col :span="8" class="flex flex-col items-center">
-            <div ref="deviceActiveChartRef" class="h-[160px] w-full"></div>
-            <div class="text-center mt-2">
-              <span class="text-sm text-gray-600">待激活设备</span>
-            </div>
-          </el-col>
-        </el-row>
-      </el-card>
+      <DeviceStateCountCard :statsData="statsData" :loading="loading" />
     </el-col>
   </el-row>
 
   <!-- 第三行:消息统计行 -->
   <el-row>
     <el-col :span="24">
-      <el-card class="chart-card" shadow="never">
-        <template #header>
-          <div class="flex items-center justify-between">
-            <span class="text-base font-medium text-gray-600">上下行消息量统计</span>
-            <div class="flex items-center space-x-2">
-              <el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
-                <el-radio-button label="1h">最近1小时</el-radio-button>
-                <el-radio-button label="24h">最近24小时</el-radio-button>
-                <el-radio-button label="7d">近一周</el-radio-button>
-              </el-radio-group>
-              <el-date-picker
-                v-model="dateRange"
-                type="datetimerange"
-                range-separator="至"
-                start-placeholder="开始时间"
-                end-placeholder="结束时间"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-                @change="handleDateRangeChange"
-              />
-            </div>
-          </div>
-        </template>
-        <div ref="deviceMessageCountChartRef" class="h-[300px]"></div>
-      </el-card>
+      <MessageTrendCard 
+        :messageStats="messageStats"
+        @time-range-change="handleTimeRangeChange"
+        :loading="loading"
+      />
     </el-col>
   </el-row>
 
@@ -148,356 +68,76 @@
 </template>
 
 <script setup lang="ts" name="Index">
-import * as echarts from 'echarts/core'
-import {
-  GridComponent,
-  LegendComponent,
-  TitleComponent,
-  ToolboxComponent,
-  TooltipComponent
-} from 'echarts/components'
-import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
-import { LabelLayout, UniversalTransition } from 'echarts/features'
-import { CanvasRenderer } from 'echarts/renderers'
 import {
   IotStatisticsDeviceMessageSummaryRespVO,
   IotStatisticsSummaryRespVO,
   ProductCategoryApi
 } from '@/api/iot/statistics'
-import { formatDate } from '@/utils/formatTime'
-
-// TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
+import { getHoursAgo } from '@/utils/formatTime'
+import ComparisonCard from './components/ComparisonCard.vue'
+import DeviceCountCard from './components/DeviceCountCard.vue'
+import DeviceStateCountCard from './components/DeviceStateCountCard.vue'
+import MessageTrendCard from './components/MessageTrendCard.vue'
 
 /** IoT 首页 */
 defineOptions({ name: 'IoTHome' })
 
 // TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
-echarts.use([
-  TooltipComponent,
-  LegendComponent,
-  PieChart,
-  CanvasRenderer,
-  LabelLayout,
-  TitleComponent,
-  ToolboxComponent,
-  GridComponent,
-  LineChart,
-  UniversalTransition,
-  GaugeChart
-])
 
-const timeRange = ref('7d') // 修改默认选择为近一周
-const dateRange = ref<[Date, Date] | null>(null)
 
 const queryParams = reactive({
-  startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
+  startTime: getHoursAgo( 7 * 24 ), // 设置默认开始时间为 7 天前
   endTime: Date.now() // 设置默认结束时间为当前时间
 })
 
-const deviceCountChartRef = ref() // 设备数量统计的图表
-const deviceOnlineCountChartRef = ref() // 在线设备统计的图表
-const deviceOfflineChartRef = ref() // 离线设备统计的图表
-const deviceActiveChartRef = ref() // 待激活设备统计的图表
-const deviceMessageCountChartRef = ref() // 上下行消息量统计的图表
 
 // 基础统计数据
 // TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
 const statsData = ref<IotStatisticsSummaryRespVO>({
-  productCategoryCount: 0,
-  productCount: 0,
-  deviceCount: 0,
-  deviceMessageCount: 0,
-  productCategoryTodayCount: 0,
-  productTodayCount: 0,
-  deviceTodayCount: 0,
-  deviceMessageTodayCount: 0,
-  deviceOnlineCount: 0,
-  deviceOfflineCount: 0,
-  deviceInactiveCount: 0,
+  productCategoryCount: -1,
+  productCount: -1,
+  deviceCount: -1,
+  deviceMessageCount: -1,
+  productCategoryTodayCount: -1,
+  productTodayCount: -1,
+  deviceTodayCount: -1,
+  deviceMessageTodayCount: -1,
+  deviceOnlineCount: -1,
+  deviceOfflineCount: -1,
+  deviceInactiveCount: -1,
   productCategoryDeviceCounts: {}
 })
 
 // 消息统计数据
 const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
-  upstreamCounts: {},
-  downstreamCounts: {}
+  statType: 0,
+  upstreamCounts: [],
+  downstreamCounts: []
 })
 
-/** 处理快捷时间范围选择 */
-const handleTimeRangeChange = (timeRange: string) => {
-  const now = Date.now()
-  let startTime: number
-
-  // TODO @super:这个的计算,看看能不能结合 dayjs 简化。因为 1h、24h、7d 感觉是比较标准的。如果没有,抽到 utils/formatTime.ts 作为一个工具方法
-  switch (timeRange) {
-    case '1h':
-      startTime = now - 60 * 60 * 1000
-      break
-    case '24h':
-      startTime = now - 24 * 60 * 60 * 1000
-      break
-    case '7d':
-      startTime = now - 7 * 24 * 60 * 60 * 1000
-      break
-    default:
-      return
-  }
-
-  // 清空日期选择器
-  dateRange.value = null
-
-  // 更新查询参数
-  queryParams.startTime = startTime
-  queryParams.endTime = now
+// 加载状态
+const loading = ref(true)
 
-  // 重新获取数据
+/** 处理时间范围变化 */
+const handleTimeRangeChange = (params: { startTime: number; endTime: number }) => {
+  queryParams.startTime = params.startTime
+  queryParams.endTime = params.endTime
   getStats()
 }
 
-/** 处理自定义日期范围选择 */
-const handleDateRangeChange = (value: [Date, Date] | null) => {
-  if (value) {
-    // 清空快捷选项
-    timeRange.value = ''
-
-    // 更新查询参数
-    queryParams.startTime = value[0].getTime()
-    queryParams.endTime = value[1].getTime()
-
-    // 重新获取数据
-    getStats()
-  }
-}
-
 /** 获取统计数据 */
 const getStats = async () => {
-  // 获取基础统计数据
-  statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
-
-  // 获取消息统计数据
-  messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
-
-  // 初始化图表
-  initCharts()
-}
-
-/** 初始化图表 */
-const initCharts = () => {
-  // 设备数量统计
-  echarts.init(deviceCountChartRef.value).setOption({
-    tooltip: {
-      trigger: 'item'
-    },
-    legend: {
-      top: '5%',
-      right: '10%',
-      align: 'left',
-      orient: 'vertical',
-      icon: 'circle'
-    },
-    series: [
-      {
-        name: 'Access From',
-        type: 'pie',
-        radius: ['50%', '80%'],
-        avoidLabelOverlap: false,
-        center: ['30%', '50%'],
-        label: {
-          show: false,
-          position: 'outside'
-        },
-        emphasis: {
-          label: {
-            show: true,
-            fontSize: 20,
-            fontWeight: 'bold'
-          }
-        },
-        labelLine: {
-          show: false
-        },
-        data: Object.entries(statsData.value.productCategoryDeviceCounts).map(([name, value]) => ({
-          name,
-          value
-        }))
-      }
-    ]
-  })
-
-  // 在线设备统计
-  initGaugeChart(deviceOnlineCountChartRef.value, statsData.value.deviceOnlineCount, '#0d9')
-  // 离线设备统计
-  initGaugeChart(deviceOfflineChartRef.value, statsData.value.deviceOfflineCount, '#f50')
-  // 待激活设备统计
-  initGaugeChart(deviceActiveChartRef.value, statsData.value.deviceInactiveCount, '#05b')
-
-  // 消息量统计
-  initMessageChart()
-}
-
-/** 初始化仪表盘图表 */
-const initGaugeChart = (el: any, value: number, color: string) => {
-  echarts.init(el).setOption({
-    series: [
-      {
-        type: 'gauge',
-        startAngle: 360,
-        endAngle: 0,
-        min: 0,
-        max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
-        progress: {
-          show: true,
-          width: 12,
-          itemStyle: {
-            color: color
-          }
-        },
-        axisLine: {
-          lineStyle: {
-            width: 12,
-            color: [[1, '#E5E7EB']]
-          }
-        },
-        axisTick: { show: false },
-        splitLine: { show: false },
-        axisLabel: { show: false },
-        pointer: { show: false },
-        anchor: { show: false },
-        title: { show: false },
-        detail: {
-          valueAnimation: true,
-          fontSize: 24,
-          fontWeight: 'bold',
-          fontFamily: 'Inter, sans-serif',
-          color: color,
-          offsetCenter: [0, '0'],
-          formatter: (value: number) => {
-            return `${value} 个`
-          }
-        },
-        data: [{ value: value }]
-      }
-    ]
-  })
-}
-
-/** 初始化消息统计图表 */
-const initMessageChart = () => {
-  // 获取所有时间戳并排序
-  // TODO @super:一些 idea 里的红色报错,要去处理掉噢。
-  const timestamps = Array.from(
-    new Set([
-      ...messageStats.value.upstreamCounts.map((item) => Number(Object.keys(item)[0])),
-      ...messageStats.value.downstreamCounts.map((item) => Number(Object.keys(item)[0]))
-    ])
-  ).sort((a, b) => a - b) // 确保时间戳从小到大排序
-
-  // 准备数据
-  const xdata = timestamps.map((ts) => formatDate(ts, 'YYYY-MM-DD HH:mm'))
-  const upData = timestamps.map((ts) => {
-    const item = messageStats.value.upstreamCounts.find(
-      (count) => Number(Object.keys(count)[0]) === ts
-    )
-    return item ? Object.values(item)[0] : 0
-  })
-  const downData = timestamps.map((ts) => {
-    const item = messageStats.value.downstreamCounts.find(
-      (count) => Number(Object.keys(count)[0]) === ts
-    )
-    return item ? Object.values(item)[0] : 0
-  })
-
-  // 配置图表
-  echarts.init(deviceMessageCountChartRef.value).setOption({
-    tooltip: {
-      trigger: 'axis',
-      backgroundColor: 'rgba(255, 255, 255, 0.9)',
-      borderColor: '#E5E7EB',
-      textStyle: {
-        color: '#374151'
-      }
-    },
-    legend: {
-      data: ['上行消息量', '下行消息量'],
-      textStyle: {
-        color: '#374151',
-        fontWeight: 500
-      }
-    },
-    grid: {
-      left: '3%',
-      right: '4%',
-      bottom: '3%',
-      containLabel: true
-    },
-    xAxis: {
-      type: 'category',
-      boundaryGap: false,
-      data: xdata,
-      axisLine: {
-        lineStyle: {
-          color: '#E5E7EB'
-        }
-      },
-      axisLabel: {
-        color: '#6B7280'
-      }
-    },
-    yAxis: {
-      type: 'value',
-      axisLine: {
-        lineStyle: {
-          color: '#E5E7EB'
-        }
-      },
-      axisLabel: {
-        color: '#6B7280'
-      },
-      splitLine: {
-        lineStyle: {
-          color: '#F3F4F6'
-        }
-      }
-    },
-    series: [
-      {
-        name: '上行消息量',
-        type: 'line',
-        smooth: true, // 添加平滑曲线
-        data: upData,
-        itemStyle: {
-          color: '#3B82F6'
-        },
-        lineStyle: {
-          width: 2
-        },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
-            { offset: 1, color: 'rgba(59, 130, 246, 0)' }
-          ])
-        }
-      },
-      {
-        name: '下行消息量',
-        type: 'line',
-        smooth: true, // 添加平滑曲线
-        data: downData,
-        itemStyle: {
-          color: '#10B981'
-        },
-        lineStyle: {
-          width: 2
-        },
-        areaStyle: {
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-            { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
-            { offset: 1, color: 'rgba(16, 185, 129, 0)' }
-          ])
-        }
-      }
-    ]
-  })
+  loading.value = true
+  try {
+    // 获取基础统计数据
+    statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
+    // 获取消息统计数据
+    messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
+  } catch (error) {
+    console.error('获取统计数据出错:', error)
+  } finally {
+    loading.value = false
+  }
 }
 
 /** 初始化 */