Pārlūkot izejas kodu

feat:【IoT 物联网】设备消息统计的代码优化

YunaiV 7 mēneši atpakaļ
vecāks
revīzija
69cf5d01db

+ 20 - 7
src/api/iot/statistics/index.ts

@@ -28,19 +28,32 @@ export interface IotStatisticsDeviceMessageSummaryRespVO {
   downstreamCounts: TimeValueItem[]
 }
 
+/** 新的消息统计数据项 */
+export interface IotStatisticsDeviceMessageSummaryByDateRespVO {
+  time: string
+  upstreamCount: number
+  downstreamCount: number
+}
+
+/** 新的消息统计接口参数 */
+export interface IotStatisticsDeviceMessageReqVO {
+  interval: number
+  times?: string[]
+}
+
 // IoT 数据统计 API
-export const ProductCategoryApi = {
-  // 查询基础的数据统计
-  getIotStatisticsSummary: async () => {
+export const StatisticsApi = {
+  // 查询全局的数据统计
+  getStatisticsSummary: async () => {
     return await request.get<IotStatisticsSummaryRespVO>({
       url: `/iot/statistics/get-summary`
     })
   },
 
-  // 查询设备上下行消息的数据统计
-  getIotStatisticsDeviceMessageSummary: async (params: { startTime: number; endTime: number }) => {
-    return await request.get<IotStatisticsDeviceMessageSummaryRespVO>({
-      url: `/iot/statistics/get-log-summary`,
+  // 获取设备消息的数据统计
+  getDeviceMessageSummaryByDate: async (params: IotStatisticsDeviceMessageReqVO) => {
+    return await request.get<IotStatisticsDeviceMessageSummaryByDateRespVO[]>({
+      url: `/iot/statistics/get-device-message-summary-by-date`,
       params
     })
   }

+ 0 - 27
src/utils/formatTime.ts

@@ -330,30 +330,3 @@ 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()
-  }
-}

+ 0 - 1
src/views/iot/device/device/DeviceForm.vue

@@ -80,7 +80,6 @@ import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
 import { DeviceGroupApi } from '@/api/iot/device/group'
 import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
 import { UploadImg } from '@/components/UploadFile'
-import { generateRandomStr } from '@/utils'
 
 /** IoT 设备表单 */
 defineOptions({ name: 'IoTDeviceForm' })

+ 2 - 2
src/views/iot/home/components/ComparisonCard.vue

@@ -22,8 +22,8 @@
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
 
-/** 统计卡片组件 */
-defineOptions({ name: 'ComparisonCard' })
+/** 【总数 + 新增数】统计卡片组件 */
+defineOptions({ name: 'IoTComparisonCard' })
 
 const props = defineProps({
   title: propTypes.string.def('').isRequired,

+ 25 - 21
src/views/iot/home/components/DeviceCountCard.vue

@@ -24,7 +24,7 @@ import { LabelLayout } from 'echarts/features'
 import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
 import type { PropType } from 'vue'
 
-/** 设备数量统计卡片 */
+/** 设备数量统计卡片 */
 defineOptions({ name: 'DeviceCountCard' })
 
 const props = defineProps({
@@ -40,27 +40,25 @@ const props = defineProps({
 
 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({
@@ -95,10 +93,12 @@ const initChart = () => {
           labelLine: {
             show: false
           },
-          data: Object.entries(props.statsData.productCategoryDeviceCounts).map(([name, value]) => ({
-            name,
-            value
-          }))
+          data: Object.entries(props.statsData.productCategoryDeviceCounts).map(
+            ([name, value]) => ({
+              name,
+              value
+            })
+          )
         }
       ]
     })
@@ -109,18 +109,22 @@ const initChart = () => {
   }
 }
 
-// 监听数据变化
-watch(() => props.statsData, () => {
-  // 使用 nextTick 确保 DOM 已更新
-  nextTick(() => {
-    initChart()
-  })
-}, { deep: true })
+/** 监听数据变化 */
+watch(
+  () => props.statsData,
+  () => {
+    // 使用 nextTick 确保 DOM 已更新
+    nextTick(() => {
+      initChart()
+    })
+  },
+  { deep: true }
+)
 
-// 组件挂载时初始化图表
-onMounted(() => {
+/** 组件挂载时初始化图表 */
+onMounted(async () => {
   // 使用 nextTick 确保 DOM 已更新
-  nextTick(() => {
+  await nextTick(() => {
     initChart()
   })
 })

+ 15 - 14
src/views/iot/home/components/DeviceStateCountCard.vue

@@ -41,7 +41,7 @@ import { CanvasRenderer } from 'echarts/renderers'
 import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
 import type { PropType } from 'vue'
 
-/** 设备状态统计卡片 */
+/** 设备状态统计卡片 */
 defineOptions({ name: 'DeviceStateCountCard' })
 
 const props = defineProps({
@@ -59,22 +59,21 @@ 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({
@@ -126,23 +125,21 @@ const initGaugeChart = (el: any, value: number, color: string) => {
   }
 }
 
-// 初始化所有图表
+/** 初始化所有图表 */
 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')
@@ -150,12 +147,16 @@ const initCharts = () => {
   })
 }
 
-// 监听数据变化
-watch(() => props.statsData, () => {
-  initCharts()
-}, { deep: true })
+/** 监听数据变化 */
+watch(
+  () => props.statsData,
+  () => {
+    initCharts()
+  },
+  { deep: true }
+)
 
-// 组件挂载时初始化图表
+/** 组件挂载时初始化图表 */
 onMounted(() => {
   initCharts()
 })

+ 80 - 178
src/views/iot/home/components/MessageTrendCard.vue

@@ -2,27 +2,36 @@
   <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"
-          />
+        <span class="text-base font-medium text-gray-600">消息量统计</span>
+        <div class="flex flex-wrap items-center gap-4">
+          <el-form-item label="时间范围" class="!mb-0">
+            <el-date-picker
+              v-model="queryParams.times"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              :shortcuts="defaultShortcuts"
+              class="!w-240px"
+              end-placeholder="结束日期"
+              start-placeholder="开始日期"
+              type="daterange"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              @change="handleQuery"
+            />
+          </el-form-item>
+          <el-form-item label="时间间隔" class="!mb-0">
+            <el-select
+              v-model="queryParams.interval"
+              class="!w-120px"
+              placeholder="间隔类型"
+              @change="handleQuery"
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </el-form-item>
         </div>
       </div>
     </template>
@@ -42,170 +51,78 @@ 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'
+import {
+  StatisticsApi,
+  IotStatisticsDeviceMessageSummaryByDateRespVO,
+  IotStatisticsDeviceMessageReqVO
+} from '@/api/iot/statistics'
+import { formatDate, beginOfDay, endOfDay, defaultShortcuts } from '@/utils/formatTime'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 
 /** 消息趋势统计卡片 */
 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 loading = ref(false)
+const messageData = ref<IotStatisticsDeviceMessageSummaryByDateRespVO[]>([])
+
+const queryParams = reactive<IotStatisticsDeviceMessageReqVO>({
+  interval: 1, // DAY, 日
+  times: [
+    // 默认显示最近一周的数据
+    formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+    formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+  ]
+}) // 查询参数
 
 // 是否有数据
 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
+  return messageData.value && messageData.value.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 handleQuery = () => {
+  fetchMessageData()
 }
 
-// 处理自定义日期范围选择
-const handleDateRangeChange = (value: [Date, Date] | null) => {
-  if (value) {
-    timeRange.value = ''
-    emit('timeRangeChange', {
-      startTime: value[0].getTime(),
-      endTime: value[1].getTime()
-    })
+// 获取消息统计数据
+const fetchMessageData = async () => {
+  loading.value = true
+  try {
+    messageData.value = await StatisticsApi.getDeviceMessageSummaryByDate(queryParams)
+
+    // 使用 nextTick 确保数据更新后重新渲染图表
+    await nextTick()
+    initChart()
+  } catch (error) {
+    console.error('获取消息统计数据失败:', error)
+    messageData.value = []
+  } finally {
+    loading.value = false
   }
 }
 
 // 初始化图表
 const initChart = () => {
-  echarts.use([
-    LineChart,
-    CanvasRenderer,
-    GridComponent,
-    LegendComponent,
-    TooltipComponent,
-    UniversalTransition
-  ])
-
   // 检查是否有数据可以绘制
   if (!hasData.value) return
-  
   // 确保 DOM 元素存在且已渲染
   if (!messageChartRef.value) {
-    console.warn('图表DOM元素不存在')
+    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 = []
-  }
-
-
   // 配置图表
+  echarts.use([
+    LineChart,
+    CanvasRenderer,
+    GridComponent,
+    LegendComponent,
+    TooltipComponent,
+    UniversalTransition
+  ])
   try {
     const chart = echarts.init(messageChartRef.value)
-    
     chart.setOption({
       tooltip: {
         trigger: 'axis',
@@ -231,7 +148,7 @@ const initChart = () => {
       xAxis: {
         type: 'category',
         boundaryGap: false,
-        data: xdata,
+        data: messageData.value.map((item) => item.time),
         axisLine: {
           lineStyle: {
             color: '#E5E7EB'
@@ -262,7 +179,7 @@ const initChart = () => {
           name: '上行消息量',
           type: 'line',
           smooth: true,
-          data: upData,
+          data: messageData.value.map((item) => item.upstreamCount),
           itemStyle: {
             color: '#3B82F6'
           },
@@ -280,7 +197,7 @@ const initChart = () => {
           name: '下行消息量',
           type: 'line',
           smooth: true,
-          data: downData,
+          data: messageData.value.map((item) => item.downstreamCount),
           itemStyle: {
             color: '#10B981'
           },
@@ -303,23 +220,8 @@ const initChart = () => {
   }
 }
 
-// 监听数据变化
-watch(
-  () => props.messageStats,
-  () => {
-    // 使用 nextTick 确保 DOM 已更新
-    nextTick(() => {
-      initChart()
-    })
-  },
-  { deep: true }
-)
-
-// 组件挂载时初始化图表
+/** 组件挂载时初始化 */
 onMounted(() => {
-  // 使用 nextTick 确保 DOM 已更新
-  nextTick(() => {
-    initChart()
-  })
+  fetchMessageData()
 })
 </script>

+ 5 - 44
src/views/iot/home/index.vue

@@ -56,11 +56,7 @@
   <!-- 第三行:消息统计行 -->
   <el-row>
     <el-col :span="24">
-      <MessageTrendCard 
-        :messageStats="messageStats"
-        @time-range-change="handleTimeRangeChange"
-        :loading="loading"
-      />
+      <MessageTrendCard />
     </el-col>
   </el-row>
 
@@ -68,12 +64,7 @@
 </template>
 
 <script setup lang="ts" name="Index">
-import {
-  IotStatisticsDeviceMessageSummaryRespVO,
-  IotStatisticsSummaryRespVO,
-  ProductCategoryApi
-} from '@/api/iot/statistics'
-import { getHoursAgo } from '@/utils/formatTime'
+import { IotStatisticsSummaryRespVO, StatisticsApi } from '@/api/iot/statistics'
 import ComparisonCard from './components/ComparisonCard.vue'
 import DeviceCountCard from './components/DeviceCountCard.vue'
 import DeviceStateCountCard from './components/DeviceStateCountCard.vue'
@@ -82,17 +73,6 @@ import MessageTrendCard from './components/MessageTrendCard.vue'
 /** IoT 首页 */
 defineOptions({ name: 'IoTHome' })
 
-// TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
-
-
-const queryParams = reactive({
-  startTime: getHoursAgo( 7 * 24 ), // 设置默认开始时间为 7 天前
-  endTime: Date.now() // 设置默认结束时间为当前时间
-})
-
-
-// 基础统计数据
-// TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
 const statsData = ref<IotStatisticsSummaryRespVO>({
   productCategoryCount: -1,
   productCount: -1,
@@ -106,33 +86,16 @@ const statsData = ref<IotStatisticsSummaryRespVO>({
   deviceOfflineCount: -1,
   deviceInactiveCount: -1,
   productCategoryDeviceCounts: {}
-})
-
-// 消息统计数据
-const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
-  statType: 0,
-  upstreamCounts: [],
-  downstreamCounts: []
-})
+}) // 基础统计数据
 
-// 加载状态
-const loading = ref(true)
-
-/** 处理时间范围变化 */
-const handleTimeRangeChange = (params: { startTime: number; endTime: number }) => {
-  queryParams.startTime = params.startTime
-  queryParams.endTime = params.endTime
-  getStats()
-}
+const loading = ref(true) // 加载状态
 
 /** 获取统计数据 */
 const getStats = async () => {
   loading.value = true
   try {
     // 获取基础统计数据
-    statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
-    // 获取消息统计数据
-    messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
+    statsData.value = await StatisticsApi.getStatisticsSummary()
   } catch (error) {
     console.error('获取统计数据出错:', error)
   } finally {
@@ -145,5 +108,3 @@ onMounted(() => {
   getStats()
 })
 </script>
-
-<style lang="scss" scoped></style>