@@ -33,6 +39,7 @@ 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' })
@@ -41,6 +48,10 @@ const props = defineProps({
statsData: {
type: Object as PropType
,
required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
}
})
@@ -48,63 +59,95 @@ 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])
- 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: {
+ 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,
- 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 }]
- }
- ]
- })
+ 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 = () => {
- // 在线设备统计
- initGaugeChart(deviceOnlineCountChartRef.value, props.statsData.deviceOnlineCount, '#0d9')
- // 离线设备统计
- initGaugeChart(deviceOfflineChartRef.value, props.statsData.deviceOfflineCount, '#f50')
- // 待激活设备统计
- initGaugeChart(deviceActiveChartRef.value, props.statsData.deviceInactiveCount, '#05b')
+ // 如果没有数据,则不初始化图表
+ 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')
+ }
+ })
}
// 监听数据变化
diff --git a/src/views/iot/home/components/MessageTrendCard.vue b/src/views/iot/home/components/MessageTrendCard.vue
index 63e4417a9..c8b2d0b4e 100644
--- a/src/views/iot/home/components/MessageTrendCard.vue
+++ b/src/views/iot/home/components/MessageTrendCard.vue
@@ -1,8 +1,13 @@
-
+
-
上下行消息量统计
+
+ 上下行消息量统计
+
+ {{ props.messageStats.statType === 1 ? '(按天)' : '(按小时)' }}
+
+
最近8小时
@@ -21,7 +26,13 @@
-
+
+
+
+
+
+
+
@@ -43,6 +54,10 @@ const props = defineProps({
messageStats: {
type: Object as PropType,
required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
}
})
@@ -52,6 +67,20 @@ const timeRange = ref('7d')
const dateRange = ref(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) => {
@@ -84,6 +113,15 @@ const initChart = () => {
UniversalTransition
])
+ // 检查是否有数据可以绘制
+ if (!hasData.value) return
+
+ // 确保 DOM 元素存在且已渲染
+ if (!messageChartRef.value) {
+ console.warn('图表DOM元素不存在')
+ return
+ }
+
// 检查数据格式并转换
const upstreamCounts = Array.isArray(props.messageStats.upstreamCounts)
@@ -117,9 +155,19 @@ const initChart = () => {
timestamps = []
}
+ console.log('时间戳:', timestamps)
- // 准备数据
- const xdata = timestamps.map((ts) => formatDate(dayjs(ts).toDate(), 'YYYY-MM-DD HH:mm'))
+ // 准备数据 - 根据 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[] = []
@@ -155,110 +203,123 @@ const initChart = () => {
// 配置图表
- 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'
+ try {
+ const chart = echarts.init(messageChartRef.value)
+
+ chart.setOption({
+ tooltip: {
+ trigger: 'axis',
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
+ borderColor: '#E5E7EB',
+ textStyle: {
+ color: '#374151'
}
},
- axisLabel: {
- color: '#6B7280'
- }
- },
- yAxis: {
- type: 'value',
- axisLine: {
- lineStyle: {
- color: '#E5E7EB'
+ legend: {
+ data: ['上行消息量', '下行消息量'],
+ textStyle: {
+ color: '#374151',
+ fontWeight: 500
}
},
- axisLabel: {
- color: '#6B7280'
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
},
- splitLine: {
- lineStyle: {
- color: '#F3F4F6'
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: xdata,
+ axisLine: {
+ lineStyle: {
+ color: '#E5E7EB'
+ }
+ },
+ axisLabel: {
+ color: '#6B7280'
}
- }
- },
- series: [
- {
- name: '上行消息量',
- type: 'line',
- smooth: true,
- data: upData,
- itemStyle: {
- color: '#3B82F6'
+ },
+ yAxis: {
+ type: 'value',
+ axisLine: {
+ lineStyle: {
+ color: '#E5E7EB'
+ }
},
- lineStyle: {
- width: 2
+ axisLabel: {
+ 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)' }
- ])
+ splitLine: {
+ lineStyle: {
+ color: '#F3F4F6'
+ }
}
},
- {
- name: '下行消息量',
- type: 'line',
- smooth: true,
- data: downData,
- itemStyle: {
- color: '#10B981'
+ 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)' }
+ ])
+ }
},
- 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)' }
- ])
+ {
+ 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,
() => {
- initChart()
+ // 使用 nextTick 确保 DOM 已更新
+ nextTick(() => {
+ initChart()
+ })
},
{ deep: true }
)
// 组件挂载时初始化图表
onMounted(() => {
- initChart()
+ // 使用 nextTick 确保 DOM 已更新
+ nextTick(() => {
+ initChart()
+ })
})
diff --git a/src/views/iot/home/index.vue b/src/views/iot/home/index.vue
index d0d5855f9..153b20ea7 100644
--- a/src/views/iot/home/index.vue
+++ b/src/views/iot/home/index.vue
@@ -8,6 +8,7 @@
:todayCount="statsData.productCategoryTodayCount"
icon="ep:menu"
iconColor="text-blue-400"
+ :loading="loading"
/>
@@ -17,6 +18,7 @@
:todayCount="statsData.productTodayCount"
icon="ep:box"
iconColor="text-orange-400"
+ :loading="loading"
/>
@@ -26,6 +28,7 @@
:todayCount="statsData.deviceTodayCount"
icon="ep:cpu"
iconColor="text-purple-400"
+ :loading="loading"
/>
@@ -35,6 +38,7 @@
:todayCount="statsData.deviceMessageTodayCount"
icon="ep:message"
iconColor="text-teal-400"
+ :loading="loading"
/>
@@ -42,10 +46,10 @@
-
+
-
+
@@ -55,6 +59,7 @@
@@ -68,7 +73,7 @@ import {
IotStatisticsSummaryRespVO,
ProductCategoryApi
} from '@/api/iot/statistics'
-import { formatDate } from '@/utils/formatTime'
+import { getHoursAgo } from '@/utils/formatTime'
import ComparisonCard from './components/ComparisonCard.vue'
import DeviceCountCard from './components/DeviceCountCard.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 等
-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() // 设置默认结束时间为当前时间
})
@@ -91,26 +94,30 @@ const queryParams = reactive({
// 基础统计数据
// TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
const statsData = ref({
- 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({
- upstreamCounts: {},
- downstreamCounts: {}
+ statType: 0,
+ upstreamCounts: [],
+ downstreamCounts: []
})
+// 加载状态
+const loading = ref(true)
+
/** 处理时间范围变化 */
const handleTimeRangeChange = (params: { startTime: number; endTime: number }) => {
queryParams.startTime = params.startTime
@@ -120,12 +127,17 @@ const handleTimeRangeChange = (params: { startTime: number; endTime: number }) =
/** 获取统计数据 */
const getStats = async () => {
- // 获取基础统计数据
- statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
- // 获取消息统计数据
- messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
- console.log('statsData', statsData.value)
- console.log('messageStats', messageStats.value)
+ loading.value = true
+ try {
+ // 获取基础统计数据
+ statsData.value = await ProductCategoryApi.getIotStatisticsSummary()
+ // 获取消息统计数据
+ messageStats.value = await ProductCategoryApi.getIotStatisticsDeviceMessageSummary(queryParams)
+ } catch (error) {
+ console.error('获取统计数据出错:', error)
+ } finally {
+ loading.value = false
+ }
}
/** 初始化 */
--
Gitee