Refactor monitoring API and dashboard charts with improved data handling and visualization
This commit is contained in:
parent
1265cf7553
commit
4a5e37b5d9
@ -3,35 +3,40 @@ import request from '@/utils/request'
|
|||||||
/**
|
/**
|
||||||
* 按时间范围查询监测数据
|
* 按时间范围查询监测数据
|
||||||
* @param {Object} params - 查询参数
|
* @param {Object} params - 查询参数
|
||||||
* @param {string} params.start_date - 开始时间
|
* @param {string} [params.start_date] - 开始时间,格式:YYYY-MM-DD
|
||||||
* @param {string} params.end_date - 结束时间
|
* @param {string} [params.end_date] - 结束时间,格式:YYYY-MM-DD
|
||||||
* @param {Array} params.indicator_ids - 指标ID数组
|
* @param {string} [params.indicator_ids] - 指标ID,多个用逗号分隔
|
||||||
* @param {number} params.point_id - 监测点ID
|
* @param {string} [params.point_id] - 监测点ID
|
||||||
* @param {number} params.device_id - 设备ID
|
* @param {string} [params.device_id] - 设备ID
|
||||||
*/
|
*/
|
||||||
export function getMonitoringData(params) {
|
export function getMonitoringData(params) {
|
||||||
return request.get('/api/monitoring/data', { params })
|
return request.get('/api/monitoring/dataQuery', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取最新数据
|
* 获取最新数据
|
||||||
* @param {Object} params - 查询参数
|
* @param {Object} params - 查询参数
|
||||||
* @param {number} params.point_id - 监测点ID
|
* @param {string} [params.point_id] - 监测点ID
|
||||||
* @param {Array} params.indicator_ids - 指标ID数组
|
* @param {string} [params.indicator_ids] - 指标ID,多个用逗号分隔
|
||||||
* @param {number} params.device_id - 设备ID
|
* @param {string} [params.device_id] - 设备ID
|
||||||
*/
|
*/
|
||||||
export function getLatestData(params) {
|
export function getLatestData(params) {
|
||||||
return request.get('/api/monitoring/data/latest', { params })
|
// 确保 indicator_ids 是字符串格式
|
||||||
|
const formattedParams = {
|
||||||
|
...params,
|
||||||
|
indicator_ids: Array.isArray(params.indicator_ids) ? params.indicator_ids.join(',') : params.indicator_ids
|
||||||
|
}
|
||||||
|
return request.get('/api/monitoring/data/latest', { params: formattedParams })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取统计数据
|
* 获取统计数据
|
||||||
* @param {Object} params - 查询参数
|
* @param {Object} params - 查询参数
|
||||||
* @param {string} params.start_date - 开始时间
|
* @param {string} [params.start_date] - 开始时间,格式:YYYY-MM-DD
|
||||||
* @param {string} params.end_date - 结束时间
|
* @param {string} [params.end_date] - 结束时间,格式:YYYY-MM-DD
|
||||||
* @param {number} params.indicator_id - 指标ID
|
* @param {string} [params.indicator_id] - 指标ID
|
||||||
* @param {number} params.point_id - 监测点ID
|
* @param {string} [params.point_id] - 监测点ID
|
||||||
* @param {number} params.device_id - 设备ID
|
* @param {string} [params.device_id] - 设备ID
|
||||||
*/
|
*/
|
||||||
export function getStatistics(params) {
|
export function getStatistics(params) {
|
||||||
return request.get('/api/monitoring/data/statistics', { params })
|
return request.get('/api/monitoring/data/statistics', { params })
|
||||||
@ -40,9 +45,9 @@ export function getStatistics(params) {
|
|||||||
/**
|
/**
|
||||||
* 获取数据质量统计
|
* 获取数据质量统计
|
||||||
* @param {Object} params - 查询参数
|
* @param {Object} params - 查询参数
|
||||||
* @param {string} params.start_date - 开始时间
|
* @param {string} [params.start_date] - 开始时间,格式:YYYY-MM-DD
|
||||||
* @param {string} params.end_date - 结束时间
|
* @param {string} [params.end_date] - 结束时间,格式:YYYY-MM-DD
|
||||||
* @param {number} params.point_id - 监测点ID
|
* @param {string} [params.point_id] - 监测点ID
|
||||||
*/
|
*/
|
||||||
export function getQualityStatistics(params) {
|
export function getQualityStatistics(params) {
|
||||||
return request.get('/api/monitoring/data/quality-statistics', { params })
|
return request.get('/api/monitoring/data/quality-statistics', { params })
|
||||||
|
|||||||
@ -13,19 +13,109 @@ const fetchSpeciesData = async () => {
|
|||||||
try {
|
try {
|
||||||
const res = await request.get('/api/admin/species/statistics/overview')
|
const res = await request.get('/api/admin/species/statistics/overview')
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
updateSpeciesChart(res.data)
|
// 中文名称映射
|
||||||
|
const categoryNames = {
|
||||||
|
bird: '鸟类',
|
||||||
|
mammal: '哺乳类',
|
||||||
|
fish: '鱼类',
|
||||||
|
amphibian: '两栖类',
|
||||||
|
reptile: '爬行类',
|
||||||
|
insect: '昆虫类',
|
||||||
|
plant: '植物'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将数据转换为饼图所需格式
|
||||||
|
const chartData = Object.entries(res.data.categories).map(([key, value]) => ({
|
||||||
|
name: categoryNames[key],
|
||||||
|
value: parseInt(value.total_count),
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: getSpeciesColor(key)[0] },
|
||||||
|
{ offset: 1, color: getSpeciesColor(key)[1] }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})).sort((a, b) => b.value - a.value) // 按数量从大到小排序
|
||||||
|
|
||||||
|
speciesChart.setOption({
|
||||||
|
series: [{
|
||||||
|
data: chartData
|
||||||
|
}]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取物种统计数据失败:', error)
|
console.error('获取物种统计数据失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取物种类别的颜色
|
||||||
|
const getSpeciesColor = (category) => {
|
||||||
|
const colors = {
|
||||||
|
bird: ['#36CFFF', '#2861F5'],
|
||||||
|
mammal: ['#FF36D9', '#C92AF5'],
|
||||||
|
fish: ['#FFB72C', '#F5612A'],
|
||||||
|
amphibian: ['#4EF568', '#2AB256'],
|
||||||
|
reptile: ['#36FFB0', '#2AF5A1'],
|
||||||
|
insect: ['#7636FF', '#2A3CF5'],
|
||||||
|
plant: ['#FF7636', '#F52A2A']
|
||||||
|
}
|
||||||
|
return colors[category] || ['#36CFFF', '#2861F5']
|
||||||
|
}
|
||||||
|
|
||||||
// 获取巡护统计数据
|
// 获取巡护统计数据
|
||||||
const fetchPatrolData = async () => {
|
const fetchPatrolData = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await request.get('/api/admin/patrol/records/statistics/overview')
|
const res = await request.get('/api/admin/patrol/records/statistics/overview')
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
updatePatrolChart(res.data)
|
const { overview } = res.data
|
||||||
|
const chartData = [
|
||||||
|
{
|
||||||
|
name: '已完成',
|
||||||
|
value: Number(overview.completed_count) || 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#67C23A' },
|
||||||
|
{ offset: 1, color: '#67C23A99' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '进行中',
|
||||||
|
value: Number(overview.in_progress_count) || 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#409EFF' },
|
||||||
|
{ offset: 1, color: '#409EFF99' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '总任务',
|
||||||
|
value: Number(overview.total_count) || 0,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#909399' },
|
||||||
|
{ offset: 1, color: '#90939999' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
patrolChart.setOption({
|
||||||
|
xAxis: {
|
||||||
|
data: chartData.map(item => item.name)
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
data: chartData,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
color: '#fff',
|
||||||
|
formatter: function(params) {
|
||||||
|
return Math.floor(params.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取巡护统计数据失败:', error)
|
console.error('获取巡护统计数据失败:', error)
|
||||||
@ -39,54 +129,50 @@ const initSpeciesChart = () => {
|
|||||||
speciesChart = echarts.init(speciesChartRef.value)
|
speciesChart = echarts.init(speciesChartRef.value)
|
||||||
const option = {
|
const option = {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
title: {
|
|
||||||
text: '物种分布统计',
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 16
|
|
||||||
},
|
|
||||||
left: 'center',
|
|
||||||
top: 0
|
|
||||||
},
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
formatter: '{b}: {c} ({d}%)'
|
formatter: function(params) {
|
||||||
|
const percent = parseInt(params.percent)
|
||||||
|
return `${params.name}: ${parseInt(params.value)}种 (${percent}%)`
|
||||||
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
orient: 'vertical',
|
show: false
|
||||||
right: '5%',
|
|
||||||
top: 'middle',
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
|
name: '物种分布',
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
radius: ['40%', '70%'],
|
radius: ['40%', '70%'],
|
||||||
center: ['40%', '55%'],
|
center: ['50%', '50%'],
|
||||||
avoidLabelOverlap: true,
|
avoidLabelOverlap: true,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
borderColor: '#fff',
|
borderColor: 'rgba(0, 0, 0, 0.2)',
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
color: '#fff',
|
position: 'outside',
|
||||||
formatter: '{b}\n{c}种'
|
formatter: function(params) {
|
||||||
|
return `${params.name}\n${parseInt(params.value)}种`
|
||||||
},
|
},
|
||||||
emphasis: {
|
color: '#fff',
|
||||||
label: {
|
fontSize: 12
|
||||||
show: true,
|
},
|
||||||
fontSize: 16,
|
labelLine: {
|
||||||
fontWeight: 'bold'
|
length: 15,
|
||||||
|
length2: 0,
|
||||||
|
maxSurfaceAngle: 80,
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data: []
|
data: []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
speciesChart.setOption(option)
|
speciesChart.setOption(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,19 +183,13 @@ const initPatrolChart = () => {
|
|||||||
patrolChart = echarts.init(patrolChartRef.value)
|
patrolChart = echarts.init(patrolChartRef.value)
|
||||||
const option = {
|
const option = {
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
title: {
|
|
||||||
text: '巡护任务统计',
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 16
|
|
||||||
},
|
|
||||||
left: 'center',
|
|
||||||
top: 0
|
|
||||||
},
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'shadow'
|
type: 'shadow'
|
||||||
|
},
|
||||||
|
formatter: function(params) {
|
||||||
|
return `${params[0].name}: ${Math.floor(params[0].value)}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
@ -121,7 +201,7 @@ const initPatrolChart = () => {
|
|||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: ['已完成', '进行中', '未开始', '已超时'],
|
data: ['已完成', '进行中', '总任务'],
|
||||||
axisLine: {
|
axisLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'rgba(255, 255, 255, 0.3)'
|
color: 'rgba(255, 255, 255, 0.3)'
|
||||||
@ -134,6 +214,9 @@ const initPatrolChart = () => {
|
|||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
|
minInterval: 1,
|
||||||
|
splitNumber: 4,
|
||||||
|
min: 0,
|
||||||
splitLine: {
|
splitLine: {
|
||||||
lineStyle: {
|
lineStyle: {
|
||||||
color: 'rgba(255, 255, 255, 0.1)',
|
color: 'rgba(255, 255, 255, 0.1)',
|
||||||
@ -141,7 +224,10 @@ const initPatrolChart = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: '#fff'
|
color: '#fff',
|
||||||
|
formatter: function(value) {
|
||||||
|
return Math.floor(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
@ -154,7 +240,10 @@ const initPatrolChart = () => {
|
|||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
color: '#fff'
|
color: '#fff',
|
||||||
|
formatter: function(params) {
|
||||||
|
return parseInt(params.value || 0) + '个'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data: []
|
data: []
|
||||||
}
|
}
|
||||||
@ -163,62 +252,6 @@ const initPatrolChart = () => {
|
|||||||
patrolChart.setOption(option)
|
patrolChart.setOption(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新物种统计图表
|
|
||||||
const updateSpeciesChart = (data) => {
|
|
||||||
const colors = {
|
|
||||||
'鸟类': '#36CFFF',
|
|
||||||
'鱼类': '#FFB72C',
|
|
||||||
'两栖类': '#4EF568',
|
|
||||||
'爬行类': '#FF36D9',
|
|
||||||
'哺乳类': '#9E87FF'
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData = Object.entries(data.category_counts || {}).map(([name, value]) => ({
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
itemStyle: {
|
|
||||||
color: colors[name] || '#36CFFF'
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
speciesChart.setOption({
|
|
||||||
series: [{
|
|
||||||
data: chartData
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新巡护统计图表
|
|
||||||
const updatePatrolChart = (data) => {
|
|
||||||
const colors = {
|
|
||||||
'已完成': '#67C23A',
|
|
||||||
'进行中': '#409EFF',
|
|
||||||
'未开始': '#909399',
|
|
||||||
'已超时': '#F56C6C'
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData = [
|
|
||||||
{ name: '已完成', value: data.completed || 0 },
|
|
||||||
{ name: '进行中', value: data.in_progress || 0 },
|
|
||||||
{ name: '未开始', value: data.not_started || 0 },
|
|
||||||
{ name: '已超时', value: data.overdue || 0 }
|
|
||||||
].map(item => ({
|
|
||||||
value: item.value,
|
|
||||||
itemStyle: {
|
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{ offset: 0, color: colors[item.name] },
|
|
||||||
{ offset: 1, color: colors[item.name].replace('FF', '99') }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
patrolChart.setOption({
|
|
||||||
series: [{
|
|
||||||
data: chartData
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动刷新数据
|
// 自动刷新数据
|
||||||
let timer = null
|
let timer = null
|
||||||
const startAutoRefresh = () => {
|
const startAutoRefresh = () => {
|
||||||
|
|||||||
@ -1,17 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import { getMonitoringData, getLatestData, getStatistics, getQualityStatistics } from '@/api/monitoring'
|
import { getLatestData } from '@/api/monitoring'
|
||||||
|
|
||||||
// 图表实例
|
// 图表实例
|
||||||
let trendChart = null
|
let chart = null
|
||||||
let qualityChart = null
|
const chartRef = ref(null)
|
||||||
let indicatorChart = null
|
|
||||||
|
|
||||||
// 图表容器引用
|
|
||||||
const trendChartRef = ref(null)
|
|
||||||
const qualityChartRef = ref(null)
|
|
||||||
const indicatorChartRef = ref(null)
|
|
||||||
|
|
||||||
// 指标选项
|
// 指标选项
|
||||||
const indicatorOptions = [
|
const indicatorOptions = [
|
||||||
@ -21,39 +15,13 @@ const indicatorOptions = [
|
|||||||
{ label: '浊度', unit: 'NTU', threshold: { min: 0, max: 10 } }
|
{ label: '浊度', unit: 'NTU', threshold: { min: 0, max: 10 } }
|
||||||
]
|
]
|
||||||
|
|
||||||
// 更新实时指标图表
|
// 初始化图表
|
||||||
const updateIndicatorChart = async () => {
|
const initChart = () => {
|
||||||
if (!indicatorChart || !indicatorChartRef.value) return
|
if (!chartRef.value) return
|
||||||
|
|
||||||
try {
|
chart = echarts.init(chartRef.value)
|
||||||
const res = await getLatestData({
|
chart.setOption({
|
||||||
point_id: 1,
|
backgroundColor: 'transparent',
|
||||||
indicator_ids: [1, 2, 3, 4],
|
|
||||||
device_id: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.success && res.data) {
|
|
||||||
const data = res.data.map(item => {
|
|
||||||
const indicator = indicatorOptions[item.indicator_id - 1]
|
|
||||||
const value = Number(item.value)
|
|
||||||
const isWarning = value < indicator.threshold.min || value > indicator.threshold.max
|
|
||||||
return {
|
|
||||||
name: indicator.label,
|
|
||||||
value: value,
|
|
||||||
itemStyle: {
|
|
||||||
color: isWarning ? '#E6A23C' : '#67C23A'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
indicatorChart.setOption({
|
|
||||||
title: {
|
|
||||||
text: '实时监测指标',
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 14
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
@ -61,32 +29,97 @@ const updateIndicatorChart = async () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
left: '3%',
|
top: '10%',
|
||||||
right: '4%',
|
right: '5%',
|
||||||
bottom: '3%',
|
bottom: '10%',
|
||||||
|
left: '10%',
|
||||||
containLabel: true
|
containLabel: true
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: data.map(item => item.name),
|
data: [],
|
||||||
axisLine: { lineStyle: { color: '#fff' } },
|
axisLine: {
|
||||||
axisLabel: { color: '#fff' }
|
lineStyle: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.3)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
interval: 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'value',
|
type: 'value',
|
||||||
axisLine: { lineStyle: { color: '#fff' } },
|
splitLine: {
|
||||||
axisLabel: { color: '#fff' },
|
lineStyle: {
|
||||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
color: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
series: [
|
series: [
|
||||||
{
|
{
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: data,
|
|
||||||
barWidth: '40%',
|
barWidth: '40%',
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: [4, 4, 0, 0]
|
||||||
|
},
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
position: 'top',
|
position: 'top',
|
||||||
color: '#fff',
|
color: '#fff'
|
||||||
|
},
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表数据
|
||||||
|
const updateChart = async () => {
|
||||||
|
if (!chart || !chartRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getLatestData({
|
||||||
|
point_id: '1',
|
||||||
|
indicator_ids: '1,2,3,4',
|
||||||
|
device_id: '1'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const data = res.data.map(item => {
|
||||||
|
const indicator = indicatorOptions[Number(item.indicator_id) - 1]
|
||||||
|
const value = Number(item.value)
|
||||||
|
const isWarning = value < indicator.threshold.min || value > indicator.threshold.max
|
||||||
|
return {
|
||||||
|
name: indicator.label,
|
||||||
|
value: value,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: isWarning ? '#E6A23C' : '#67C23A' },
|
||||||
|
{ offset: 1, color: isWarning ? '#F56C6C' : '#95D475' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
chart.setOption({
|
||||||
|
xAxis: {
|
||||||
|
data: data.map(item => item.name)
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: data,
|
||||||
|
label: {
|
||||||
formatter: (params) => {
|
formatter: (params) => {
|
||||||
const indicator = indicatorOptions[params.dataIndex]
|
const indicator = indicatorOptions[params.dataIndex]
|
||||||
return params.value + (indicator.unit || '')
|
return params.value + (indicator.unit || '')
|
||||||
@ -101,204 +134,43 @@ const updateIndicatorChart = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新趋势图表
|
// 定时更新数据
|
||||||
const updateTrendChart = async () => {
|
let timer = null
|
||||||
if (!trendChart || !trendChartRef.value) return
|
const startAutoRefresh = () => {
|
||||||
|
updateChart()
|
||||||
try {
|
timer = setInterval(updateChart, 60000) // 每分钟更新一次
|
||||||
const now = new Date()
|
|
||||||
let startDate = new Date()
|
|
||||||
startDate.setHours(startDate.getHours() - 24)
|
|
||||||
|
|
||||||
const res = await getMonitoringData({
|
|
||||||
start_date: startDate.toISOString(),
|
|
||||||
end_date: now.toISOString(),
|
|
||||||
indicator_ids: [1], // 默认显示水温趋势
|
|
||||||
point_id: 1,
|
|
||||||
device_id: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.success && res.data) {
|
|
||||||
const data = res.data.map(item => [
|
|
||||||
new Date(item.timestamp).getTime(),
|
|
||||||
item.value
|
|
||||||
])
|
|
||||||
|
|
||||||
trendChart.setOption({
|
|
||||||
title: {
|
|
||||||
text: '24小时水温趋势',
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 14
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'axis'
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: '3%',
|
|
||||||
right: '4%',
|
|
||||||
bottom: '3%',
|
|
||||||
containLabel: true
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: 'time',
|
|
||||||
axisLine: { lineStyle: { color: '#fff' } },
|
|
||||||
axisLabel: { color: '#fff' }
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: 'value',
|
|
||||||
name: '°C',
|
|
||||||
nameTextStyle: { color: '#fff' },
|
|
||||||
axisLine: { lineStyle: { color: '#fff' } },
|
|
||||||
axisLabel: { color: '#fff' },
|
|
||||||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
|
||||||
},
|
|
||||||
series: [{
|
|
||||||
name: '水温',
|
|
||||||
type: 'line',
|
|
||||||
smooth: true,
|
|
||||||
symbol: 'none',
|
|
||||||
areaStyle: {
|
|
||||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
||||||
{ offset: 0, color: 'rgba(58,77,233,0.8)' },
|
|
||||||
{ offset: 1, color: 'rgba(58,77,233,0.1)' }
|
|
||||||
])
|
|
||||||
},
|
|
||||||
data: data
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取趋势数据失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据质量图表
|
|
||||||
const updateQualityChart = async () => {
|
|
||||||
if (!qualityChart || !qualityChartRef.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const now = new Date()
|
|
||||||
let startDate = new Date()
|
|
||||||
startDate.setDate(startDate.getDate() - 30)
|
|
||||||
|
|
||||||
const res = await getQualityStatistics({
|
|
||||||
start_date: startDate.toISOString(),
|
|
||||||
end_date: now.toISOString(),
|
|
||||||
point_id: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.success && res.data) {
|
|
||||||
qualityChart.setOption({
|
|
||||||
title: {
|
|
||||||
text: '数据质量分析',
|
|
||||||
textStyle: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 14
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
trigger: 'item'
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: 'vertical',
|
|
||||||
right: 10,
|
|
||||||
top: 'center',
|
|
||||||
textStyle: { color: '#fff' }
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '数据质量',
|
|
||||||
type: 'pie',
|
|
||||||
radius: ['40%', '70%'],
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: 10,
|
|
||||||
borderColor: '#fff',
|
|
||||||
borderWidth: 2
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: 'inside',
|
|
||||||
formatter: '{d}%',
|
|
||||||
fontSize: 12,
|
|
||||||
color: '#fff'
|
|
||||||
},
|
|
||||||
data: [
|
|
||||||
{ value: res.data.valid_rate, name: '有效数据', itemStyle: { color: '#67C23A' } },
|
|
||||||
{ value: res.data.invalid_rate, name: '无效数据', itemStyle: { color: '#F56C6C' } },
|
|
||||||
{ value: res.data.missing_rate, name: '缺失数据', itemStyle: { color: '#E6A23C' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取数据质量统计失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化图表
|
|
||||||
const initCharts = () => {
|
|
||||||
if (indicatorChartRef.value) {
|
|
||||||
indicatorChart = echarts.init(indicatorChartRef.value)
|
|
||||||
}
|
|
||||||
if (trendChartRef.value) {
|
|
||||||
trendChart = echarts.init(trendChartRef.value)
|
|
||||||
}
|
|
||||||
if (qualityChartRef.value) {
|
|
||||||
qualityChart = echarts.init(qualityChartRef.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理窗口大小变化
|
// 处理窗口大小变化
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
indicatorChart?.resize()
|
chart?.resize()
|
||||||
trendChart?.resize()
|
|
||||||
qualityChart?.resize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定时更新数据
|
|
||||||
let updateTimer = null
|
|
||||||
const startDataUpdate = () => {
|
|
||||||
updateIndicatorChart()
|
|
||||||
updateTimer = setInterval(() => {
|
|
||||||
updateIndicatorChart()
|
|
||||||
}, 60000) // 每分钟更新一次
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initCharts()
|
initChart()
|
||||||
startDataUpdate()
|
startAutoRefresh()
|
||||||
updateTrendChart()
|
|
||||||
updateQualityChart()
|
|
||||||
window.addEventListener('resize', handleResize)
|
window.addEventListener('resize', handleResize)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (updateTimer) {
|
if (timer) {
|
||||||
clearInterval(updateTimer)
|
clearInterval(timer)
|
||||||
}
|
}
|
||||||
window.removeEventListener('resize', handleResize)
|
window.removeEventListener('resize', handleResize)
|
||||||
indicatorChart?.dispose()
|
chart?.dispose()
|
||||||
trendChart?.dispose()
|
|
||||||
qualityChart?.dispose()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="middle-card">
|
<div class="middle-card">
|
||||||
<div class="card-title">监测数据</div>
|
<div class="card-header">
|
||||||
|
<div class="title">监测数据</div>
|
||||||
|
<div class="update-time">实时监测中</div>
|
||||||
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<!-- 实时指标图表 -->
|
<div class="chart-item">
|
||||||
<div ref="indicatorChartRef" class="indicator-chart"></div>
|
<div ref="chartRef" class="chart"></div>
|
||||||
|
</div>
|
||||||
<!-- 趋势图表 -->
|
|
||||||
<div ref="trendChartRef" class="trend-chart"></div>
|
|
||||||
|
|
||||||
<!-- 数据质量图表 -->
|
|
||||||
<div ref="qualityChartRef" class="quality-chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -307,36 +179,67 @@ onUnmounted(() => {
|
|||||||
.middle-card {
|
.middle-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: rgba(0, 0, 0, 0.2);
|
box-sizing: border-box;
|
||||||
border-radius: 8px;
|
background: rgba(6, 30, 93, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
.card-title {
|
.card-header {
|
||||||
font-size: 16px;
|
display: flex;
|
||||||
font-weight: 500;
|
justify-content: space-between;
|
||||||
color: #fff;
|
align-items: center;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: linear-gradient(to bottom, #ffffff, #3fa7dd);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-time {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #3fa7dd;
|
||||||
|
opacity: 0.8;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: #67C23A;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: blink 1s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-content {
|
.card-content {
|
||||||
height: calc(100% - 32px);
|
height: calc(100% - 60px);
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
.indicator-chart {
|
.chart-item {
|
||||||
flex: 1;
|
height: 100%;
|
||||||
min-height: 180px;
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trend-chart {
|
|
||||||
flex: 2;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quality-chart {
|
|
||||||
flex: 1;
|
|
||||||
min-height: 180px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
0% { opacity: 0.2; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
100% { opacity: 0.2; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user