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

This commit is contained in:
YunaiV
2025-06-14 17:15:07 +08:00
parent 108782ba80
commit 69cf5d01db
8 changed files with 147 additions and 294 deletions

View File

@ -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' })

View File

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

View File

@ -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()
})
})

View File

@ -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()
})

View File

@ -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,68 +51,68 @@ 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 = () => {
// 检查是否有数据可以绘制
if (!hasData.value) return
// 确保 DOM 元素存在且已渲染
if (!messageChartRef.value) {
console.warn('图表 DOM 元素不存在')
return
}
// 配置图表
echarts.use([
LineChart,
CanvasRenderer,
@ -112,100 +121,8 @@ const initChart = () => {
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',
@ -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>

View File

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