feat: iothome
parent
89a8b25f17
commit
d246c780e4
|
|
@ -23,6 +23,7 @@ interface TimeValueItem {
|
||||||
|
|
||||||
/** IoT 消息统计数据类型 */
|
/** IoT 消息统计数据类型 */
|
||||||
export interface IotStatisticsDeviceMessageSummaryRespVO {
|
export interface IotStatisticsDeviceMessageSummaryRespVO {
|
||||||
|
statType: number
|
||||||
upstreamCounts: TimeValueItem[]
|
upstreamCounts: TimeValueItem[]
|
||||||
downstreamCounts: TimeValueItem[]
|
downstreamCounts: TimeValueItem[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card class="stat-card" shadow="never">
|
<el-card class="stat-card" shadow="never" :loading="loading">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex justify-between items-center mb-1">
|
<div class="flex justify-between items-center mb-1">
|
||||||
<span class="text-gray-500 text-base font-medium">{{ title }}</span>
|
<span class="text-gray-500 text-base font-medium">{{ title }}</span>
|
||||||
<Icon :icon="icon" class="text-[32px]" :class="iconColor" />
|
<Icon :icon="icon" :class="`text-[32px] ${iconColor}`" />
|
||||||
</div>
|
</div>
|
||||||
<span class="text-3xl font-bold text-gray-700">
|
<span class="text-3xl font-bold text-gray-700">
|
||||||
{{ value }}
|
<span v-if="value === -1">--</span>
|
||||||
|
<span v-else>{{ value }}</span>
|
||||||
</span>
|
</span>
|
||||||
<el-divider class="my-2" />
|
<el-divider class="my-2" />
|
||||||
<div class="flex justify-between items-center text-gray-400 text-sm">
|
<div class="flex justify-between items-center text-gray-400 text-sm">
|
||||||
<span>今日新增</span>
|
<span>今日新增</span>
|
||||||
<span class="text-green-500">+{{ todayCount }}</span>
|
<span class="text-green-500" v-if="todayCount !== -1">+{{ todayCount }}</span>
|
||||||
|
<span v-else>--</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
@ -28,6 +30,21 @@ const props = defineProps({
|
||||||
value: propTypes.number.def(0).isRequired,
|
value: propTypes.number.def(0).isRequired,
|
||||||
todayCount: propTypes.number.def(0).isRequired,
|
todayCount: propTypes.number.def(0).isRequired,
|
||||||
icon: propTypes.string.def('').isRequired,
|
icon: propTypes.string.def('').isRequired,
|
||||||
iconColor: propTypes.string.def('')
|
iconColor: propTypes.string.def(''),
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card class="chart-card" shadow="never">
|
<el-card class="chart-card" shadow="never" :loading="loading">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="text-base font-medium text-gray-600">设备数量统计</span>
|
<span class="text-base font-medium text-gray-600">设备数量统计</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="deviceCountChartRef" class="h-[240px]"></div>
|
<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>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -16,6 +22,7 @@ import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { TooltipComponent, LegendComponent } from 'echarts/components'
|
import { TooltipComponent, LegendComponent } from 'echarts/components'
|
||||||
import { LabelLayout } from 'echarts/features'
|
import { LabelLayout } from 'echarts/features'
|
||||||
import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
|
import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
|
||||||
/** 设备数量统计卡片 */
|
/** 设备数量统计卡片 */
|
||||||
defineOptions({ name: 'DeviceCountCard' })
|
defineOptions({ name: 'DeviceCountCard' })
|
||||||
|
|
@ -24,64 +31,97 @@ const props = defineProps({
|
||||||
statsData: {
|
statsData: {
|
||||||
type: Object as PropType<IotStatisticsSummaryRespVO>,
|
type: Object as PropType<IotStatisticsSummaryRespVO>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const deviceCountChartRef = ref()
|
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 = () => {
|
const initChart = () => {
|
||||||
|
// 如果没有数据,则不初始化图表
|
||||||
|
if (!hasData.value) return
|
||||||
|
|
||||||
|
// 确保 DOM 元素存在且已渲染
|
||||||
|
if (!deviceCountChartRef.value) {
|
||||||
|
console.warn('图表DOM元素不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
echarts.use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer, LabelLayout])
|
echarts.use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer, LabelLayout])
|
||||||
|
|
||||||
const chart = echarts.init(deviceCountChartRef.value)
|
try {
|
||||||
chart.setOption({
|
const chart = echarts.init(deviceCountChartRef.value)
|
||||||
tooltip: {
|
chart.setOption({
|
||||||
trigger: 'item'
|
tooltip: {
|
||||||
},
|
trigger: 'item'
|
||||||
legend: {
|
},
|
||||||
top: '5%',
|
legend: {
|
||||||
right: '10%',
|
top: '5%',
|
||||||
align: 'left',
|
right: '10%',
|
||||||
orient: 'vertical',
|
align: 'left',
|
||||||
icon: 'circle'
|
orient: 'vertical',
|
||||||
},
|
icon: 'circle'
|
||||||
series: [
|
},
|
||||||
{
|
series: [
|
||||||
name: 'Access From',
|
{
|
||||||
type: 'pie',
|
name: 'Access From',
|
||||||
radius: ['50%', '80%'],
|
type: 'pie',
|
||||||
avoidLabelOverlap: false,
|
radius: ['50%', '80%'],
|
||||||
center: ['30%', '50%'],
|
avoidLabelOverlap: false,
|
||||||
label: {
|
center: ['30%', '50%'],
|
||||||
show: false,
|
|
||||||
position: 'outside'
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: false,
|
||||||
fontSize: 20,
|
position: 'outside'
|
||||||
fontWeight: 'bold'
|
},
|
||||||
}
|
emphasis: {
|
||||||
},
|
label: {
|
||||||
labelLine: {
|
show: true,
|
||||||
show: false
|
fontSize: 20,
|
||||||
},
|
fontWeight: 'bold'
|
||||||
data: Object.entries(props.statsData.productCategoryDeviceCounts).map(([name, value]) => ({
|
}
|
||||||
name,
|
},
|
||||||
value
|
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, () => {
|
watch(() => props.statsData, () => {
|
||||||
initChart()
|
// 使用 nextTick 确保 DOM 已更新
|
||||||
|
nextTick(() => {
|
||||||
|
initChart()
|
||||||
|
})
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
// 组件挂载时初始化图表
|
// 组件挂载时初始化图表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initChart()
|
// 使用 nextTick 确保 DOM 已更新
|
||||||
|
nextTick(() => {
|
||||||
|
initChart()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card class="chart-card" shadow="never">
|
<el-card class="chart-card" shadow="never" :loading="loading">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="text-base font-medium text-gray-600">设备状态统计</span>
|
<span class="text-base font-medium text-gray-600">设备状态统计</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-row class="h-[240px]">
|
<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">
|
<el-col :span="8" class="flex flex-col items-center">
|
||||||
<div ref="deviceOnlineCountChartRef" class="h-[160px] w-full"></div>
|
<div ref="deviceOnlineCountChartRef" class="h-[160px] w-full"></div>
|
||||||
<div class="text-center mt-2">
|
<div class="text-center mt-2">
|
||||||
|
|
@ -33,6 +39,7 @@ import * as echarts from 'echarts/core'
|
||||||
import { GaugeChart } from 'echarts/charts'
|
import { GaugeChart } from 'echarts/charts'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
|
import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
|
||||||
/** 设备状态统计卡片 */
|
/** 设备状态统计卡片 */
|
||||||
defineOptions({ name: 'DeviceStateCountCard' })
|
defineOptions({ name: 'DeviceStateCountCard' })
|
||||||
|
|
@ -41,6 +48,10 @@ const props = defineProps({
|
||||||
statsData: {
|
statsData: {
|
||||||
type: Object as PropType<IotStatisticsSummaryRespVO>,
|
type: Object as PropType<IotStatisticsSummaryRespVO>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -48,63 +59,95 @@ const deviceOnlineCountChartRef = ref()
|
||||||
const deviceOfflineChartRef = ref()
|
const deviceOfflineChartRef = ref()
|
||||||
const deviceActiveChartRef = 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) => {
|
const initGaugeChart = (el: any, value: number, color: string) => {
|
||||||
|
// 确保 DOM 元素存在且已渲染
|
||||||
|
if (!el) {
|
||||||
|
console.warn('图表DOM元素不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
echarts.use([GaugeChart, CanvasRenderer])
|
echarts.use([GaugeChart, CanvasRenderer])
|
||||||
|
|
||||||
const chart = echarts.init(el)
|
try {
|
||||||
chart.setOption({
|
const chart = echarts.init(el)
|
||||||
series: [
|
chart.setOption({
|
||||||
{
|
series: [
|
||||||
type: 'gauge',
|
{
|
||||||
startAngle: 360,
|
type: 'gauge',
|
||||||
endAngle: 0,
|
startAngle: 360,
|
||||||
min: 0,
|
endAngle: 0,
|
||||||
max: props.statsData.deviceCount || 100, // 使用设备总数作为最大值
|
min: 0,
|
||||||
progress: {
|
max: props.statsData.deviceCount || 100, // 使用设备总数作为最大值
|
||||||
show: true,
|
progress: {
|
||||||
width: 12,
|
show: true,
|
||||||
itemStyle: {
|
|
||||||
color: color
|
|
||||||
}
|
|
||||||
},
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
width: 12,
|
width: 12,
|
||||||
color: [[1, '#E5E7EB']]
|
itemStyle: {
|
||||||
}
|
color: color
|
||||||
},
|
}
|
||||||
axisTick: { show: false },
|
},
|
||||||
splitLine: { show: false },
|
axisLine: {
|
||||||
axisLabel: { show: false },
|
lineStyle: {
|
||||||
pointer: { show: false },
|
width: 12,
|
||||||
anchor: { show: false },
|
color: [[1, '#E5E7EB']]
|
||||||
title: { show: false },
|
}
|
||||||
detail: {
|
},
|
||||||
valueAnimation: true,
|
axisTick: { show: false },
|
||||||
fontSize: 24,
|
splitLine: { show: false },
|
||||||
fontWeight: 'bold',
|
axisLabel: { show: false },
|
||||||
fontFamily: 'Inter, sans-serif',
|
pointer: { show: false },
|
||||||
color: color,
|
anchor: { show: false },
|
||||||
offsetCenter: [0, '0'],
|
title: { show: false },
|
||||||
formatter: (value: number) => {
|
detail: {
|
||||||
return `${value} 个`
|
valueAnimation: true,
|
||||||
}
|
fontSize: 24,
|
||||||
},
|
fontWeight: 'bold',
|
||||||
data: [{ value: value }]
|
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 = () => {
|
const initCharts = () => {
|
||||||
// 在线设备统计
|
// 如果没有数据,则不初始化图表
|
||||||
initGaugeChart(deviceOnlineCountChartRef.value, props.statsData.deviceOnlineCount, '#0d9')
|
if (!hasData.value) return
|
||||||
// 离线设备统计
|
|
||||||
initGaugeChart(deviceOfflineChartRef.value, props.statsData.deviceOfflineCount, '#f50')
|
// 使用 nextTick 确保 DOM 已更新
|
||||||
// 待激活设备统计
|
nextTick(() => {
|
||||||
initGaugeChart(deviceActiveChartRef.value, props.statsData.deviceInactiveCount, '#05b')
|
// 在线设备统计
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听数据变化
|
// 监听数据变化
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card class="chart-card" shadow="never">
|
<el-card class="chart-card" shadow="never" :loading="loading">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-base font-medium text-gray-600">上下行消息量统计</span>
|
<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">
|
<div class="flex items-center space-x-2">
|
||||||
<el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
|
<el-radio-group v-model="timeRange" @change="handleTimeRangeChange">
|
||||||
<el-radio-button label="8h">最近8小时</el-radio-button>
|
<el-radio-button label="8h">最近8小时</el-radio-button>
|
||||||
|
|
@ -21,7 +26,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="messageChartRef" class="h-[300px]"></div>
|
<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>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -43,6 +54,10 @@ const props = defineProps({
|
||||||
messageStats: {
|
messageStats: {
|
||||||
type: Object as PropType<IotStatisticsDeviceMessageSummaryRespVO>,
|
type: Object as PropType<IotStatisticsDeviceMessageSummaryRespVO>,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -52,6 +67,20 @@ const timeRange = ref('7d')
|
||||||
const dateRange = ref<any>(null)
|
const dateRange = ref<any>(null)
|
||||||
const messageChartRef = ref()
|
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 作为一个工具方法
|
// TODO @super:这个的计算,看看能不能结合 dayjs 简化。因为 1h、24h、7d 感觉是比较标准的。如果没有,抽到 utils/formatTime.ts 作为一个工具方法
|
||||||
// 处理快捷时间范围选择
|
// 处理快捷时间范围选择
|
||||||
const handleTimeRangeChange = (range: string) => {
|
const handleTimeRangeChange = (range: string) => {
|
||||||
|
|
@ -84,6 +113,15 @@ const initChart = () => {
|
||||||
UniversalTransition
|
UniversalTransition
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 检查是否有数据可以绘制
|
||||||
|
if (!hasData.value) return
|
||||||
|
|
||||||
|
// 确保 DOM 元素存在且已渲染
|
||||||
|
if (!messageChartRef.value) {
|
||||||
|
console.warn('图表DOM元素不存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// 检查数据格式并转换
|
// 检查数据格式并转换
|
||||||
const upstreamCounts = Array.isArray(props.messageStats.upstreamCounts)
|
const upstreamCounts = Array.isArray(props.messageStats.upstreamCounts)
|
||||||
|
|
@ -117,9 +155,19 @@ const initChart = () => {
|
||||||
timestamps = []
|
timestamps = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('时间戳:', timestamps)
|
||||||
|
|
||||||
// 准备数据
|
// 准备数据 - 根据 statType 确定时间格式
|
||||||
const xdata = timestamps.map((ts) => formatDate(dayjs(ts).toDate(), 'YYYY-MM-DD HH:mm'))
|
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 upData: number[] = []
|
||||||
let downData: number[] = []
|
let downData: number[] = []
|
||||||
|
|
@ -155,110 +203,123 @@ const initChart = () => {
|
||||||
|
|
||||||
|
|
||||||
// 配置图表
|
// 配置图表
|
||||||
const chart = echarts.init(messageChartRef.value)
|
try {
|
||||||
chart.setOption({
|
const chart = echarts.init(messageChartRef.value)
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis',
|
chart.setOption({
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
tooltip: {
|
||||||
borderColor: '#E5E7EB',
|
trigger: 'axis',
|
||||||
textStyle: {
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
color: '#374151'
|
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: {
|
legend: {
|
||||||
color: '#6B7280'
|
data: ['上行消息量', '下行消息量'],
|
||||||
}
|
textStyle: {
|
||||||
},
|
color: '#374151',
|
||||||
yAxis: {
|
fontWeight: 500
|
||||||
type: 'value',
|
|
||||||
axisLine: {
|
|
||||||
lineStyle: {
|
|
||||||
color: '#E5E7EB'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
axisLabel: {
|
grid: {
|
||||||
color: '#6B7280'
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
},
|
},
|
||||||
splitLine: {
|
xAxis: {
|
||||||
lineStyle: {
|
type: 'category',
|
||||||
color: '#F3F4F6'
|
boundaryGap: false,
|
||||||
}
|
data: xdata,
|
||||||
}
|
axisLine: {
|
||||||
},
|
lineStyle: {
|
||||||
series: [
|
color: '#E5E7EB'
|
||||||
{
|
}
|
||||||
name: '上行消息量',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
data: upData,
|
|
||||||
itemStyle: {
|
|
||||||
color: '#3B82F6'
|
|
||||||
},
|
},
|
||||||
lineStyle: {
|
axisLabel: {
|
||||||
width: 2
|
color: '#6B7280'
|
||||||
},
|
|
||||||
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)' }
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
yAxis: {
|
||||||
name: '下行消息量',
|
type: 'value',
|
||||||
type: 'line',
|
axisLine: {
|
||||||
smooth: true,
|
lineStyle: {
|
||||||
data: downData,
|
color: '#E5E7EB'
|
||||||
itemStyle: {
|
}
|
||||||
color: '#10B981'
|
|
||||||
},
|
},
|
||||||
lineStyle: {
|
axisLabel: {
|
||||||
width: 2
|
color: '#6B7280'
|
||||||
},
|
},
|
||||||
areaStyle: {
|
splitLine: {
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
lineStyle: {
|
||||||
{ offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
|
color: '#F3F4F6'
|
||||||
{ offset: 1, color: 'rgba(16, 185, 129, 0)' }
|
}
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
]
|
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(
|
watch(
|
||||||
() => props.messageStats,
|
() => props.messageStats,
|
||||||
() => {
|
() => {
|
||||||
initChart()
|
// 使用 nextTick 确保 DOM 已更新
|
||||||
|
nextTick(() => {
|
||||||
|
initChart()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// 组件挂载时初始化图表
|
// 组件挂载时初始化图表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initChart()
|
// 使用 nextTick 确保 DOM 已更新
|
||||||
|
nextTick(() => {
|
||||||
|
initChart()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
:todayCount="statsData.productCategoryTodayCount"
|
:todayCount="statsData.productCategoryTodayCount"
|
||||||
icon="ep:menu"
|
icon="ep:menu"
|
||||||
iconColor="text-blue-400"
|
iconColor="text-blue-400"
|
||||||
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
:todayCount="statsData.productTodayCount"
|
:todayCount="statsData.productTodayCount"
|
||||||
icon="ep:box"
|
icon="ep:box"
|
||||||
iconColor="text-orange-400"
|
iconColor="text-orange-400"
|
||||||
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
|
|
@ -26,6 +28,7 @@
|
||||||
:todayCount="statsData.deviceTodayCount"
|
:todayCount="statsData.deviceTodayCount"
|
||||||
icon="ep:cpu"
|
icon="ep:cpu"
|
||||||
iconColor="text-purple-400"
|
iconColor="text-purple-400"
|
||||||
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
|
|
@ -35,6 +38,7 @@
|
||||||
:todayCount="statsData.deviceMessageTodayCount"
|
:todayCount="statsData.deviceMessageTodayCount"
|
||||||
icon="ep:message"
|
icon="ep:message"
|
||||||
iconColor="text-teal-400"
|
iconColor="text-teal-400"
|
||||||
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
@ -42,10 +46,10 @@
|
||||||
<!-- 第二行:图表行 -->
|
<!-- 第二行:图表行 -->
|
||||||
<el-row :gutter="16" class="mb-4">
|
<el-row :gutter="16" class="mb-4">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<DeviceCountCard :statsData="statsData" />
|
<DeviceCountCard :statsData="statsData" :loading="loading" />
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<DeviceStateCountCard :statsData="statsData" />
|
<DeviceStateCountCard :statsData="statsData" :loading="loading" />
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
|
@ -55,6 +59,7 @@
|
||||||
<MessageTrendCard
|
<MessageTrendCard
|
||||||
:messageStats="messageStats"
|
:messageStats="messageStats"
|
||||||
@time-range-change="handleTimeRangeChange"
|
@time-range-change="handleTimeRangeChange"
|
||||||
|
:loading="loading"
|
||||||
/>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
@ -68,7 +73,7 @@ import {
|
||||||
IotStatisticsSummaryRespVO,
|
IotStatisticsSummaryRespVO,
|
||||||
ProductCategoryApi
|
ProductCategoryApi
|
||||||
} from '@/api/iot/statistics'
|
} from '@/api/iot/statistics'
|
||||||
import { formatDate } from '@/utils/formatTime'
|
import { getHoursAgo } from '@/utils/formatTime'
|
||||||
import ComparisonCard from './components/ComparisonCard.vue'
|
import ComparisonCard from './components/ComparisonCard.vue'
|
||||||
import DeviceCountCard from './components/DeviceCountCard.vue'
|
import DeviceCountCard from './components/DeviceCountCard.vue'
|
||||||
import DeviceStateCountCard from './components/DeviceStateCountCard.vue'
|
import DeviceStateCountCard from './components/DeviceStateCountCard.vue'
|
||||||
|
|
@ -79,11 +84,9 @@ defineOptions({ name: 'IoTHome' })
|
||||||
|
|
||||||
// TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
|
// TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
|
||||||
|
|
||||||
const timeRange = ref('7d') // 修改默认选择为近一周
|
|
||||||
const dateRange = ref<[Date, Date] | null>(null)
|
|
||||||
|
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
|
startTime: getHoursAgo( 7 * 24 ), // 设置默认开始时间为 7 天前
|
||||||
endTime: Date.now() // 设置默认结束时间为当前时间
|
endTime: Date.now() // 设置默认结束时间为当前时间
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -91,26 +94,30 @@ const queryParams = reactive({
|
||||||
// 基础统计数据
|
// 基础统计数据
|
||||||
// TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
|
// TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
|
||||||
const statsData = ref<IotStatisticsSummaryRespVO>({
|
const statsData = ref<IotStatisticsSummaryRespVO>({
|
||||||
productCategoryCount: 0,
|
productCategoryCount: -1,
|
||||||
productCount: 0,
|
productCount: -1,
|
||||||
deviceCount: 0,
|
deviceCount: -1,
|
||||||
deviceMessageCount: 0,
|
deviceMessageCount: -1,
|
||||||
productCategoryTodayCount: 0,
|
productCategoryTodayCount: -1,
|
||||||
productTodayCount: 0,
|
productTodayCount: -1,
|
||||||
deviceTodayCount: 0,
|
deviceTodayCount: -1,
|
||||||
deviceMessageTodayCount: 0,
|
deviceMessageTodayCount: -1,
|
||||||
deviceOnlineCount: 0,
|
deviceOnlineCount: -1,
|
||||||
deviceOfflineCount: 0,
|
deviceOfflineCount: -1,
|
||||||
deviceInactiveCount: 0,
|
deviceInactiveCount: -1,
|
||||||
productCategoryDeviceCounts: {}
|
productCategoryDeviceCounts: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 消息统计数据
|
// 消息统计数据
|
||||||
const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
|
const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
|
||||||
upstreamCounts: {},
|
statType: 0,
|
||||||
downstreamCounts: {}
|
upstreamCounts: [],
|
||||||
|
downstreamCounts: []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
/** 处理时间范围变化 */
|
/** 处理时间范围变化 */
|
||||||
const handleTimeRangeChange = (params: { startTime: number; endTime: number }) => {
|
const handleTimeRangeChange = (params: { startTime: number; endTime: number }) => {
|
||||||
queryParams.startTime = params.startTime
|
queryParams.startTime = params.startTime
|
||||||
|
|
@ -120,12 +127,17 @@ const handleTimeRangeChange = (params: { startTime: number; endTime: number }) =
|
||||||
|
|
||||||
/** 获取统计数据 */
|
/** 获取统计数据 */
|
||||||
const getStats = async () => {
|
const getStats = async () => {
|
||||||
// 获取基础统计数据
|
loading.value = true
|
||||||
statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
|
try {
|
||||||
// 获取消息统计数据
|
// 获取基础统计数据
|
||||||
messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
|
statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
|
||||||
console.log('statsData', statsData.value)
|
// 获取消息统计数据
|
||||||
console.log('messageStats', messageStats.value)
|
messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计数据出错:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue